第五章:树(考纲一览)
5.1树的基本概念
5.1.1树的定义
树是 n ( n ≥ 0 ) n(n≥0) n(n≥0)个结点的有限集,当 n = 0 n=0 n=0时称为空树,树有且仅有一个称为根的结点,除根结点以外的结点可分为 m ( m > 0 ) m(m>0) m(m>0)个互不相交的有限集 T 1 , T 2 , T 3 , . . . , T m T_1,T_2,T_3,...,T_m T1,T2,T3,...,Tm,其中每一个集合本身又是一棵树,并成为根的子树。
5.1.2基本术语
- 描述结点、树的属性
- 结点的度:结点所拥有的子树数目。
- 树的度:树内各结点度的最大值。
- 叶子结点/终端结点:度为0的结点。
- 分支结点/非终端结点:度不为0的结点。
- 结点的层次/深度:从根结点向下,根为第一层,根的孩子为第二层,以此类推,直到该结点的层。上图中 K 、 L 、 M K、L、M K、L、M同为第 4 4 4层,深度也为 4 4 4。
- 结点的高度:以该结点所有子树中最底层叶子结点为第一层向上数所得。上图中结点 C C C的高度为 2 2 2,而结点 D D D的高度为 3 3 3。
- 树的层次/高度/深度:从根结点向下,根为第一层,根的孩子为第二层,以此类推所得最大层数。
- 描述结点关系
- 双亲和孩子:结点子树的根称为该结点的孩子,相应地,该结点称为孩子的双亲。
- 兄弟:同一个双亲的孩子之间互为兄弟。
- 祖先:从根到该结点所经分支上的所有结点。
- 子孙:以某结点为根的子树中的任一结点都称为该结点的子孙。
- 堂兄弟:双亲在同一层的结点互称堂兄弟结点。上图中,结点 K K K与结点 M M M互为堂兄弟结点。
- 有序树与无序树:树中结点的各子树从左到右是有次序的,不能互换,则称为有序树,否则称为无序树。
- 路径和路径长度:两个结点的路径由着两个结点之间所经过的结点序列构成(树中分支是有向的,即从双亲指向孩子,所以树中的路径从上向下,同一双亲的两个孩子之间不存在路径),而路径长度时路径上所经过的边的个数(而非结点个数)。
- 森林:森林是 m ( m ≥ 0 ) m(m≥0) m(m≥0)棵互不相交的树的集合。
5.1.3树的性质
- 树中结点数等于所有结点的度数之和加 1 1 1。
- 度为 m m m的树中第 i i i层上至多有 m i − 1 m^{i-1} mi−1个结点 ( i ≥ 1 ) (i≥1) (i≥1)。
- 高度为 h h h的 m m m叉树至多有 ( m h − 1 ) / ( m − 1 ) (m^h-1)/(m-1) (mh−1)/(m−1)个结点。
- 具有 n n n个结点的 m m m叉树的最小高度为 ⌈ l o g m n m − n + 1 ⌉ \lceil log_m^{nm-n+1} \rceil ⌈logmnm−n+1⌉。
证明:
当该
m
m
m叉树是满树时,设高度为
h
1
h_1
h1:
n
=
(
m
h
−
1
)
/
(
m
−
1
)
n=(m^h-1)/(m-1)
n=(mh−1)/(m−1)
解得:
h
1
=
l
o
g
m
n
m
−
n
+
1
h_1=log_m^{nm-n+1}
h1=logmnm−n+1
当该
m
m
m叉树并非满树时,设高度为
h
2
h_2
h2:
n
=
(
m
2
h
−
1
)
/
(
m
−
1
)
n=(m^h_2-1)/(m-1)
n=(m2h−1)/(m−1)
解得:
h
2
=
l
o
g
m
n
m
−
n
+
1
h_2=log_m^{nm-n+1}
h2=logmnm−n+1,此时
h
h
h是一个介于
h
1
−
1
h_1-1
h1−1与
h
1
h_1
h1之间的数,故而需要向上取整。
综上可得,高度为
h
h
h的
m
m
m叉树至多有
(
m
h
−
1
)
/
(
m
−
1
)
(m^h-1)/(m-1)
(mh−1)/(m−1)个结点。
- 度为 m m m的树与 m m m叉树的区别:
5.1.4树的存储结构
①双亲表示法:顺序存储
使用一组连续的存储单元存储树的结点,每个结点存储数据域
d
a
t
a
data
data和指向双亲结点位置的
p
a
r
e
n
t
parent
parent。
#define MAX_TREE_SIZE 100
typedef struct{
ElemType data;
int parent;
}PTNode;
typedef struct{
PTNode nodes[MAX_TREE_SIZE];
int n; //结点数
}PTree;
②孩子表示法:顺序+链式
孩子表示法是将每个结点的孩子结点都用单链表链接起来形成一个线性结构,此时
n
n
n个结点就有
n
n
n个孩子链表(叶子结点的孩子链表为空表)。
这种存储方式寻找子女的操作非常直接,而寻找双亲的操作需要遍历 n n n个结点中孩子链表指针域所指向的 n n n个孩子链表。
③孩子兄弟表示法:链式存储
孩子兄弟表示法又称二叉树表示法,即以二叉链表作为树的存储结构。孩子兄弟法使每个结点包含结点值、指向结点第一个孩子结点的指针、指向结点下一个兄弟结点的指针(沿此域可找到结点的所有兄弟结点),定义:
typedef struct CSNode{
ElemType data;
CSNode* firstchild,*nextsibling;
}CSNode,*CSTree;
这种存储表示法最大的优点是可方便地实现树转换为二叉树的操作,易于查找结点的孩子等,但缺点是从当前结点查找其双亲结点比较麻烦(可为每个结点增设一个
p
a
r
e
n
t
parent
parent域指向其父结点)。
5.2二叉树的基本概念
5.2.1二叉树的定义
二叉树是
n
(
n
≥
0
)
n(n≥0)
n(n≥0)个结点所构成的集合,或为空树
(
n
=
0
)
(n=0)
(n=0),或为非空树,对于非空树,每个结点至多只有两棵子树(不存在度大于
2
2
2的结点),且是有序树,子树有左右之分,分子数与右子树本身又是二叉树。
二叉树的五种基本形态:
二叉树与度为2的有序树的区别:
- 度为2的树至少有3个结点,而二叉树可以为空。
- 度为2的有序树的孩子的左右次序是相对于另一孩子而言的,若某个结点只有一个孩子,则这个孩子无需区分左右次序,而二叉树无论孩子数是否为2,均需确定其左右次序,即二叉树的结点次序不是相对于另一结点而言,而是确定的。
5.2.2几种特殊的二叉树
- 满二叉树:一棵高度为
h
h
h,且含有
2
h
−
1
2^h-1
2h−1个结点的二叉树称为满二叉树,即树每层都有最多的结点,且叶子结点都集中在最下一层,除叶子结点外的每个结点度数均为2。
- 可对满二叉树按层序编号,约定编号从根结点(根结点编号为1)起,自上而下,自左向右,此时对于编号为 i i i的结点,若有双亲,则双亲为 ⌊ i / 2 ⌋ \lfloor i/2 \rfloor ⌊i/2⌋,若有左孩子,则左孩子为 2 i 2i 2i,若有右孩子,则右孩子为 2 i + 1 2i+1 2i+1。
- 完全二叉树:高度为
h
h
h、有
n
n
n个结点的二叉树,当且仅当每个结点都与高度为
h
h
h的满二叉树中编号为
1
1
1~
n
n
n的结点意义对应时,称为完全二叉树,有如下特点:
- ①若 i ≤ ⌊ n / 2 ⌋ i≤\lfloor n/2 \rfloor i≤⌊n/2⌋,则结点 i i i为分支结点,否则为叶子结点。
- ②叶子结点只可能出现在层次最大的两层上,对于最大层次中的叶子结点,都依次排序在改层最左边的位置上。
- ③若有度为 1 1 1的结点,则只可能有 1 1 1个,且该结点只有左孩子而无右孩子。
- ④按层序编号后,一旦出现某结点(编号为 i i i)为叶子结点或只有左孩子,则编号大于 i i i的结点均为叶子结点。
- ⑤若 n n n为奇数,则每个分支结点都有左孩子和右孩子,若 n n n为偶数,则编号最大的叶子结点 ⌊ n / 2 ⌋ \lfloor n/2 \rfloor ⌊n/2⌋只有左孩子,没有右孩子,其余分支结点左右孩子都有。
- 二叉排序树:左子树上所有结点关键字均小于根结点关键字,右子树上所有结点关键字均大于根结点关键字,左右子树又各是一棵二叉排序树。
- 平衡二叉树:树上任一结点的左右子树深度之差不超过1。
5.2.3二叉树的性质
关于第四点的两种情况:
对于
(
a
)
(a)
(a)中的情况,无需说明,对于
(
b
)
(b)
(b)中的情况,设序号为
i
i
i的结点位于第
h
h
h层,则有:
2
h
−
1
=
i
2^h-1=i
2h−1=i,则,对于
i
i
i的右孩子,其序号即为:
2
∗
(
2
h
−
1
)
+
1
=
2
i
+
1
2*(2^h-1)+1=2i+1
2∗(2h−1)+1=2i+1(因为序号代表之前所有的结点个数)。
关于完全二叉树的推广:
由于完全二叉树度为
1
1
1的结点个数只能为
0
0
0或
1
1
1,故有如下两种情况:
(1)当度为
1
1
1的结点个数只能为
0
0
0时,有:
n
=
n
0
+
n
2
=
2
n
2
+
1
n=n_0+n_2=2n_2+1
n=n0+n2=2n2+1。
(2)当度为
1
1
1的结点个数只能为
1
1
1时,有:
n
=
n
0
+
1
+
n
2
=
2
n
2
+
2
n=n_0+1+n_2=2n_2+2
n=n0+1+n2=2n2+2。
5.2.3二叉树的存储结构
①顺序存储
使用一组地址连续的存储单元自上而下、自左至右存储完全二叉树上的结点,即将完全二叉树上编号为
i
i
i的结点存储在一维数组下标为
(
i
−
1
)
(i-1)
(i−1)的分量中。
顺序存储适用于完全二叉树和满二叉树,因为树中结点的序号能唯一反映结点之间的逻辑关系,但对于一般的二叉树,为了能让数组下标反映二叉树中结点之间的逻辑关系,只能添加一些并不存在的空结点,使结点与完全二叉树上的结点对应,再存储到一维数组当中。例:
最坏情况下,一棵深度为
h
h
h,只有
h
h
h个结点的二叉树却需要长度为
2
h
−
1
2^h-1
2h−1的一维数组
#define MAXSIZE 10
typedef struct TreeNode{
//二叉树的顺序存储中,一定要把二叉树的结点编号与完全二叉树对应起来.
int value;
bool isEmpty; //结点是否为空
}TreeNode;
//顺序存储的初始化
void InitSqBiTree(){
TreeNode t[MAXSIZE];
for(int i=0;i<MAXSIZE;i++)
t[i].isEmpty=true;
}
可让数组下标从 1 1 1开始存储树中结点,使之与完全二叉树中的序号相对应,方便计算结点之间的关系。
②链式存储
顺序存储的空间利用率较低,一般二叉树的存储都采用链式存储,通常结点结构至少包含:数据域
d
a
t
a
data
data、左指针域
l
c
h
i
l
d
lchild
lchild、右指针域
r
c
h
i
l
d
rchild
rchild:
typedef struct{
int data;
BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
//初始化
void InitBiTree(BiTree T){
T=NULL;
}
易验证,含有
n
n
n个结点的二叉链表中含有
(
n
+
1
)
(n+1)
(n+1)个空链域。
事实上,还可加上一些指针域,如增加指向父结点的指针后形成三叉链表结构:
5.2.4二叉树的遍历
二叉树的遍历是指按某条搜索路径访问树中每个结点,将二叉树这一非线性结构转为线性结构,常见的遍历次序有先序 ( N L R ) (NLR) (NLR)、中序 ( L N R ) (LNR) (LNR)、后序 ( L R N ) (LRN) (LRN)三种遍历算法,其中,序指的是访问根结点的次序。
①先序遍历
typedef 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); //递归遍历右子树
}
}
②中序遍历
typedef 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); //递归遍历右子树
}
}
非递归算法:
void InOrder(BiTree T){
InitStack(S);
p=T;
q=new BiTree;
while(p||!StackEmpty(S)){
if(p){
Push(S,p);
p=p->lchild;
}else{
Pop(S,q);
cout<<q->data;
p=q->rchild;
}
}
}
③后序遍历
typedef 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); //访问根结点
}
}
④层序遍历
算法思想:
- 初始化一个辅助队列。
- 根节点入队。
- 若队列非空,则队头结点出队,访问该结点,依次将其左、右孩子插入队尾(如果有的话)。
- 重复以上操作直至队列为空。
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); //右孩子入队
}
}
⑤由遍历序列构造二叉树
有三种确定方式:
- 中序+先序
- 中序+后序
- 中序+层序
以
中序
+
先序
中序+先序
中序+先序为例,先序遍历是先访问根结点,故第一个结点必然是二叉树根结点,而在中序遍历中,根结点必将中序遍历序列分割为两个子序列,前一个子序列是根节点左子树的中序遍历序列,后一个子序列是根节点右子树的中序遍历序列,根据这两个子序列,可在先序序列中找到对应的左子序列和右子序列,而左子序列第一个结点是左子树根节点,右子序列第一个结点是右子树根节点,以此类推即可确定这棵二叉树。
例,由中序序列
B
D
C
E
A
F
H
G
BDCEAFHG
BDCEAFHG和中序序列
D
E
C
B
H
G
F
A
DECBHGFA
DECBHGFA确定的二叉树:
但是只知道二叉树的先序和后序并不能唯一确定一棵二叉树,如先序序列
A
B
AB
AB,后序序列
B
A
BA
BA,则无法确定
B
B
B是左子树还是右子树,可得以下两种情况:
参考代码是当时写 L e e t C o d e LeetCode LeetCode第 105 105 105题:前序与中序遍历序列构造二叉树,提交的代码,代码基本思想是先利用先序遍历来确定根节点,再中序遍历来确定左右子树。
/**
1. Definition for a binary tree node.
2. struct TreeNode {
3. int val;
4. TreeNode *left;
5. TreeNode *right;
6. TreeNode() : val(0), left(nullptr), right(nullptr) {}
7. TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
8. TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
9. };
*/
class Solution {
//LeetCode:从前序与中序遍历序列构造二叉树
/*
先序遍历的特点:第一个值即为根节点,随后是左子树和右子树结点值.左子树和右子树中的第一个值仍为根节点
中序遍历的特点:根结点的左右两侧依次为左子树与右子树.
故,可利用先序遍历来确定根节点,中序遍历来确定左右子树
*/
public:
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
return build(preorder,0,preorder.size()-1,inorder,0,inorder.size()-1);
}
private:
/*
build函数的定义:
先序数组为preorder[preStart:preEnd],中序数组为inorder[inStart:inEnd],来构造二叉树并返回根节点
*/
TreeNode* build(vector<int>&preorder,int preStart,int preEnd,vector<int>&inorder,int InStart,int InEnd){
TreeNode* root=NULL;
if(preStart>preEnd)return root;
if(InStart>InEnd)return root;
int rootval=preorder[preStart];
int index=InStart;
for(;index<=InEnd;index++){
if(rootval==inorder[index])break;
}
root=new TreeNode;
root->val=rootval;
int leftsize=index-InStart;
root->left=build(preorder,preStart+1,preStart+leftsize,inorder,InStart,index-1);
root->right=build(preorder,preStart+leftsize+1,preEnd,inorder,index+1,InEnd);
return root;
}
};
5.3线索二叉树
5.3.1线索二叉树的定义
传统的二叉链表存储仅能体现一种父子关系,不能直接得到结点在遍历中的前驱或后继,事实上,在含
n
n
n个结点的二叉树中,有
(
n
+
1
)
(n+1)
(n+1)个空指针,这是因为每个叶结点有
2
2
2个空指针,每个度为
1
1
1的结点有
1
1
1个空指针,空指针总数为
2
n
0
+
n
1
2n_0+n_1
2n0+n1,又
n
0
=
n
2
+
1
n_0=n_2+1
n0=n2+1,所以空指针总数为
n
0
+
n
1
+
n
2
+
1
=
n
+
1
n_0+n_1+n_2+1=n+1
n0+n1+n2+1=n+1,故可利用这些空指针来存放指向其前驱或后继的指针,从而可像遍历单链表一样遍历二叉树,加快了查找结点前驱和后继的速度。
规定:若无左子树,则令
l
c
h
i
l
lchil
lchil指向其前驱结点,若无右子树,则令
r
c
h
i
l
d
rchild
rchild指向其后继结点,并增加两个标志域标识指针域是指向左(右)孩子还是指向前驱(后继)。
typedef struct ThreadNode{
ElemType data;
ThreadNode* lchild,*rchild;
int ltag,rtag;
}ThreadNode,*ThreadTree;
以这种结点结构构成的二叉链表作为二叉树的存储结构,称为线索链表,其中指向结点前驱和后继的指针称为线索,加上线索的二叉树称为线索二叉树。
5.3.2二叉树的线索化
a.二叉树的中序线索化
typedef struct ThreadNode{
int data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag;
}ThreadNode, *ThreadTree;
//全局变量pre,指向当前访问的结点的前驱,初始化无前驱即为NULL
TreadNode *pre=NULL;
//中序遍历二叉树
void InThread(ThreadTree p,ThreadTree& pre){
if(p!=NULL){
InThread(p->lchild,pre);
if(p->lchild==NULL){
p->lchild=pre;
p->ltag=1;
}
if(pre!=NULL&&pre->rchild==NULL){
pre->rchild=p;
pre->rtag=1;
}
}
pre=p;
InThread(p->rchild,pre);
}
//给定根结点T中序线索化二叉树
void CreateInThread(ThreadTree T){
pre = NULL; //pre初始为NULL
if(T!=NULL){ //非空二叉树才能进行线索化
InThread(T,pre); //中序线索化二叉树
pre->rchild==NULL;
pre->rtag=1; //处理遍历的最后一个结点
}
}
b.二叉树的后序线索化
typedef struct ThreadNode{
int data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag;
}ThreadNode, *ThreadTree;
//全局变量pre,指向当前访问的结点的前驱
TreadNode *pre=NULL;
//后序遍历二叉树
void PostThread(ThreadTree p,ThreadTree &pre){
if(p!=NULL){
PostThread(p->lchild,pre);
PostThread(p->rchild,pre);
if(p->lchild=NULL){
p->lchild=pre;
p->ltag=1;
}
if(pre!=NULL&&pre->rchild==NULL{
pre->rchild=p;
pre->rtag=1;
}
pre=q;
}
}
//后序线索化二叉树T
void CreatePostThread(ThreadTree T){
pre = NULL; //pre初始为NULL
if(T!=NULL);{ //非空二叉树才能进行线索化
PostThread(T); //后序线索化二叉树
if(pre->rchild == NULL)
//必须进行判断,因为对于后序线索化而言,pre最后指向的是根结点.
pre->rtag=1;
}
}
c.二叉树的先序线索化
对于中序遍历和后序遍历:当遍历到根结点时左孩子已经遍历过了,而右指针要么指向右孩子,要么指向该结点的后继结点,和正常遍历的顺序一样,故不存在转圈圈问题,故可以正常遍历。
对于先序遍历:先序遍历一个结点的左子树时,需要判断其
l
t
a
g
ltag
ltag的值是否为
0
0
0,如果为
0
0
0则可以正常遍历(此时代表其左孩子是真是存在的),如果
l
t
a
g
=
1
ltag=1
ltag=1,则说明该结点的左指针指向的是它的前驱结点(左孩子实际是不存在的),继续遍历的话就会陷入“转圈圈”。
举例:
遍历
A
A
A时执行
v
i
s
i
t
visit
visit,因为其左子树为
N
U
L
L
NULL
NULL,故赋
l
t
a
g
ltag
ltag为
1
1
1,
l
c
h
i
l
d
lchild
lchild指向
p
r
e
pre
pre(假设此时
p
r
e
pre
pre不为空),若没有:
i
f
(
T
−
>
l
t
a
g
=
=
0
)
if(T->ltag==0)
if(T−>ltag==0)进行判断,则继续执行
P
r
e
T
h
r
e
a
d
(
T
−
>
l
c
h
i
l
d
)
PreThread(T->lchild)
PreThread(T−>lchild),等价于:
P
r
e
T
h
r
e
a
d
(
p
r
e
)
PreThread(pre)
PreThread(pre),而此时的
p
r
e
=
=
A
pre==A
pre==A,故会陷入死循环。由于这里的问题来源于先遍历了
T
T
T,故而改变了
l
c
h
i
l
d
lchild
lchild,使得发生了误判,而中序和后序都是先遍历
l
c
h
i
l
d
lchild
lchild再遍历
T
T
T,故不会出现遍历完
T
T
T再回去遍历
l
c
h
i
l
d
lchild
lchild的情况。
对于所有结点的
r
c
h
i
l
d
rchild
rchild,其要么指向右孩子结点,此时无影响,要么指向后继结点,当指向后继结点时,本身遍历完当前结点后就是要继续遍历后继结点,故亦无影响。
typedef struct ThreadNode{
int data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag; // 左、右线索标志
}ThreadNode, *ThreadTree;
//全局变量pre, 指向当前访问的结点的前驱
TreadNode *pre=NULL;
//先序线索化
void PreThread(ThreadTree p,ThreadTree &pre){
if(p!=NULL){
if(p->lchild==NULL){
p->lchild=pre;
p->ltag=1;
}
if(pre!=NULL&&pre->rchild==NULL){
pre->rchild=p;
pre->rtag=1;
}
pre=p;
if(p->ltag==0)
PreThread(p->child,pre);
PreThread(p->rchild,pre);
}
}
}
//先序线索化二叉树T
void CreatePreThread(ThreadTree T){
pre = NULL; //pre初始为NULL
if(T!=NULL);{ //非空二叉树才能进行线索化
PreThread(T);
if(pre->rchild == NULL)
pre->rtag=1; //处理遍历的最后一个结点
}
}
5.3.3线索二叉树找前驱、后继与遍历
a.中序线索二叉树
后继
在中序线索二叉树中找到指定结点
∗
p
*p
∗p的中序后继
n
e
x
t
next
next:
- 若 p − > r t a g = 1 p->rtag=1 p−>rtag=1,则 n e x t = p − > r c h i l d next=p->rchild next=p−>rchild。
- 若 p − > r t a g = = 0 p->rtag==0 p−>rtag==0,即 p p p有右孩子,则 ∗ p *p ∗p的中序后继 n e x t next next即为 ∗ p *p ∗p右子树中最左下结点。
//找到以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;
}
//对中序线索二叉树进行中序遍历,时间复杂度O(n),空间复杂度O(1)
void Inorder(ThreadNode* T){
for(ThreadNode* p=FirstNode(T);p!=NULL;p=NextNode(p)){
visit(p);
}
}
前驱
在中序线索二叉树中找到指定结点 ∗ p *p ∗p的中序前驱 p r e pre pre:
- 若 p − > l t a g = 1 p->ltag=1 p−>ltag=1,则 p r e = p − > l t a g pre=p->ltag pre=p−>ltag。
- 若 p − > l t a g = = 0 p->ltag==0 p−>ltag==0,即 p p p有左孩子,则 ∗ p *p ∗p的中序前驱 p r e pre pre即为 ∗ p *p ∗p左子树中最右下结点。
//找到以P为根的子树中,最后一个被中序遍历的结点
ThreadNode* LastNode(ThreadNode* p){
//循环找到最左下结点(不一定是叶结点)
while(p->rtag==0)p=p->rchild;
return p;
}
//在中序线索二叉树中找到结点p的后继结点
ThreadNode* PreNode(ThreadNode* p){
//右子树中最左下结点
if(p->ltag==0)return FirstNode(p->lchild);
else return p->lchild;
}
//对中序线索二叉树进行逆向中序遍历
void RevInorder(ThreadNode* T){
for(ThreadNode* p=LastNode(T);p!=NULL;p=PreNode(p)){
visit(p);
}
}
b.先序线索二叉树
后继
在先序线索二叉树中找到指定结点
∗
p
*p
∗p的先序后继
n
e
x
t
next
next:
- 若 p − > r t a g = 1 p->rtag=1 p−>rtag=1,则 n e x t = p − > r c h i l d next=p->rchild next=p−>rchild。
- 若 p − > r t a g = = 0 p->rtag==0 p−>rtag==0,即 p p p有右孩子,则 ∗ p *p ∗p的先序后继 n e x t next next即为 p p p的左子树根(若存在)或右子树根。
前驱
在先序线索二叉树中找到指定结点
∗
p
*p
∗p的先序前驱
p
r
e
pre
pre:
- 若 p − > l t a g = 1 p->ltag=1 p−>ltag=1,则 p r e = p − > l c h i l d pre=p->lchild pre=p−>lchild。
- 若 p − > l a g = = 0 p->lag==0 p−>lag==0,即 p p p有左孩子,此时 ∗ p *p ∗p的前驱有两种情况:若 ∗ p *p ∗p是其双亲的左孩子,则其前驱为其双亲结点,否则应是其双亲的左子树上先序遍历最后访问到的结点(故无法直接得到)。
c.后序线索二叉树
后继:
在后序线索二叉树中找到指定结点
∗
p
*p
∗p的后序后继
n
e
x
t
next
next:
- 若 ∗ p *p ∗p是二叉树的根,则后继为空。
- 若 ∗ p *p ∗p是其双亲的右孩子,则其后继为双亲结点。
- 若 ∗ p *p ∗p是其双亲的左孩子,且 ∗ p *p ∗p没有右兄弟,则其后继为双亲结点。
- 若 ∗ p *p ∗p是其双亲的左孩子,且 ∗ p *p ∗p有右兄弟,则其后继为双亲的右子树按后序遍历列出的第一个结点(右子树最左下的叶结点)。
故,无法直接找到。
前驱:
在后序线索二叉树中找到指定结点
∗
p
*p
∗p的后序前驱
p
r
e
pre
pre:
- 若 p − > l t a g = 1 p->ltag=1 p−>ltag=1,则 p r e = p − > l c h i l d pre=p->lchild pre=p−>lchild。
- 若 p − > l a g = = 0 p->lag==0 p−>lag==0,当 p − > r t a g p->rtag p−>rtag也为 0 0 0时,则 p p p的右链指示其前驱;否则, p p p的左链指示其前驱。
d.总结
5.3.4线索二叉树的遍历
5.4树、二叉树与森林
5.4.1树、二叉树与森林的转换
由于二叉树和树都可用二叉链表作为存储结构,因此二叉链表可作为媒介导出树与二叉树的对应关系,即给定一棵二叉树,可找到唯一的一棵二叉树与之对应。
a.树转二叉树
基本步骤:
- 在兄弟结点之间加一条线。
- 对每个结点,只保留它与第一个孩子的连线,而与其它孩子的连线全部抹掉。
- 旋转调整位置。
即,每个结点左指针指向它的第一个孩子,右指针指向它在树中的相邻右兄弟,这个规则也称为
左孩子,右兄弟
左孩子,右兄弟
左孩子,右兄弟,此时所得二叉树根结点没有右子树。
b.森林转二叉树
基本步骤:
- 将森林中每一棵树转为二叉树。
- 每棵树的根结点视为兄弟关系,在每棵树的根之间加一条连线。
- 以第一棵树的根为轴顺时针旋转45°。
c.二叉树转树
基本步骤:
- 加线:若某结点 X X X的左孩子结点存在,则将这个左孩子的右孩子结点、右孩子的右孩子结点、右孩子的右孩子的右孩子结点 … … …等等以此类推,都作为结点 X X X的孩子,将结点 X X X与这些右孩子结点用线连接起来。
- 抹线:删除原二叉树中所有结点与其右孩子结点的连线。
d.二叉树转森林
基本步骤:
- 先把每个结点与右孩子结点的连线删除,得到分离的二叉树。
- 将分离后的所有二叉树转为树。
注:二叉树转树或森林是唯一的。
5.4.2树和森林的遍历
①树的遍历
先根遍历:若树非空,则先访问根结点,再依次遍历根结点的每棵子树,遍历子树时仍遵守先根后子树的规则,其遍历序列与这棵树相应二叉树的先序序列相同。图中树的先根遍历为:
A
B
E
F
C
D
G
ABEFCDG
ABEFCDG。
后根遍历:若树非空,则先依次遍历根结点的每棵子树,再访问根结点,遍历子树时仍遵循先子树后根的规则,其遍历序列与这棵树相应二叉树的中序序列相同。图中树的后根遍历为:
E
F
B
C
G
D
A
EFBCGDA
EFBCGDA。
层序遍历:与二叉树的层序遍历思想基本相同,即按层序依次访问各结点。图中树的层序遍历为:
A
B
C
D
E
F
G
ABCDEFG
ABCDEFG。
②森林的遍历
先序遍历
- 访问森林第一棵树的根结点。
- 先序遍历第一棵树中根结点的子树森林。
- 先序遍历除去第一棵树之后剩余的树构成的森林。
上图中森林的先序遍历序列为: A B C D E F G H I ABCDEFGHI ABCDEFGHI。
中序遍历
- 中序遍历森林中第一棵树的根结点的子树森林。
- 访问第一棵树的根结点。
- 中序遍历除去第一棵树之后剩余的树构成的森林。
上图中森林的中序遍历序列为: B C D A F E H I G BCDAFEHIG BCDAFEHIG。
将其转化为二叉树:
可见,森林的先序和中序遍历即为其对应二叉树的先序和中序遍历。
事实上,森林和树的遍历与二叉树遍历的对应关系为:
5.5树和二叉树的应用
5.5.1哈夫曼树与哈夫曼编码
a.哈夫曼树的定义
哈夫曼树是一类带权路径最短的树。
路径:从树的一个结点到另一个结点之间的分支(边)构成这两个结点之间的路径。
路径长度:路径上分支数目(边的数目)称为路径长度。
结点的权:树中结点被赋予的一个表示某种意义的数值,称为结点的权。
树的路径长度:从树根到每一结点的路径长度之和。
结点的带权路径长度:从树根到结点的路径长度(所经过的边数)与该结点上权值的乘积。
树的带权路径长度:树中所有叶结点的带权路径之和称为树的带权路径长度,记为
W
P
L
=
∑
i
=
1
n
w
i
∗
l
i
WPL=\sum_{i=1}^{n}w_i*l_i
WPL=∑i=1nwi∗li。
哈夫曼树:在含有
n
n
n个带权叶结点的二叉树中,带权路径长度
W
P
L
WPL
WPL最小的二叉树称为哈夫曼树。例:
- (a): W P L WPL WPL=72+52+22+42=36
- (b): W P L WPL WPL=42+73+53+21=46
- ©: W P L WPL WPL=71+52+23+43=35
在以上三棵树中,**©**是该 4 4 4个带权叶结点的哈夫曼树,事实上可以发现,哈夫曼树有着,权值越大的叶结点离根结点越近的特点。
b.哈夫曼树的构造
c.哈夫曼编码
两类编码方式
- 固定长度编码:在数据通信中,对每个字符采用相同长度的二进制位表示,这种编码方式称为固定长度编码。
- 可变长度编码:在数据通信中,对频率高的字符赋予短编码,对于频率低的字符赋予较长编码,这种编码方式称为可变长度编码。
前缀编码与哈夫曼编码
- 前缀编码:若一个编码方案中,任一个编码都不是其他任何编码的前缀,则称该编码是前缀编码,如编码方案: 0 , 10 , 110 , 111 0,10,110,111 0,10,110,111就是一个前缀编码。因为没有一个编码是其他编码的前缀,故前缀编码的解码非常简单,对于识别出的第一个编码,直接将其翻译为原码即可。
- 哈夫曼编码:对一棵具有 n n n个带权叶结点的哈夫曼树,对树中每个左分支赋予 0 0 0,右分支赋予 1 1 1,则从根到每个叶子的路径上,各分支的赋值分别构成一个二进制串,该二进制串就称为哈夫曼编码。
上图矩阵中方块表示字符及其出现次数此时,
W
P
L
WPL
WPL为:
此处的
W
P
L
WPL
WPL可视为最终编码得到二进制编码的长度,共
224
224
224位,若采用
3
3
3位固定长度编码,则得到的二进制编码长度为
300
300
300位,因此哈夫曼编码共压缩了
25
25%
25的数据,利用哈夫曼树可设计出总长度最短的二进制前缀编码。
哈夫曼编码的性质:
- 0 0 0和 1 1 1表示左右子树并不固定,所以构造出的哈夫曼树并不唯一,但各哈夫曼树的带权路径长度 W P L WPL WPL相同且为最优,且,若有若干权值相同的叶结点,构造出的哈夫曼树可能不同,但 W P L WPL WPL必然相同且最优。
- 哈夫曼编码是从根到叶子路径上的编码序列,由树的特点可知,若路径 A A A是路径 B B B的最左部分,则 B B B经过了 A A A,则 A A A的终点一定不是叶子,而哈夫曼编码对应的终点一定为叶子结点,故,任一哈夫曼码都不会与其他哈夫曼编码的前缀部分完全重叠,故而哈夫曼编码是前缀编码。
- 哈夫曼编码是最优前缀编码。对于包括 n n n个字符的数据文件,分别以他们出现次数为权值构造哈夫曼树,可利用该树对应的哈夫曼编码为文件进行编码,假设每种字符出现的次数为 w i w_i wi,其编码长度为 l i l_i li,文件中只有 n n n种字符,则文件总长度为 ∑ i = 1 n w i l i \sum_{i=1}^{n}w_il_i ∑i=1nwili,对应到二叉树上,即为二叉树上带权路径长度。
5.5.2并查集
a.并查集的定义
并查集用来表示非线性逻辑结构——集合,对于集合,其只强调元素是否属于同一集合。
一般使用树(森林)的双亲表示法作为并查集的存储结构,以一棵树表示一个子集合,所有的树构成表示全集合的森林,一般用数组元素下标代表元素名,用根结点下标代码子集合名,根结点的双亲结点为负数。
双亲表示法:
并查集的存储结构:
b.并查集的基本操作
并查集是逻辑结构——集合的一种具体体现,只进行 并 并 并和 查 查 查两种基本操作。
- Initial:初始化并查集。
- Find:查操作,确定一个元素所属集合。
- Union:并操作,将两个不相交的集合合并为一个。
#include<iostream>
using namespace std;
#define SIZE 13
int UFSets[SIZE];
//初始化并查集
void Initial(int S[]){
for(int i=0;i<SIZE;i++){
S[i]=-1;
}
}
//Find:查操作,寻找x所属集合,返回x所属根结点下标
int Find(int S[],int x){
while(S[x]>=0){
x=S[x];
}
return x;
}
//Union:并操作,将两个集合合并为一个(需要先找到两个集合的根结点)
void Union(int S[],int Root1,int Root2){
if(Root1==Root2)return ;
S[Root2]=Root1;
}
对于并操作
U
n
i
o
n
Union
Union操作,其时间复杂度为
O
(
1
)
O(1)
O(1),但对于查操作,其最坏时间复杂度为
O
(
n
)
O(n)
O(n),如:
c.Union操作的优化
思路:使用根结点的绝对值表示树的结点总数,在每次 U n i o n Union Union操作构建树时,让小树合并到大树,使树尽可能不太 高 高 高。
void Union(int S[],int Root1,int Root2){
if(Root1=Root2)return ;
if(S[Root2]>S[Root1]){
S[Root1]+=S[Root2];
S[Root2]=Root1;
}else{
S[Root2]+=S[Root1];
S[Root1]=Root2;
}
}
在优化之后,树的高度不会超
⌊
l
o
g
2
n
⌋
+
1
\lfloor log_2n \rfloor+1
⌊log2n⌋+1,使得
F
i
n
d
Find
Find操作最坏时间复杂度为
O
(
l
o
g
2
n
)
O(log_2n)
O(log2n)。
5.6补充:利用二叉树求解表达式的值
一般情况下,一个表达式由一个运算符和两个操作数构成,两个操作数有次序之分,并且操作数本身也可以是表达式,这个结构类似二叉树,故可利用二叉树来表示表达式。
以二叉树表示表达式的递归定义:
- 若表达式为数或简单常量,则相应二叉树中仅有一个根结点,其数据域存放该表达式信息。
- 若表达式为: 第一操作数 第一操作数 第一操作数 运算符 运算符 运算符 第二操作数 第二操作数 第二操作数 的形式,则相应的二叉树中以左子树表示第一操作数,右子树表示第二操作数,根结点的数据域存放运算符(若为一元运算符,则左子树为空),其中操作数本身又是表达式。
例,用二叉树表示表达式
a
+
b
∗
(
c
−
d
)
−
e
/
f
a+b*(c-d)-e/f
a+b∗(c−d)−e/f,在二叉树表达式中并无括号,但其结果却有效表达了其运算符之间的运算次序:
- 先序遍历: − + a ∗ b − c d / e f -+a*b-cd/ef −+a∗b−cd/ef,即为前缀表达式(波兰式)。
- 中序遍历: a + b ∗ c − d − e / f a+b*c-d-e/f a+b∗c−d−e/f,即为中缀表达式。
- 后序遍历: a b c d − ∗ + e f / − abcd-*+ef/- abcd−∗+ef/−,即为后缀表达式(逆波兰式)。