😎二叉树
🍉 二叉树定义
n(n ≥ 0) 个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成
二叉树有什么特点?
- 每个结点最多有两棵子树
- 二叉树是有序的,其次序不能任意颠倒,即子树有左右之分,顺序不能颠倒
🍑 二叉树的基本术语
左斜树: 所有结点都只有左子树的二叉树
右斜树: 所有结点都只有右子树的二叉树
斜树: 左斜树和右斜树的统称
斜树有什么特点呢?
- 每一层只有一个结点
- 结点个数与其深度相同
斜树是树结构的特例,是从树结构退化成了线性结构
🍑 满二叉树
所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上的二叉树
满二叉树有什么特点呢?
- 叶子只能出现在最下一层
- 只有度为 0 和度为 2 的结点
- 在同样深度的二叉树中结点个数最多
- 在同样深度的二叉树中叶子结点个数最多
满二叉树是树结构的特例,是最丰满的二叉树
🍑 完全二叉树
在满二叉树中,从最后一个结点开始,连续去掉任意个结点得到的二叉树
完全二叉树有什么特点呢?
- 叶子结点只能出现在最下两层且最下层的叶子结点都集中在二叉树的左面
- 完全二叉树中如果有度为 1 的结点,只可能有一个,且该结点只有左孩子
- 深度为
k 的完全二叉树在 k-1 层上一定是满二叉树 - 在同样结点个数的二叉树中,完全二叉树的深度最小
🍉 二叉树的基本性质
性质 1: 在一棵二叉树中,如果叶子结点数为
n
0
n_{0}
n0?,度为 2 的结点数为
n
2
n_{2}
n2?,则有:
n
0
n_{0}
n0?=
n
2
n_{2}
n2?+1
证明:
设 n 为二叉树的结点总数,
n
1
n_{1}
n1? 为二叉树中度为 1 的结点数,则有:n=
n
0
n_{0}
n0?+
n
1
n_{1}
n1?+
n
2
n_{2}
n2? ①
在二叉树中,除了根结点外,其余结点都有唯一的一个分枝进入,一个度为 1 的结点射出一个分枝,一个度为 2 的结点射出两个分枝,所以有:n=
n
1
n_{1}
n1?+2
n
2
n_{2}
n2?+1 ②
联立①②可得
n
0
n_{0}
n0?=
n
2
n_{2}
n2?+1
性质 2: 二叉树的第 i 层上最多有
2
i
?
1
2^{i-1}
2i?1个结点(i ≥ 1)
证明:
采用归纳法证明。 当 i = 1时,只有一个根结点,而
2
i
?
1
2^{i-1}
2i?1 =
2
0
2^{0}
20=1,结论成立
假设i = k 时结论成立,即第 k 层上最多有
2
k
?
1
2^{k-1}
2k?1个结点。
考虑 i = k+1 时的情形。由于第 k+1 层上的结点是第 k 层上结点的孩子,而二叉树中每个结点最多有两个孩子,故在第 k+1 层上的最多大结点个数有2×
2
k
?
1
2^{k-1}
2k?1=
2
k
2^{k}
2k 个结点,则在 i = k+1时结论也成立。
由此,结论成立。
性质 3: 一棵深度为 k 的二叉树中,最多有
2
k
?
1
2^{k}-1
2k?1个结点
证明:
根据性质2可知,第 i 层上最多有
2
i
?
1
2^{i-1}
2i?1个结点,由此可以算出每一层最多有多少个结点,那么深度为k的二叉树,最多有
2
i
?
1
2^{i-1}
2i?1的前k项和个结点,如下
n
=
∑
i
=
1
k
(
第
i
层
上
结
点
的
最
大
个
数
)
=
∑
i
=
1
k
2
i
?
1
n =\sum_{i=1}^{k}(第i层上结点的最大个数)=\sum_{i=1}^{k}2^{i-1}
n=i=1∑k?(第i层上结点的最大个数)=i=1∑k?2i?1
2
i
?
1
2^{i-1}
2i?1是等比数列,因此可以通过等比数列的前n项和公式
a
1
(
1
?
q
n
)
1
?
q
\frac{a_{1}(1-q^{n})}{1-q}
1?qa1?(1?qn)?来求级数
∑
i
=
1
k
2
i
?
1
=
2
k
?
1
\sum_{i=1}^{k}2^{i-1}=2^{k}-1
i=1∑k?2i?1=2k?1
显然,具有
2
k
?
1
2^{k}-1
2k?1个结点的二叉树是满二叉树,且满二叉树的深度k= l
o
g
2
(
n
+
1
)
og_{2}(n+1)
og2?(n+1),推导过程如下
2
k
=
n
+
1
→
移
位
2^{k}=n+1\rightarrow 移位
2k=n+1→移位
k
=
l
o
g
2
(
n
+
1
)
→
取
对
数
k=log_{2}(n+1)\rightarrow取对数
k=log2?(n+1)→取对数
性质 4: 具有 n 个结点的完全二叉树的深度为
?
l
o
g
2
n
?
\left \lfloor log_{2}n \right \rfloor
?log2?n? +1
证明:设具有 n 个结点的完全二叉树的深度为 k,则根据性质2可知,最少有
2
k
?
1
2^{k-1}
2k?1结点。根据性质3可知,最多有
2
k
?
1
2^{k}-1
2k?1个结点,为了方便取对数,假设最多的结点不会超过
2
k
2^{k}
2k,如下
2
k
?
1
≤
n
<
2
k
2^{k-1}\leq _{}n< _{}2^{k}
2k?1≤?n<?2k
k
?
1
≤
l
o
g
2
n
<
k
→
取
对
数
k-1\leq _{}log_{2}n< _{}k\rightarrow取对数
k?1≤?log2?n<?k→取对数
l
o
g
2
n
<
k
≤
l
o
g
2
n
+
1
→
移
项
log_{2}n< _{}k\leq _{}log_{2}n+1\rightarrow移项
log2?n<?k≤?log2?n+1→移项
由
于
k
是
整
数
,
则
必
有
由于k是整数,则必有
由于k是整数,则必有
k
=
?
l
o
g
2
n
?
+
1
→
?
l
o
g
2
n
?
是
向
下
取
整
,
即
取
不
超
过
l
o
g
2
n
的
最
大
整
数
k=\left \lfloor log_{2}n \right \rfloor+1\rightarrow\left \lfloor log_{2}n \right \rfloor是向下取整,即取不超过log_{2}n的最大整数
k=?log2?n?+1→?log2?n?是向下取整,即取不超过log2?n的最大整数
性质 5: 对一棵具有 n 个结点的完全二叉树中从 1 开始按层序编号,对于任意的序号为 i(1 ≤ i ≤ n)的结点(简称结点 i),有:
① 如果i > 1,则结点i的双亲的编号为
?
i
2
?
\left \lfloor \frac{i}{2} \right \rfloor
?2i??,否则结点 i 是根结点,无双亲。
② 如果2i
≤
\leq
≤ n,则结点i的左孩子的编号为2i,否则结点 i 无左孩子
③ 如果2i+1
≤
\leq
≤ n,则该结点i的右孩子的编号为2i+1,否则结点 i 无右孩子
证明:
在证明过程中,可以从② 和③推出① ,所以先证明② 和③,采用归纳法证明
先讨论具体的情形
当 i = 1时,结点 i 就是根结点,因此无双亲。由完全二叉树的定义,其左孩子是结点2,若2>n,即不存在结点2,此时,结点 i 无左孩子,满足②。结点 i 的右孩子是结点3,若结点3不存在,即3>n,此时结点 i 无右孩子,满足③
假设i=k时结论依然成立,下面讨论i=k+1的情形
设第 j (
1
≤
j
<
?
l
o
g
2
n
?
+
1
1\leq j<\left \lfloor log_{2}n \right \rfloor+1
1≤j<?log2?n?+1)层上某个结点编号为 i (
2
i
?
1
≤
i
<
2
i
2^{i-1}\leq i< 2^{i}
2i?1≤i<2i),其左孩子为2i,右孩子为2i+1,如果结点 i 不是第 j 层最后一个结点,则结点i+1是结点 i 的右兄弟或堂兄弟。如果结点 i 是第 j 层最后一个结点,则结点i+1是第j+1层的第一个结点。若结点i+1有左孩子,则左孩子的编号必定为2i+2=2
×
(
i
+
1
)
\times(i+1)
×(i+1),若结点i+1有右孩子,则右孩子的编号必定为2i+3=2
×
(
i
+
1
)
+
1
\times(i+1)+1
×(i+1)+1,如下图所示,因此②③成立
当 i > 1时,如果 i 为左孩子,即2
×
(
i
2
)
=
i
\times(\frac{i}{2})=i
×(2i?)=i,则
i
2
\frac{i}{2}
2i?是 i 的的双亲。如果 i 为右孩子,则 i = 2j+1,即结点i的双亲应为 j ,而 j = ( i - 1 ) / 2=
?
i
2
?
\left \lfloor \frac{i}{2} \right \rfloor
?2i??,因此①成立
🍉 二叉树遍历
二叉树的遍历:从根结点出发,按照某种次序访问树中所有结点,并且每个结点仅被访问一次
按照什么次序对二叉树进行遍历呢?
二叉树由根结点(D),根结点的左子树(L)和根结点的右子树(R)三部分组成,只要依次遍历这三部分,就可以遍历整个二叉树,这三部分共有六种全排列,即第1个位置有3种可能,第2个位置有3-1=2种可能,第3个位置有3-2=1种可能,共有3 x 2 x 1 = 6种可能,如下
D
L
R
、
L
D
R
、
L
R
D
、
D
R
L
、
R
D
L
、
R
L
D
DLR、LDR、LRD、DRL、RDL、RLD
DLR、LDR、LRD、DRL、RDL、RLD
不失一般性,约定先左子树后右子树,则有前序(根)遍历,中序(根)遍历和后序(根)遍历。如果按二叉树的层序依次访问各结点,可得到另一种遍历次序,层序遍历
前序遍历
若二叉树为空,则空操作返回;否则:
- 访问根结点
- 前序遍历根结点的左子树
- 前序遍历根结点的右子树
中序遍历
若二叉树为空,则空操作返回;否则:
- 中序遍历根结点的左子树
- 访问根结点
- 中序遍历根结点的右子树
后序遍历
若二叉树为空,则空操作返回;否则:
- 后序遍历根结点的左子树
- 后序遍历根结点的右子树
- 访问根结点
层序遍历
从二叉树的根结点开始,从上至下逐层遍历,在同一层中,则按从左到右的顺序对结点逐个访问
🍉 二叉树的存储结构
🍑 顺序存储结构
二叉树的顺序存储结构是用一维数组存储二叉树的结点,结点的存储位置(下标)应能体现结点之间的逻辑关系——父子关系
对于普通的二叉树,如何顺序存储呢?
由二叉树的性质5可知,完全二叉树中结点的层序编号可以唯一地反映结点之间的逻辑关系,对于一般的二叉树,可以按照完全二叉树进行层序编号,然后再用一维数组顺序存储。具体步骤如下:
将二叉树按完全二叉树编号:
- 根结点的编号为
1 - 若某结点
i 有左孩子,则其左孩子的编号为 2i - 若某结点
i 有右孩子,则其右孩子的编号为 2i+1
缺点
显然,这种存储方法的缺点是浪费存储空间,最坏情况是右斜树,一棵深度为k的右斜树只有k个结点,由二叉树的性质3可知,需要分配
2
k
?
1
2^{k}-1
2k?1个存储单元。事实上,二叉树的顺序存储结构一般仅适合于存储完全二叉树。
🌈 代码实现
不同的二叉树的存储结构具有相同的操作,如遍历,建立树等,因此可以建立一种规范接口。如下
public interface BinaryTreeInterface<T> {
void preOrder();
void inOrder();
void postOrder();
void levelOrder();
void createBiTree(SequentialList<T> treeElements);
}
🌟 顺序存储结构实现
可在SequentialTree 类中,通过定义泛型数组 seqTree (存储数据元素)、整型常量TREE_MAX_SIZE (定义数组容量),整形变量len (存储数据元素个数),SequentialTree 类实现 BinaryTreeInterface 接口定义的方法实现二叉树顺序存储。
public class SequentialTree<T> implements BinaryTreeInterface {
protected T[] seqTree;
private int len;
private final static int TREE_MAX_SIZE = 100;
}
算法实现如下:
二叉树的初始化
通过二叉树类(SequentialTree )的构造函数实现空二叉树初始化。
public SequentialTree() {
seqTree = (T[]) new Object[TREE_MAX_SIZE];
}
建立二叉树
函数 creatBiTree 的参数为某种遍历序列的顺序表(可为任意数据类型)。依据遍历序列实现二叉树建立,当前仅针对完全二叉树的层序编号序列实现,如下
@Override
public void createBiTree(SequentialList treeElements) {
for (int i = 0; i < treeElements.getLen(); i++) {
seqTree[i] = (T) treeElements.getElementsData(i + 1);
len++;
}
}
SequentialList.java
public class SequentialList<T> {
private T[] elementsData;
public SequentialList(T[] elementsData) {
this.elementsData = elementsData;
}
public T getElementsData(int i) {
return elementsData[i - 1];
}
public int getLen() {
return elementsData.length;
}
}
🌟 递归算法实现前序,中序,后序遍历
前序遍历
二叉树对象对其自身的遍历应是无参数的(仅针对自身遍历,不允许通过外部调用遍历其他二叉树),但递归调用过程需要逐级遍历当前结点子树,以参数形式传入当前根结点引用,为此设置preOrder重载私有有参函数。
根据二叉树前序遍历的操作定义,容易写出前序遍历的递归算法,成员函数定义如下。
@Override
public void preOrder() {
if (seqTree == null || len == 0) {
return;
}
preOrder(0);
}
private void preOrder(int i) {
if (!seqTree[i].equals("^")) {
System.out.print(seqTree[i] + " ");
}
if (2 * i + 1 < len) {
preOrder(2 * i + 1);
}
if (2 * i + 2 < len) {
preOrder(2 * i + 2);
}
}
中序遍历
根据二叉树中序遍历的操作定义,访问结点的操作发生在该结点的左子树遍历完毕且尚未遍历右子树时,所以中序遍历的递归实现只需要将输出操作System. out. print( seqTree[i] + " ") 放到递归遍历左子树之后即可。
@Override
public void inOrder() {
if (seqTree == null || len == 0) {
return;
}
inOrder(0);
}
private void inOrder(int i) {
if (2 * i + 1 < len) {
inOrder(2 * i + 1);
}
if (!seqTree[i].equals("^")) {
System.out.print(seqTree[i] + " ");
}
if (2 * i + 2 < len) {
inOrder(2 * i + 2);
}
}
后序遍历
根据二叉树后序遍历的操作定义,访问结点的操作发生在该结点的左子树和右子树均遍历完毕后,所以后序遍历的递归实现只需要将输出操作System. out. print( seqTree[i] + " ") 放到递归遍历右子树之后即可。
@Override
public void postOrder() {
if (seqTree == null || len == 0) {
return;
}
postOrder(0);
}
private void postOrder(int i) {
if (2 * i + 1 < len) {
postOrder(2 * i + 1);
}
if (2 * i + 2 < len) {
postOrder(2 * i + 2);
}
if (!seqTree[i].equals("^")) {
System.out.print(seqTree[i] + " ");
}
}
🌟 非递归算法实现前序,中序,后序遍历
递归算法虽然简洁,但一般而言,其执行效率不高。因此,有时需要把递归算法转换为非递归算法。对于二叉树的遍历算法,可以仿照递归执行过程中工作栈的状态变化得到非递归算法。
前序遍历非递归算法
二叉树前序遍历非递归算法(iteratorPreOrder )的关键是:在前序遍历过某结点整个左子树后,如何找到该结点右子树的根引用
可以在访问某结点时,将该结点放进容器中,当访问某结点的左子树时,如果左子树为空时,此时应该访问当前结点的右子树。右子树可以通过当前结点间接访问到(2 * i + 2) ,因此需要取出容器最后进的结点,即为当前结点,综上此容器满足先进后出规律,可以使用栈作为容器
@Override
public void preOrder() {
if (seqTree == null || len == 0) {
return;
}
Stack<Integer> stack = new Stack<>();
Integer index = 0;
while ((index < len && !seqTree[index].equals("^")) || !stack.isEmpty()) {
while (index < len && !"^".equals(seqTree[index])) {
System.out.print(seqTree[index] + " ");
stack.push(index);
index = index * 2 + 1;
}
if (!stack.isEmpty()) {
Integer poll = stack.pop();
index = poll * 2 + 2;
}
}
}
中序遍历非递归算法
在二叉树的中序遍历中,访问结点的操作发生在该结点的左子树遍历完毕并准备遍历右子树时,所以在遍历过程中遇到某结点时并不能立即访问它,而是将它入栈,待其左子树遍历完毕,再出栈并访问。中序遍历非递归算法只需要将前序遍历非递归算法中的输出语句System.out.print(seqTree[index] + " "); 移到出栈操作之后
@Override
public void inOrder() {
if (seqTree == null || len == 0) {
return;
}
Stack<Integer> stack = new Stack<>();
Integer index = 0;
while ((index < len && !seqTree[index].equals("^")) || !stack.isEmpty()) {
while (index < len && !"^".equals(seqTree[index])) {
stack.push(index);
index = index * 2 + 1;
}
if (!stack.isEmpty()) {
Integer poll = stack.pop();
System.out.print(seqTree[poll] + " ");
index = poll * 2 + 2;
}
}
}
后序遍历非递归算法
后序遍历(iteratorPostOrder)与前序遍历和中序遍历不同。在后序遍历过程中,当遍历完左子树时,由于右子树尚未遍历,因此栈顶结点不能出栈,通过栈顶结点找到它的右子树,准备遍历它的右子树;当遍历完右子树时,将栈顶结点出栈,并访问它。
为了区别对栈顶结点的不同处理,可设置标志变量flag: flag=1 表示遍历完左子树,栈顶结点不能出栈;flag=2 表示遍历完右子树,栈顶结点可以出栈并访问。定义相应结点类。
FlagBiNode.java
public class FlagBiNode {
private Integer index;
private Integer flag;
public FlagBiNode(Integer index, Integer flag) {
this.index = index;
this.flag = flag;
}
public Integer getIndex() {
return index;
}
public void setIndex(Integer index) {
this.index = index;
}
public Integer getFlag() {
return flag;
}
public void setFlag(Integer flag) {
this.flag = flag;
}
}
算法实现
@Override
public void postOrder() {
if (seqTree == null || len == 0) {
return;
}
Stack<FlagBiNode> stack = new Stack<>();
Integer index = 0;
FlagBiNode flagBiNode;
while ((index < len && !seqTree[index].equals("^")) || !stack.isEmpty()) {
while (index < len && !"^".equals(seqTree[index])) {
flagBiNode = new FlagBiNode(index, 1);
stack.push(flagBiNode);
index = index * 2 + 1;
}
while (!stack.isEmpty() && (stack.peek().getFlag() == 2)) {
index = stack.pop().getIndex();
System.out.print(seqTree[index] + " ");
index = len;
}
if (!stack.isEmpty()) {
flagBiNode = stack.pop();
flagBiNode.setFlag(2);
stack.push(flagBiNode);
index = flagBiNode.getIndex() * 2 + 2;
}
}
}
🌟 队列实现层序遍历
在进行层序遍历时,结点访问应遵循“从上至下、从左至右”逐层访问的原则,使得先被访问结点的左,右孩子先于后被访问结点的左、右孩子被访问。为保证这种“先先”的特性,可应用队列作为辅助结构。首先根结点入队,队头出队,输出出队结点,出队结点的左、右孩子分别入队,以此类推,直至队列为空。
@Override
public void levelOrder() {
if (seqTree == null || len == 0) {
return;
}
Queue<Integer> queue = new LinkedList<>();
queue.offer(0);
while (!queue.isEmpty()) {
Integer index = queue.poll();
if (!seqTree[index].equals("^")) {
System.out.print(seqTree[index] + " ");
}
if (index * 2 + 1 < len) {
queue.offer(index * 2 + 1);
}
if (index * 2 + 2 < len) {
queue.offer(index * 2 + 2);
}
}
}
🌟 测试
public class TestBiTree {
public static void main(String[] args) {
SequentialTree<String> biTree = new SequentialTree<>();
String initialList[] = {"A", "B", "C", "^", "D", "F", "^", "^", "^", "E", "^", "^", "G"};
SequentialList<String> treeElements = new SequentialList<>(initialList);
biTree.createBiTree(treeElements);
System.out.print("二叉树前序遍历:");
biTree.preOrder();
System.out.print("\n二叉树中序遍历:");
biTree.inOrder();
System.out.print("\n二叉树后序遍历:");
biTree.postOrder();
System.out.print("\n二叉树层序遍历:");
biTree.levelOrder();
}
}
🍑 二叉链表存储结构
如何用链接存储方式存储二叉树呢?
二叉链表: 二叉树的每个结点对应一个链表结点,链表结点存放结点的数据信息和指示左右孩子的指针
🌈 代码实现
🌟 二叉链表的实现
在BinaryTree 类中,通过定义BiNode 类型根结点变量root实现二叉链表存储结构。BinaryTree 类实现 BinaryTreeInterface 接口定义的方法。
public class BinaryTree<T> implements BinaryTreeInterface {
protected BiNode<T> root;
}
结点数据结构
public class BiNode<T> {
private T data;
private BiNode leftChild;
private BiNode rightChild;
public BiNode(T data) {
this.data = data;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public BiNode getLeftChild() {
return leftChild;
}
public void setLeftChild(BiNode leftChild) {
this.leftChild = leftChild;
}
public BiNode getRightChild() {
return rightChild;
}
public void setRightChild(BiNode rightChild) {
this.rightChild = rightChild;
}
}
二叉树的初始化
通过二叉树类(BinaryTree )的构造函数实现空二叉树初始化。
public BinaryTree() {
this.root = null;
}
二叉树的建立
在内存中建立一棵二叉链表,如何输入二叉树的信息?
遍历将二叉树审视一遍,将非线性结构转换为线性结构。遍历是二叉树各种操作的基础,可以在遍历的过程中建立一棵二叉树
如何由一种遍历序列生成该二叉树?
由于前序、中序和后序序列中的任何一个都不能唯一确定一棵二叉树,因此不能直接使用。可以对二叉树作如下处理:
扩展二叉树: 将二叉树中每个结点的空指针引出一个虚结点,其值为一特定值如 ‘#’
算法实现
函数 creatBiTree 的参数为某种遍历序列的顺序表(可为任意数据类型)。依据遍历序列实现二叉树建立,当前仅针对前序遍历序列实现,调用creatByPreOrder 递归函数实现算法。creatBiTree,creatByPreOrder 算法实现如下:
@Override
public void createBiTree(SequentialList treeElements) {
root = createByPreOrder(treeElements);
}
private int elementCount = 0;
private BiNode<T> createByPreOrder(SequentialList<T> treeElements) {
BiNode node;
elementCount++;
if (elementCount > treeElements.getLen() ||
treeElements.getElementsData(elementCount).equals("#")) {
node = null;
} else {
node = new BiNode<>(treeElements.getElementsData(elementCount));
node.setLeftChild(createByPreOrder(treeElements));
node.setRightChild(createByPreOrder(treeElements));
}
return node;
}
🌟 递归算法实现前序,中序,后序遍历
递归算法实现和前面的顺序存储结构是类似的,这里不再重复阐述
前序遍历
@Override
public void preOrder() {
preOrder(root);
}
private void preOrder(BiNode node) {
if (node == null) {
return;
} else {
System.out.print(node.getData() + " ");
preOrder(node.getLeftChild());
preOrder(node.getRightChild());
}
}
中序遍历
@Override
public void inOrder() {
inOrder(root);
}
private void inOrder(BiNode node) {
if (node == null) {
return;
} else {
inOrder(node.getLeftChild());
System.out.print(node.getData() + " ");
inOrder(node.getRightChild());
}
}
后序遍历
@Override
public void postOrder() {
postOrder(root);
}
private void postOrder(BiNode node) {
if (node == null) {
return;
} else {
postOrder(node.getLeftChild());
postOrder(node.getRightChild());
System.out.print(node.getData() + " ");
}
}
🌟 非递归算法实现前序,中序,后序遍历
前序遍历非递归算法
@Override
public void preOrder() {
Stack<BiNode> stack = new Stack<>();
BiNode node = root;
while (node != null || !stack.isEmpty()) {
while (node != null) {
System.out.print(node.getData() + " ");
stack.push(node);
node = node.getLeftChild();
}
if (!stack.isEmpty()) {
node = stack.pop();
node = node.getRightChild();
}
}
}
中序遍历非递归算法
@Override
public void inOrder() {
Stack<BiNode> stack = new Stack<>();
BiNode node = root;
while (node != null || !stack.isEmpty()) {
while (node != null) {
stack.push(node);
node = node.getLeftChild();
}
if (!stack.isEmpty()) {
node = stack.pop();
System.out.print(node.getData() + " ");
node = node.getRightChild();
}
}
}
后序遍历非递归算法
同样地,后序遍历与前序遍历和中序遍历不同。在后序遍历过程中,当遍历完左子树时,由于右子树尚未遍历,因此栈顶结点不能出栈,通过栈顶结点找到它的右子树,准备遍历它的右子树;当遍历完右子树时,将栈顶结点出栈,并访问它。
为了区别对栈顶结点的不同处理,可设置标志变量flag: flag=1 表示遍历完左子树,栈顶结点不能出栈;flag=2 表示遍历完右子树,栈顶结点可以出栈并访问。定义相应结点类。
FlagBiNode.java
public class FlagBiNode {
private BiNode node;
private Integer flag;
public FlagBiNode(BiNode node, Integer flag) {
this.node = node;
this.flag = flag;
}
public BiNode getNode() {
return node;
}
public void setNode(BiNode node) {
this.node = node;
}
public Integer getFlag() {
return flag;
}
public void setFlag(Integer flag) {
this.flag = flag;
}
}
算法实现
@Override
public void postOrder() {
Stack<FlagBiNode> stack = new Stack<>();
BiNode node = root;
FlagBiNode flagNode;
while (node != null || !stack.isEmpty()) {
while (node != null) {
flagNode = new FlagBiNode(node, 1);
stack.push(flagNode);
node = node.getLeftChild();
}
while (!stack.isEmpty() && stack.peek().getFlag() == 2) {
node = stack.pop().getNode();
System.out.print(node.getData() + " ");
node = null;
}
if (!stack.isEmpty()) {
flagNode = stack.pop();
flagNode.setFlag(2);
stack.push(flagNode);
node = flagNode.getNode().getRightChild();
}
}
}
🌟 队列实现层序遍历
@Override
public void levelOrder() {
Queue<BiNode> queue = new LinkedList<>();
if (root == null) {
return;
} else {
queue.offer(root);
while (!queue.isEmpty()) {
BiNode tempNode = queue.poll();
System.out.print(tempNode.getData() + " ");
if (tempNode.getLeftChild() != null) {
queue.offer(tempNode.getLeftChild());
}
if (tempNode.getRightChild() != null) {
queue.offer(tempNode.getRightChild());
}
}
}
}
🌟 测试
public class BiTreeTest {
public static void main(String[] args) {
BinaryTree biTree = new BinaryTree();
String initialList[] = {"A", "B", "#", "D", "#", "#", "C", "#", "#"};
SequentialList<String> treeElements = new SequentialList<>(initialList);
biTree.createBiTree(treeElements);
System.out.print("二叉树前序遍历:");
biTree.preOrder();
System.out.print("\n二叉树中序遍历:");
biTree.inOrder();
System.out.print("\n二叉树后序遍历:");
biTree.postOrder();
System.out.print("\n二叉树层序遍历:");
biTree.levelOrder();
}
}
🍑 三叉链表存储结构
在二叉链表存储方式下,从某结点出发可以直接访问到它的孩子结点,但要找到它的双亲结点,则需要从根结点开始搜索,最坏情况需要遍历整个二叉链表。此时,应该采用三叉链表(trident linked list )存储二叉树。
在三叉链表中,每个结点由四个域组成,其中,data 、lchild 和 rchild 三个域的含义与二叉链表的结点结构相同;parent 域为该结点的双亲结点的引用。
缺点: 相对于二叉链表而言,它增加了空间开销
🌈 代码实现
🌟 三叉链表实现
二叉树的三叉链表存储结构比二叉链表多一个指向双亲结点的指针,因此,求双亲和左右兄弟都很容易。但在构造二叉树时要另给双亲指针赋值,从而增加了复杂度。
在BinaryTree 类中,通过定义TriNode 类型根结点变量root 实现三叉链表存储结构。BinaryTree 类实现 BinaryTreeInterface 接口定义的方法。
public class BinaryTree<T> implements BinaryTreeInterface {
protected TriNode<T> root;
}
结点数据结构
public class TriNode<T> {
private T data;
private TriNode<T> leftChild;
private TriNode<T> rightChild;
private TriNode<T> parent;
public TriNode(T data) {
this.data = data;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public TriNode<T> getLeftChild() {
return leftChild;
}
public void setLeftChild(TriNode<T> leftChild) {
this.leftChild = leftChild;
}
public TriNode<T> getRightChild() {
return rightChild;
}
public void setRightChild(TriNode<T> rightChild) {
this.rightChild = rightChild;
}
public TriNode<T> getParent() {
return parent;
}
public void setParent(TriNode<T> parent) {
this.parent = parent;
}
}
二叉树的初始化
通过二叉树类(BinaryTree )的构造函数实现空二叉树初始化。
public BinaryTree() {
this.root = null;
}
二叉树的建立
@Override
public void createBiTree(SequentialList treeElements) {
root = createByPreOrder(treeElements);
}
private int elementCount = 0;
private TriNode<T> createByPreOrder(SequentialList<T> treeElements) {
TriNode node;
elementCount++;
if (elementCount > treeElements.getLen() ||
treeElements.getElementsData(elementCount).equals("#")) {
node = null;
} else {
node = new TriNode<T>(treeElements.getElementsData(elementCount));
node.setLeftChild(createByPreOrder(treeElements));
if (node.getLeftChild() != null) {
node.getLeftChild().setParent(node);
}
if (node.getRightChild() != null) {
node.getRightChild().setParent(node);
}
node.setRightChild(createByPreOrder(treeElements));
}
return node;
}
三叉链表只是的树的建立跟二叉链表稍有不同,其他操作如遍历同二叉链表相同,故此不再重复上代码
|