1 基础介绍
1.1 树的基本概念
树的定义、根结点、树的结点、结点的路径、路径的长度、结点的度、树的度、结点点、分支结点、子结点、父结点、祖先结点、子孙结点、兄弟结点、树的深度、有序树/无序树、森林。
1.2 二叉树介绍与性质
二叉树每个结点最多两个子结点,且有左右之分,是有序树。特殊的二叉树有满二叉树,完全二叉树,左支树/右支树。重要的是记住二叉树的性质,这在编程中可能用到:
性质1 二叉树中第
i
(
i
≥
0
)
i(i \geq 0)
i(i≥0)层上的结点数最多为
2
i
2^i
2i;
性质2 深度为
h
(
h
≥
1
)
h(h \geq 1)
h(h≥1)的二叉树中最多有
2
h
−
1
2^h-1
2h−1个结点;
性质3 对于任何一颗二叉树,若其叶节点的个数为
n
0
n_0
n0,度为2的结点个数为
n
2
n_2
n2,则有
n
0
=
n
2
+
1
⟸
n
=
n
0
+
n
1
+
n
2
,
e
=
n
1
+
2
×
n
2
,
n
=
e
+
1
n_0 = n_2 + 1 \Longleftarrow n = n_0 + n_1 + n_2, e = n_1 + 2\times n_2, n = e + 1
n0=n2+1⟸n=n0+n1+n2,e=n1+2×n2,n=e+1;
性质4 具有n个结点的完全二叉树,其深度为
⌊
l
o
g
2
n
⌋
+
1
\lfloor log_2n \rfloor + 1
⌊log2n⌋+1或
⌈
l
o
g
2
(
n
+
1
)
⌉
\lceil log_2(n+1) \rceil
⌈log2(n+1)⌉
性质5 对于具有n个结点的完全二叉树,若从根节点开始自上到下并且按照层次由左向右对结点从0开始进行编号,则对于任意一个编号为
i
(
i
≤
n
)
i(i\leq n)
i(i≤n)的结点有:
- 若 i = 0 i=0 i=0,则编号为i的结点是二叉树的根节点;若 i > 1 i>1 i>1,则编号为i的结点其父节点的编号为 ⌊ i − 1 2 ⌋ \lfloor \frac{i-1}{2} \rfloor ⌊2i−1⌋;
- 若 2 i + 1 ≥ n 2i+1\geq n 2i+1≥n,则编号为 i i i的结点无左孩子,否则编号为 2 i + 1 2i+1 2i+1的结点就是其左孩子。
- 若 2 i + 2 ≥ n 2i+2\geq n 2i+2≥n,则编号为 i i i的结点无右孩子,否则编号为 2 i + 2 2i+2 2i+2的结点就是其右孩子。
1.3 二叉树的存储
二叉树存储方式有顺序存储和链式存储。完全二叉树和满二叉树适合顺序存储,其余则适合链式存储。链式存储又有二叉链表和三叉链表两种。
2 二叉树的遍历
代码中的BiTreeNode、LinkStack、LinkQueue为自己实现,不是java包中的类。
二叉树的访问有7种方式,分别为:层次遍历、DLR、LDR、LRD、DRL、RDL和RLD, 其中(L表示lChild,D表示Data,R表示rChild)。由于2-4和5-7只有左右之分,其遍历方式无本质区别,所以我们只讨论前四种遍历方式。
2.1 先根遍历(DLR)
递归方法:若二叉树为空则为空操作,否则
- 访问根节点;
- 先根遍历左子树;
- 先根遍历右子树。
public void preRootTraverse(BiTreeNode T) {
if (T != null) {
System.out.print(T.getData()); // 访问根结点
preRootTraverse(T.getLchild());// 访问左子树
preRootTraverse(T.getRchild());// 访问右子树
}
}
非递归方法:
- 创建一个栈对象,根节点入栈;
- 若栈非空,将栈顶结点弹出栈并访问该节点;
- 对当前访问结点的非空左孩子结点相继依次访问,并将当前访问结点的非空右孩子结点压入栈内;
- 重复执行步骤2.和3.,直到栈为空为止。
public void preRootTraverse() {
BiTreeNode T = root;
if (T != null) {
LinkStack S = new LinkStack();// 构造栈
S.push(T);// 根结点入栈
while (!S.isEmpty()) {
T = (BiTreeNode) S.pop();// 移除栈顶结点,并返回去值
System.out.print(T.getData()); // 访问结点
while (T != null) {
if (T.getLchild() != null) // 访问左孩子
System.out.print(T.getLchild().getData()); // 访问结点
if (T.getRchild() != null)// 右孩子非空入栈
S.push(T.getRchild());
T = T.getLchild();
}
}
}
}
2.2 中根遍历(LDR)
递归方法:若二叉树为空则为空操作,否则
- 中根遍历左子树;
- 访问根节点;
- 中根遍历右子树。
public void inRootTraverse(BiTreeNode T) {
if (T != null) {
inRootTraverse(T.getLchild());// 访问左子树
System.out.print(T.getData()); // 访问根结点
inRootTraverse(T.getRchild());// 访问右子树
}
}
非递归方法:
- 创建一个栈对象,根节点入栈;
- 当栈为非空时,将栈顶结点的非空左孩子相继入栈;
- 栈顶结点出栈并访问非空栈顶结点,并使该栈顶结点的非空右孩子结点入栈;
- 重复执行步骤2.和3.,直到栈为空为止。
public void inRootTraverse() {
BiTreeNode T = root;
if (T != null) {
LinkStack S = new LinkStack();// 构造链栈
S.push(T); // 根结点入栈
while (!S.isEmpty()) {
while (S.peek() != null)
// 将栈顶结点的所有左孩子结点入栈
S.push(((BiTreeNode) S.peek()).getLchild());
S.pop(); // 空结点退栈
if (!S.isEmpty()) {
T = (BiTreeNode) S.pop();// 移除栈顶结点,并返回去值
System.out.print(T.getData()); // 访问结点
S.push(T.getRchild());// 结点的右孩子入栈
}
}
}
}
2.3 后根遍历(LRD)
递归方法:若二叉树为空则为空操作,否则
- 后根遍历左子树;
- 后根遍历右子树;
- 访问根节点。
public void postRootTraverse(BiTreeNode T) {
if (T != null) {
postRootTraverse(T.getLchild());// 访问左子树
postRootTraverse(T.getRchild());// 访问右子树
System.out.print(T.getData()); // 访问根结点
}
}
非递归方法:
- 创建一个栈对象,根节点入栈, p赋初始值null;
- 若栈非空,将栈顶结点的非空左孩子相继入栈;
- 若栈非空,查看栈顶结点,若栈顶结点的右孩子为空,或者与p相等,则将栈顶结点弹出栈并访问它,同时使p指向该节点,并置flag值为true;否则,将栈顶结点的右孩子入栈,并置flag为false;
- 若flag值为true, 则重复执行步骤3. , 否则,重复执行步骤2.和3.,直到栈为空为止。
public void postRootTraverse() {
BiTreeNode T = root;
if (T != null) {
LinkStack S = new LinkStack();// 构造链栈
S.push(T); // 根结点进栈
Boolean flag;// 访问标记
BiTreeNode p = null;// p指向刚被访问的结点
while (!S.isEmpty()) {
while (S.peek() != null)
// 将栈顶结点的所有左孩子结点入栈
S.push(((BiTreeNode) S.peek()).getLchild());
S.pop(); // 空结点退栈
while (!S.isEmpty()) {
T = (BiTreeNode) S.peek();// 查看栈顶元素
if (T.getRchild() == null || T.getRchild() == p) {
System.out.print(T.getData()); // 访问结点
S.pop();// 移除栈顶元素
p = T;// p指向刚被访问的结点
flag = true;// 设置访问标记
} else {
S.push(T.getRchild());// 右孩子结点入栈
flag = false;// 设置未被访问标记
}
if (!flag)
break;
}
}
}
}
2.4 层次遍历
- 创建一个队列对象,根节点入队;
- 若队列非空,则将队首结点出队并访问该节点,再将该节点的非空左、右孩子结点依次入队;
- 重复执行步骤2. ,直到队列 为空为止。
public void levelTraverse() {
BiTreeNode T = root;
if (T != null) {
LinkQueue L = new LinkQueue();// 构造队列
L.offer(T);// 根结点入队列
while (!L.isEmpty()) {
T = (BiTreeNode) L.poll();
System.out.print(T.getData()); // 访问结点
if (T.getLchild() != null)// 左孩子非空,入队列
L.offer(T.getLchild());
if (T.getRchild() != null)// 右孩子非空,入队列
L.offer(T.getRchild());
}
}
}
树的应用
- 查询每个结点的值
- 求左叶子节点的和
- 求二叉树直径
- 判断树是否对称
- 判断两颗树是否相等
- 镜像翻转一颗树
- N叉树的层次遍历和中序遍历
- 后序遍历
- 求和位某个数的路径条数
- 二叉树最大深度和最小深度
- 求N树的最大深度
- 二叉树的坡度
- t是否是s的子树
- 合并两颗二叉树
……