第五章 树与二叉树
树的基本概念
数的定义和基本术语
- 树:从树根生发,逐级分支。
- 空树:结点数为0的树。
- 非空树的特性:
- 有且仅有一个根结点。
- 没有后继的结点称为“叶子结点”(或终端结点)。
- 有后继的结点称为“分支结点”(或非终端结点)。
- 除了根结点外,任何一个结点都有且仅有一个前驱。
- 每个结点可以有一个或多个后驱。
- 树是 n (n >= 0)个结点的有限集合,n = 0 时,称为空树,这是一种特殊情况。在任意一颗非空树中应满足:
- 有且仅有一个特定的称为根的结点。
- 当 n > 1 时,其余结点可分为 m (m > 0)个互不相交的有限集合 T1,T2,…Tm,其中每个集合本身又是一颗树,并且称为根结点的子树。
- 树是递归定义的数据结构。
- 结点之间的关系描述:
- 祖先结点
- 子孙结点
- 双亲结点(父结点)
- 孩子结点
- 兄弟结点
- 堂兄弟结点
- 两个结点之间的路径:只能从上往下
- 路径长度:经过几条边
- 结点和树的属性描述:
- 结点的层次(深度)——从上往下数
- 结点的高度——从下往上数
- 树的高度(深度)——总共多少层
- 结点的度——有几个孩子(分支)
- 树的度——各结点的度的最大值
- 有序树:逻辑上看,树中结点的各子树从左至右是有次序的,不能互换。
- 有序树:逻辑上看,树中结点的各子树从左至右是无次序的,不能互换。
- 森林:森林是 m(M >= 0)棵互不相交的树的集合。
树的性质
结点数 = 总度数 + 1
树的度——各结点度的最大值
m叉树——每个结点最多只能有m个孩子的树
度为 m 的树 m 叉树 任意结点的度 <= m(最多 m 个孩子) 任意结点的度 <= m(最多 m 个孩子) 至少有一个结点度 = m(有 m 个孩子) 允许所有结点的度都 < m 一定是非空树,至少有 m + 1 个结点 可以是空树 度为 m 的树第 i 层至多有 mi-1个结点(i >= 1)
- m 叉树第 i 层至多有mi-1个结点(i >= 1)
高度为 h 的 m 叉树至多有$\frac{m^h-1}{m-1}$(等比求和)
高度为 h 的 m 叉树至少有 h 个结点;高度为 h 同时度为 m 的树至少有 h+m-1 个结点。
具有 n 个结点的 m 叉树的最小高度为 logm(n(m-1)+1)。
二叉树
二叉树的定义和基本术语
- m 叉树是 n (n >= 0)个结点的有限集合:
- 或者为空二叉树,即 n = 0。
- 或者由一个根结点和两个互不相交的被称为根的左子树和右子树组成。左子树和右子树分别是一颗二叉树。
- 特点:
- 每个结点至多只有两棵子树
- 左右子树不能颠倒(二叉树是有序树)
- 二叉树是递归定义的数据结构。
- 二叉树的五种状态:
- 空二叉树
- 只有左子树
- 只有右子树
- 只有根结点
- 左右子树都有
- 几个特殊的二叉树:
- 满二叉树。一棵高度为 h,且含有2h-1个结点的二叉树。特点:
- 只有最后一层有叶子结点。
- 不存在度为1的结点。
- 按层序从1开始编号,结点 i 的左孩子为 2i,右孩子为 2i+1;结点 i 的父结点为 i/2。
- 完全二叉树。当且进当其每个结点都与高度为 h 的满二叉树中编号为 1~n 的结点一一对应时,称为完全二叉树。特点:
- 只有最后两层可能有叶子结点。
- 最多只有一个度为1的结点。
- 按层序从1开始编号,结点 i 的左孩子为 2i,右孩子为 2i+1;结点 i 的父结点为 i/2。
- i <= n/2 为分支结点,i > n/2 为叶子结点。
- 如果某结点只有一个孩子,那么一定是左孩子。
- 二叉排序树。一棵二叉树或者是空二叉树,或者是具有如下性质的二叉树:
- 左子树上所有结点的关键字均小于根结点的关键字。
- 右子树上所有结点的关键字均大于根结点的关键字。
- 左子树和右子树又各是一颗二叉排序树。
- 平衡二叉树。树上任一结点的左子树和右子树的深度之差不超过1。
- 平衡二叉树能有更高的搜索效率。
- 满二叉树。一棵高度为 h,且含有2h-1个结点的二叉树。特点:
二叉树的性质
- 设非空二叉树度为0、1和2的结点个数分别为n0、n1和n2,则 n0 = n2 + 1(叶子结点比二分支结点多一个)
- 二叉树第 i 层至多有 2i-1个结点(i >= 1)
- 高度为 h 的二叉树至多有$2^h-1$(等比求和)
- 具有 n 个(n > 0)结点的完全二叉树的高度 h 为 log2(n+1) 或 log2n + 1
- 对于完全二叉树,可以由结点数 n 推出度为0、1和2的结点个数分别为n0、n1和n2
- 完全二叉树最多只有一个度为1的结点,即 n1 = 0或1,而n0 = n2 + 1故n0 + n2 一定是奇数
- 若完全二叉树有 2k (偶数)个结点,则必有n1 = 1,n0 = k,n2 = k-1
- 若完全二叉树有 2k-1 (奇数)个结点,则必有n1 = 0,n0 = k,n2 = k-1
- 完全二叉树最多只有一个度为1的结点,即 n1 = 0或1,而n0 = n2 + 1故n0 + n2 一定是奇数
二叉树的存储结构
二叉树的顺序存储
顺序存储的实现
1
2
3
4
5
6
7
struct TreeNode {
ElemType value; //结点中的数据元素
bool isEmpty; //结点是否为空
}
TreeNode t[MaxSize];定义一个长度为 MaxSize 的数组 t,按照从上至下、从左至右的顺序依次存储完全二叉树中的各个结点。
几个重要的基本操作
- i 的左孩子—— 2i
- i 的右孩子—— 2i+1
- i 的父结点—— i/2
- i 所在的层次—— log2(n+1))或 log2n + 1
若完全二叉树中共有 n 个结点
- 判断 i 是否有左孩子——2i <= n
- 判断 i 是否有右孩子——2i+1 <= n
- 判断 i 是否是叶子或分支结点——i > n/2
如果不是完全二叉树,依然按层序将各结点顺序存储,则无法从结点编号反映出结点间的逻辑关系。
二叉的顺序存储中,一定要把二叉树的结点编号与完全二叉树对应起来。
最坏情况:高度为 h 且只有 h 个结点的单支树(所有结点只有右孩子),也至少需要 2h-1个存储单元。
二叉树的链式存储
链式存储的代码实现
1
2
3
4typedef struct BiTNode {
ElemType data; //数据域
struct BiTNode *lchild, *rchild; //左、右孩子指针
} BiTNode, *BiTree;n 个结点的二叉链表共有 n+1 个空链域。
二叉树的建立
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24struct ElemType {
int value;
}
typedef struct BiTNode {
ElemType data;
struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;
//定义一棵空树
BiTree root = NULL;
//插入根结点
root = (BiTree)malloc(sizeof(BiTree));
root->data = {1};
root->lchild = NULL;
root->rchild = NULL;
//插入新结点
BiTNode * p = (BiTNode *)malloc(sizeof(BiTNode));
p->data = {2};
p->lchild = NULL;
p->rchild = NULL;
root->lchild = p; //作为根结点的左孩子三叉链表——方便找父结点
1
2
3
4
5typedef struct BiTNode {
ElemType data; //数据域
struct BiTNode *lchild, *rchild; //左、右孩子指针
struct BiTNode *parent; //父结点指针
} BiTNode, *BiTree;
二叉树的先/中/后序遍历
遍历:按照某种次序把所有结点都访问一遍。
层次遍历:基于树的层次特性确定的次序规则。
先/中/后序遍历:基于树的递归特性确定的次序规则。
- 先序遍历:根左右(NLR)
- 中序遍历:左根右(LNR)
- 后序遍历:左右根(LRN)
二叉树的递归特性:
- 要么是个空二叉树。
- 要么就是由“根结点+左子树+右子树”组成的二叉树。
算术表达式的“分析树”
- 先序遍历:根左右 -> 前缀表达式
- 中序遍历:左根右 -> 中缀表达式(需要加界限符)
- 后序遍历:左右根 -> 后缀表达式
先序遍历的代码实现
- 若二叉树为空,则什么都不做
- 若二叉树非空:
- 访问根节点
- 先序遍历左子树
- 先序遍历右子树
1
2
3
4
5
6
7
8
9
10
11
12
13typedef struct BiTNode {
ElemType data;
struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;
//先序遍历
void PreOrder(BiTree T) {
if (T != NULL) {
visit(T); //访问根结点
PreOrder(T->lchild); //递归遍历左子树
PreOrder(T->rchild); //递归遍历右子树
}
}中序遍历的代码实现
- 若二叉树为空,则什么都不做
- 若二叉树非空:
- 先序遍历左子树
- 访问根节点
- 先序遍历右子树
1
2
3
4
5
6
7
8
9
10
11
12
13typedef struct BiTNode {
ElemType data;
struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;
//中序遍历
void InOrder(BiTree T) {
if (T != NULL) {
InOrder(T->lchild); //递归遍历左子树
visit(T); //访问根结点
InOrder(T->rchild); //递归遍历右子树
}
}后序遍历的代码实现
- 若二叉树为空,则什么都不做
- 若二叉树非空:
- 先序遍历左子树
- 先序遍历右子树
- 访问根节点
1
2
3
4
5
6
7
8
9
10
11
12
13typedef struct BiTNode {
ElemType data;
struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;
//后序遍历
void PostOrder(BiTree T) {
if (T != NULL) {
PostOrder(T->lchild); //递归遍历左子树
PostOrder(T->rchild); //递归遍历右子树
visit(T); //访问根结点
}
}先序遍历——第一次路过时访问结点
中序遍历——第二次路过时访问结点
后序遍历——第三次路过时访问结点
应用:求树的深度
1
2
3
4
5
6
7
8
9
10
11int treeDepth(BiTree T) {
if (T == NULL) {
return 0;
}
else {
int l = treeDepth(T->lchild);
int r = treeDepth(T->rchild);
//树的深度 = Max(左子树深度,右子树深度)+1
return l > r ? l+1 : r+1;
}
}
二叉树的层序遍历
算法思想:
- 初始化一个辅助队列
- 根结点入队
- 若队列非空,则队头结点出队,访问该结点,并将其左、右孩子插入队尾(如果有的话)
- 重复上一步直至队列为空
代码实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30//二叉树的结点(链式存储)
typedef struct BiTNode {
ElemType data;
struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;
//链式队列结点
typedef struct LinkNode {
BiTNode * data; //存指针而不是结点
struct LinkNode *next;
}LinkNode;
typedef struct {
LinkNode *front, *rear; //队头队尾
}
//层序遍历
void LevelOrder(BiTree T) {
LinkQueue Q;
InitQueue(Q); //初始化辅助队列
BiTree p;
EnQueue(Q, T); //将根结点入队
while (!IsEmpty(Q)) { //队列不空则循环
DeQueue(Q, p); //队列结点出队
visit(p); //访问出队结点
if(p->lchild != NULL)
EnQueue(Q, p->lchild); //左孩子入队
if(p->rchild != NULL)
EnQueue(Q, p->rchild); //右孩子入队
}
}
由遍历序列构造二叉树
- 一个中/前/后/层序遍历序列可能对应多种二叉树形态。中序 + 前/后/层序遍历可得到唯一二叉树。
- 由前序/后序/层序找到树的根结点,并根据中序序列划分左右子树,再找到左右子树根结点。
线索二叉树
如何找到指定结点 p 在中序遍历序列中的前驱和后继:从根结点出发,重新进行一次中序遍历,指针 q 记录当前访问的结点,指针 pre 记录上一个被访问的结点。
当 q == p 时,pre 为前驱。
当 pre == p 时,q 为后继。
中序线索二叉树:
- 前驱线索:左孩子指针充当
- 后继线索:右孩子指针充当
线索二叉树的存储结构
1
2
3
4
5
6
7
8
9
10
11
12//二叉树的结点(链式存储)
typedef struct BiTNode {
ElemType data;
struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;
//线索二叉树结点
typedef struct ThtreadNode {
ElemType data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag; //左、右线索标志 tag:1 表示线索 tag:0 表示孩子
} TreadNode, *TreadTree;
二叉树线索化
借用 pre 指针找到中序前驱
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21//中序遍历
void InOrder(BiTree T) {
if (T != NULL) {
InOrder(T->lchild); //递归遍历左子树
visit(T); //访问根结点
InOrder(T->rchild); //递归遍历右子树
}
}
//访问结点 q
void visit(BiTNode *q) {
if (q == p) //当前访问结点刚好是结点 p
final = pre; //找到 p 的前驱
else
pre = q; //pre 指向当前访问的结点
}
//辅助全局变量,用于查找结点 p 的前驱
BiTNode * p; //p 指向目标结点
BiTNode * pre == NULL; //指向当前访问的结点的前驱
BiTNode * final == NULL; //用于记录最终结果中序线索化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39//全局变量 pre,指向当前访问结点的前驱
ThreadNode *pre = NULL;
//中序线索化二叉树 T
void CreateInThread(ThreadTree T) {
pre = NULL; //pre初始化为NULL
if (T != NULL) { //非空二叉树才能线索化
InThread(T); //中序线索化二叉树
if (pre->rchild == NULL)
pre->rtag = 1; //处理遍历的最后一个结点
}
}
//线索二叉树结点
typedef struct ThtreadNode {
ElemType data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag; //左、右线索标志 tag:1 表示线索 tag:0 表示孩子
} TreadNode, *TreadTree;
//中序遍历二叉树,一边遍历一边线索化
void InThread(ThreadTree T){
if (T != NULL) {
InThread(T->lchild); //递归遍历左子树
visit(T); //访问根结点
InThread(T->rchild); //递归遍历右子树
}
}
void visit(ThreadNode *q) {
if (q->lchild == NULL) { //左子树为空,建立前驱线索
q->lchild = pre;
q->ltag = 1;
}
if (pre != NULL && pre->rchild == NULL) {
pre->rchild = q; //建立前驱结点的后继线索
pre->rtag = 1;
}
pre = q;
}先序线索化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40//全局变量 pre,指向当前访问结点的前驱
ThreadNode *pre = NULL;
//先序线索化二叉树 T
void CreatePreThread(ThreadTree T) {
pre = NULL; //pre初始化为NULL
if (T != NULL) { //非空二叉树才能线索化
PreThread(T); //先序线索化二叉树
if (pre->rchild == NULL)
pre->rtag = 1; //处理遍历的最后一个结点
}
}
//线索二叉树结点
typedef struct ThtreadNode {
ElemType data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag; //左、右线索标志 tag:1 表示线索 tag:0 表示孩子
} TreadNode, *TreadTree;
//先序遍历二叉树,一边遍历一边线索化
void PreThread(ThreadTree T){
if (T != NULL) {
visit(T); //访问根结点
if (T->ltag == 0)
PreThread(T->lchild); //递归遍历左子树
PreThread(T->rchild); //递归遍历右子树
}
}
void visit(ThreadNode *q) {
if (q->lchild == NULL) { //左子树为空,建立前驱线索
q->lchild = pre;
q->ltag = 1;
}
if (pre != NULL && pre->rchild == NULL) {
pre->rchild = q; //建立前驱结点的后继线索
pre->rtag = 1;
}
pre = q;
}后序线索化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39//全局变量 pre,指向当前访问结点的前驱
ThreadNode *pre = NULL;
//后序线索化二叉树 T
void CreatePostThread(ThreadTree T) {
pre = NULL; //pre初始化为NULL
if (T != NULL) { //非空二叉树才能线索化
PostThread(T); //后序线索化二叉树
if (pre->rchild == NULL)
pre->rtag = 1; //处理遍历的最后一个结点
}
}
//线索二叉树结点
typedef struct ThtreadNode {
ElemType data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag; //左、右线索标志 tag:1 表示线索 tag:0 表示孩子
} TreadNode, *TreadTree;
//后序遍历二叉树,一边遍历一边线索化
void PreThread(ThreadTree T){
if (T != NULL) {
PreThread(T->lchild); //递归遍历左子树
PreThread(T->rchild); //递归遍历右子树
visit(T); //访问根结点
}
}
void visit(ThreadNode *q) {
if (q->lchild == NULL) { //左子树为空,建立前驱线索
q->lchild = pre;
q->ltag = 1;
}
if (pre != NULL && pre->rchild == NULL) {
pre->rchild = q; //建立前驱结点的后继线索
pre->rtag = 1;
}
pre = q;
}
线索二叉树——找前驱/后继
在中序线索二叉树中找到指定结点 *p 的中序后继 next
- 若 p->rtag == 1,则 next = p->rchild
- 若 p->rtag == 0,则 next = p的右子树中最左下结点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22//找到以 p 为根的子树中,第一个被中序遍历的结点
ThreadNode *Firstnode(ThreadNode *p) {
//循环找到最左下结点(不一定是叶结点)
while(p->ltag == 0)
p = p->lchild;
return p;
}
//在中序线索二叉树中找到结点 p 的后继结点
ThreadNode *Nextnode(ThreadNode *p) {
//右子树中最左下结点
if (p->rtag == 0)
return Firstnode(p->rchild);
else
return p->rchild; //rtag == 1直接返回后继线索
}
//对中序线索二叉树进行中序遍历(利用线索实现的非递归算法) 空间复杂度:O(1)
void Inorder(ThreadNode *T) {
for(ThreadNode *p = Firstnode(T); p != NULL; p = Nextnode(p))
visit(p);
}在中序线索二叉树中找到指定结点 *p 的中序前驱 pre
- 若 p->ltag == 1,则 next = p->lchild
- 若 p->ltag == 0,则 next = p的左子树中最右下结点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22//找到以 p 为根的子树中,最后一个被中序遍历的结点
ThreadNode *Lastnode(ThreadNode *p) {
//循环找到最右下结点(不一定是叶结点)
while(p->ltag == 0)
p = p->lchild;
return p;
}
//在中序线索二叉树中找到结点 p 的前驱结点
ThreadNode *Prenode(ThreadNode *p) {
//左子树中最右下结点
if (p->ltag == 0)
return Firstnode(p->lchild);
else
return p->lchild; //rtag == 1直接返回后继线索
}
//对中序线索二叉树进行逆向中序遍历
void RevInorder(ThreadNode *T) {
for(ThreadNode *p = Lastnode(T); p != NULL; p = Prenode(p))
visit(p);
}在先序线索二叉树中找到指定结点 *p 的先序后继 next
- 若 p->rtag == 1,则 next = p->rchild
- 若 p->rtag == 0,则
- 若 p 有左孩子,则先序后继为左孩子
- 若 p 没有左孩子,则先序后继为右孩子
在先序线索二叉树中找到指定结点 *p 的先序前驱 pre
- 若 p->ltag == 1,则 next = p->lchild
- 若 p->ltag == 0,则
- p 必有左孩子。由于先序遍历中左右子树中的结点只可能是根的后继,不可能是前驱。只能从头开始先序遍历找前驱。
- 若改用三叉链表可以找到父节点
- p 为左孩子,则 p 的父结点即为其前驱
- p 为右孩子,
- 其左兄弟为空,则 p 的父结点即为其前驱
- 其左兄弟非空,则 p 的前驱为左兄弟子树中最后一个被先序遍历的结点
在后序线索二叉树中找到指定结点 *p 的后序前驱 pre
- 若 p->ltag == 1,则 next = p->lchild
- 若 p->ltag == 0,则 p 必有左孩子
- 若 p 有右孩子,则后序前驱为右孩子
- 若 p 没有右孩子,则后序前驱为左孩子
在后序线索二叉树中找到指定结点 *p 的先序后继 next
- 若 p->rtag == 1,则 next = p->rchild
- 若 p->rtag == 0,则
- p 必有右孩子。由于先序遍历中左右子树中的结点只可能是根的后继,不可能是前驱。只能从头开始先序遍历找前驱。
- 若改用三叉链表可以找到父节点
- p 为右孩子,则 p 的父结点即为其前驱
- p 为左孩子,
- 其右兄弟为空,则 p 的父结点即为其前驱
- 其右兄弟非空,则 p 的前驱为右兄弟子树中第一个被后序遍历的结点
树和森林
树的存储结构
双亲表表示法(顺序存储)
- 用数组顺序存储各个结点。每个结点中保存数据元素、指向双亲结点(父节点)的“指针”。
1
2
3
4
5
6
7
8
9
typedef struct { //树的结点定义
ElemType data; //数据元素
int parent; //双亲位置域
} PTNode;
typedef struct { //树的类型定义
PTNode nodes[MAX_TREE_SIZE]; //双亲表示
int n; //结点数
} PTree;优缺点:
优点:找双亲(父结点)方便
缺点:找孩子不方便,只能从头到尾遍历整个数组
适用于“找父亲“多,”找孩子“少的应用场景。如:并查集
双亲表示法也可表示森林,每棵树的根结点双亲指针 = -1。
孩子表示法(顺序存储 + 链式存储)
- 用数组顺序存储各个节点。每个结点中保存数据元素、孩子链表头指针
1
2
3
4
5
6
7
8
9
10
11
12struct CTNode {
int child; //孩子结点在数组中的位置
struct CTNode *next; //下一个孩子
};
typedef struct {
ElemType data;
struct CTNode *firstChild; //第一个孩子
} CTBox;
typedef struct {
CTBox nodes[MAX_TREE_SIZE];
int n, r;
} CTree;- 优缺点:
- 优点:找孩子很方便
- 缺点:找双亲(父结点)不方便,只能遍历每个链表
- 适用于“找父亲“少,”找孩子“多的应用场景。如:服务流程树
- 用孩子表示法存储森林,需要记录多个根的位置。
孩子兄弟表示法(链式存储)
1
2
3
4typedef struct CSNode {
ElemType data;
struct CSNode *firstchild, *nextsibling;
} CSNode, *CSTree;- 树的孩子兄弟表示法与二叉树类似,采用二叉链表实现。每个结点内保存数据元素和两个指针,但两个指针的含义和二叉树结点不同。
- 存储森林时,森林中每棵树的根结点视为平级的兄弟关系。
树、森林和二叉树的转化
- 树转换为二叉树
- 先在二叉树中,画一个根结点。
- 按“树的层序”依次处理每个结点。
- 处理一个结点的方法是:如果当前处理的结点在树中有孩子,就把所有孩子结点用右指针串成一串,并在二叉树中把第一个孩子挂在当前结点的左指针上。
- 森林转换为二叉树
- 先把所有树的根结点画出来,在二叉树中用右指针串成一串。
- 按“森林的层序”依次处理每个结点。
- 处理一个结点的方法是:如果当前处理的结点在树中有孩子,就把所有孩子结点用右指针串成一串,并在二叉树中把第一个孩子挂在当前结点的左指针上。
- 二叉树转换为树
- 先画出树的根结点
- 从树的根结点开始,按“树的层序”恢复每个结点的孩子
- 如何恢复一个结点的孩子,在二叉树中,如果当前处理的结点有左孩子,就把左孩子和一整串右指针拆下来,按顺序挂在当前结点的下面。
- 二叉树转换为森林
- 先把二叉树的根结点和一整串右指针拆下来,作为多棵树的根结点
- 按“森林的层序”恢复每个结点的孩子
- 如何恢复一个结点的孩子,在二叉树中,如果当前处理的结点有左孩子,就把左孩子和一整串右指针拆下来,按顺序挂在当前结点的下面。
树、森林的遍历
树的先根遍历(深度优先遍历)。若树非空,先访问根结点,再依次对每棵子树进行先根遍历。
1
2
3
4
5
6
7
8//树的先根遍历
void PreOrder(TreeNode *R) {
if (R != NULL) {
visit(R); //访问根结点
while (R 还有下一个子树 T)
Prerder(T); //先根遍历下一棵子树
}
}树的后根遍历(深度优先遍历)。若树非空,先依次对每棵子树进行后根遍历,最后再访问根结点。
1
2
3
4
5
6
7
8//树的后根遍历
void PostOrder(TreeNode *R) {
if (R != NULL) {
while (R 还有下一个子树 T)
Prerder(T); //后根遍历下一棵子树
visit(R); //访问根结点
}
}树的后根遍序列与这棵树相应二叉树的中序序列相同。
数的层次遍历(用队列实现)(广度优先遍历)
- 若树非空,则根结点入队
- 若队列非空,队头元素出队并访问,同时该元素的孩子依次入队
- 重复上一步直至队空
先序遍历森林。
- 若森林为非空,则按如下规则进行遍历:
- 访问森林中第一棵树的根结点
- 先序遍历第一棵树中根结点的子树森林
- 先序遍历除去第一棵树之后剩余的树构成的森林
- 效果等同于依次对二叉树的先序遍历。
- 若森林为非空,则按如下规则进行遍历:
中序遍历森林。
- 若森林为非空,则按如下规则进行遍历:
- 中序遍历森林中第一棵树的根结点的子树森林
- 访问第一棵树的根结点
- 中序遍历除去第一棵树之后剩余的树构成的森林
- 效果等同于依次对各个树进行后根遍历
- 转化为二叉树后效果等同于依次对二叉树的中序遍历
- 若森林为非空,则按如下规则进行遍历:
哈夫曼树
- 结点的权:有某种现实含义的数值(如:表示结点的重要性等)
- 结点的带权路径长度:从树的根到该结点的路径长度(经过的边数)与该结点上权值的乘积
- 树的带权路径长度:树中所有叶结点的带权路径长度之和(WPL,Weighted Path Length)
- 在含有 n 个带权叶结点的二叉树中,其中带权路径长度(WPL)最小的二叉树称为哈夫曼树,也称最优二叉树。
- 哈夫曼树的构造:给定 n 个权值分别为 w1,w2,…,wn 的结点。
- 将这 n 个结点分别作为 n 棵仅含一个结点的二叉树,构成森林 F。
- 构造一个新结点,从 F 中选取两棵根结点权值最小的树作为新结点的左、右子树,并且将新结点的权值置为左、右子树上根结点的权值之和。
- 从 F 中删除刚才选出的两棵树,同时将新得到的树加入 F 中。
- 重复以上两步,直至 F 中只剩下一棵树为止。
- 每个初始结点最终都成为叶结点,且权值越小的结点到根结点的路径长度越大。
- 哈夫曼树的结点总数为 2n-1。
- 哈夫曼树中不存在度为1的结点。
- 哈夫曼树并不唯一,但 WPL 必然相同且为最优。
- 固定长度编码——每个字符用相等长度的二进制位表示。
- 可变长度编码——允许对不同字符用不等长的二进制位表示。
- 若没有一个编码是另一个编码的前缀,则称这样的编码为前缀编码。非前缀编码有歧义。
- 由哈夫曼树得到哈夫曼编码——字符集中的每个字符作为一个叶子结点,每个字符出现的频度作为结点的权值,再构建哈夫曼树。
并查集
用互不相交的树,表示多个“集合”
- 如何“查”到一个元素到底属于哪一个集合——从指定元素出发,找到根结点
- 如何把两个集合“并”为一个集合——让一棵树成为另一棵树的子树即可
“并查集”的存储结构:参考树的双亲表示法
- 集合的两个基本操作——“并”和“查”
- Find——“查”操作:确定一个指定元素所属集合
- Union——“并”操作:将两个不相交的集合合并为一个
- 并查集(Disjoint Set)是逻辑结构——集合的一种具体实现,只进行“并”和“查”两种基本操作
- 集合的两个基本操作——“并”和“查”
“并查集”的代码实现——初始化
1
2
3
4
5
6
7
8
int UFSets[SIZE];
//初始化并查集
void Initial(int S[]) {
for (int i = 0; i < SIZE; i++)
S[i] = -1;
}“并查集”的代码实现——并、查
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15//Find “查”操作,找 x 所属集合(返回 x 所属根结点) 最坏时间复杂度:O(n)
int Find(int S[], int x) {
while(S[x] >= 0) //循环寻找 x 的根
x = S[x];
return x; //根的S[]小于0
}
//Union “并”操作,将两个集合合并为一个 时间复杂度:O(1)
void Union(int S[], int Root1, int Root2) {
//要求 Root1 与 Root2 是不同的集合
if (Root1 == Root2)
return;
//将根 Root2 连接到另一根 Root1 下面
S[Root2] = Root1;
}优化思路:在每次 Union 操作构建树的时候,尽量不让树长高
- 用根结点的绝对值表示树的结点总数(例如 -6 而不是 -1)
- Union 操作,让小树合并到大树
1
2
3
4
5
6
7
8
9
10
11
12//Union “并”操作,小树合并到大树
void Union(int S[], int Root1, int Root2) {
if (Root1 == Root2)
return;
if (S[Root2] > S[Root1]) { //Root2 结点数更少
S[Root1] += S[Root2]; //累加结点总数
S[Root2] = Root1; //小树合并到大树
} else {
S[Root2] += S[Root1]; //累加结点总数
S[Root1] = Root2; //小树合并到大树
}
}该方法构造的树高不超过 log2n+1
并查集的进一步优化
Find 操作的优化(压缩路径):先找到根结点,再将查找路径上所有结点都挂到根结点下。
1
2
3
4
5
6
7
8
9
10
11
12//Find “查”操作优化,先找到根结点,再进行“压缩路径”
int Find(int S[], int x) {
int root = x;
while (S[root] >= 0)
root = S[root]; //循环找到根
while (x != root) { //压缩路径
int t = S[x]; //t 指向 x 的父结点下
S[x] = root; //x 直接挂到根结点下
x = t;
}
return root; //返回根结点编号
}每次 Find 操作,先找根,再“压缩路径”,可使树的高度不超过 O(α(n))。α(n) 是一个增长很缓慢的函数,对于常见的 n 值,通常 α(n) <= 4,因此优化后并查集的 Find、Union 操作时间开销都很低。