五、树、二叉树
5.1 树
5.1.1 树的基本概念
树是n个结点的有限集。n = 0时,称为空树。
任意一棵非空树应该满足:
- 有且仅有一个特定的称为根的结点。
- n > 1时,其余结点可以非为m(m > 0)个互不相交的有限集 T 1 , T 2 , . . . , T m T_1,T_2,...,T_m T1,T2,...,Tm,其中每个集合本身又是一棵树,并且称为根的子树。
树的定义是递归的。
树作为一种逻辑结构,也是一种分层结构。
具有以下特点:
- 树的根结点没有前驱,除根结点外的所有结点有且只有一个前驱。
- 树中所有结点可以有0个或多个后继。
树适合表示具有层次结构的数据。树中的某个结点(除根结点外)最多只和上层的一个结点(即其父结点)有直接关系,根结点没有直接上层结点,因此在n个结点的树中有n - 1条边。而树中每个结点与其下一层的0个或多个结点有直接关系(子女结点)。
5.1.2 树的基本术语
祖先 - 到根的唯一路径上的任意结点(子孙)。
双亲 - 路径上最近的结点(孩子)。
树中任意一个结点的孩子个数称为该结点的度。树中结点的最大度数称为树的度。
度大于0的结点称为分支结点(非终端结点);度为0的结点成为叶子结点(终端结点)。每个结点的分支数就是该结点的度。
结点的层次从树根开始定义,根结点为第一层,它的子结点为第2层,以此类推。
双亲结点在同一层的结点互为堂兄弟。
结点的深度是从根结点开始自顶向下逐层累加的。
结点的高度是从叶结点开始自底向上逐层累加的。
树的高度(深度)是树中结点的最大层数。
树中结点的各子树从左到右是有次序的,不能互换,则该树为有序树,否则称为无序树。
树中两个结点之间的路径是由这两个结点之间所经过的结点序列构成的,路径长度是路径上所经过的边的个数。(同一个双亲的两个孩子间不存在路径,因为树中的分支是有向的,从双亲指向孩子,路径都是从上向下的)
树的路径长度是指树根到每个结点的路径长度总和,根到每个结点的路径长度的最大值应该是树的高度 - 1。
森林是 m ( m ≥ 0 ) m(m\geq 0) m(m≥0)棵互不相交的树的集合。树的根结点删去就成了森林,m棵独立的树加上一个结点,并把这m棵树作为该结点的子树,则森林就变成了树。
5.1.3 树的性质
- 树的结点数 = 所有的结点的度数 + 1。
- 度为m的树中第 i i i层上至多有 m i − 1 m^{i-1} mi−1个结点 ( i ≥ 1 ) (i\geq 1) (i≥1)。
- 高度为h的m叉树至多有 ( m h − 1 ) / ( m − 1 ) (m^h - 1)/(m-1) (mh−1)/(m−1)个结点。
- 具有n个结点的m叉树的最小高度为 ⌈ l o g m ( n ( m − 1 ) + 1 ) ⌉ \lceil log_m(n(m-1)+1) \rceil ⌈logm(n(m−1)+1)⌉。
5.2 二叉树
5.2.1 二叉树定义和特性
二叉树也是一种树形结构,其特点是每个结点至多有两棵子树(即二叉树中不存在度大于2的结点)。
且二叉树的子树有左右只分,次序不能任意颠倒。即使数中结点只有一棵子树,也要区分左右子树。
二叉树是 n ( n ≥ 0 ) n(n\geq 0) n(n≥0)个结点的有限集合,递归形式定义:
- 或者为空二叉树,n = 0。
- 或者由一个根结点和两个互不相交的被称为根的左子树和右子树组成。左子树和右子树有分别是一棵二叉树。
度为2的有序树和二叉树之间的区别:
- 度为2的有序树至少3个结点,二叉树可为空。
- 度为2的有序树的孩子的左右次序是相对于另一个孩子而言的,若某个结点只有一个孩子,则这个孩子就无需区分左右顺序,而二叉树无论怎样都应该区分左右,是确定的,不是相对于另一个而言的。
5.2.2 特殊二叉树
满二叉树:每层都含有最多的结点。共 2 h − 1 2^h - 1 2h−1个结点。编号顺序自上而下自左至右, i i i的左右儿子分别为 2 i 2i 2i 和 2 i + 1 2i+1 2i+1。 i i i 的双亲为 ⌊ i / 2 ⌋ \lfloor i/2 \rfloor ⌊i/2⌋。 i i i 为奇数则是双亲的右结点,偶数则为左结点。
完全二叉树:出最后一层外全满。
- 若 i ≤ ⌊ n / 2 ⌋ i\leq \lfloor n/2\rfloor i≤⌊n/2⌋,则结点 i i i 为分支结点,否则为叶子结点。
- 叶子结点只可能在层次最大的两层上。
- 度为1的结点可能只有1个,且该结点无右孩子。
- 一旦某结点 i i i 只有左孩子,那么大于 i i i 的所有结点均为叶子结点。
- 若n为奇数,那么每个分支结点都有左孩子和右孩子,否则会有一个只有左孩子。
二叉排序树:左子树上所有结点的关键字均小于根结点的关键字,右子树上的所有结点的关键字均大于根结点的关键字,左右子树又各是一棵二叉排序树。
平衡二叉树:树上任一结点的左子树和右子树的深度之差不超过1。
5.2.3 二叉树的性质
- 非空二叉树的叶子结点数等于度为2的结点数+1。即, n 0 = n 2 + 1 n_0 = n_2 + 1 n0=n2+1。
- 非空二叉树上第 k k k 层上至多有 2 k − 1 2^{k-1} 2k−1 个结点( k ≥ 1 k\geq 1 k≥1)。
- 高度为h的二叉树至多有 2 h − 1 2^h - 1 2h−1个结点( h ≥ 1 h\geq 1 h≥1)。
- 具有n个( n > 0 n>0 n>0)结点的完全二叉树的高度为 ⌈ l o g 2 ( n + 1 ) ⌉ \lceil log_2(n+1)\rceil ⌈log2(n+1)⌉ 或 ⌊ l o g 2 n ⌋ + 1 \lfloor log_2n \rfloor + 1 ⌊log2n⌋+1。
5.2.4 二叉树的存储结构
二叉树的存储结构是指用一组连续的存储单元依次自上而下、自左至右存储完全二叉树上的结点元素,即将完全二叉树上编号为 i i i 的结点元素存储在一维数组下标为 i − 1 i-1 i−1 的分量中。
完全二叉树和满二叉树适合采用顺序存储。(数组下标1开始存储)
树中结点的序号可以唯一地反映结点之间的逻辑关系,这样既能最大可能地节省存储空间,又能利用数组元素的下标值确定结点在二叉树中的位置,以及结点之间的关系。
对于一般的二叉树,这种方法并不适合。
链式存储结构:
typedef struct BiTNode{
ElemType data; // 数据域
struct BiTNode *lchild, *rchild; // 左右孩子指针
}BiTNode, *BiTree;
在含有n个结点的二叉链表中,含有n + 1个空链域。
5.2. examples - 1
e.1
设二叉树有2n个结点,且m<n,则不可能存在()的结点。
A. n个度为0
B. 2m个度为0
C. 2m个度为1
D. 2m个度为2
A. 可能,例n = 1,两个结点时。
B. 可能,例n = 3, n 0 = 2 , n 1 = 3 , n 2 = 1 n_0 = 2,n_1 = 3,n_2 = 1 n0=2,n1=3,n2=1。
D. 可能,例n = 3, n 0 = 3 , n 1 = 1 , n 2 = 2 n_0 = 3,n_1 = 1,n_2= 2 n0=3,n1=1,n2=2。
C. 不可能。
n 0 = n 2 + 1 n_0 = n_2 + 1 n0=n2+1
2 n = n 0 + n 1 + n 2 2n = n_0+n_1 + n_2 2n=n0+n1+n2
代入得, 2 n = 2 n 2 + 1 + n 1 2n = 2n_2 + 1 + n_1 2n=2n2+1+n1
于是, n 1 n_1 n1 一定为奇数。
e.2
一棵高度为h的满m叉树,按层次自顶向下,同一层次自左向右,顺序从1开始对全部结点进行编号。则
(1)各层结点数: m k − 1 m^{k-1} mk−1
(2)编号为 i i i 的结点的双亲结点(若存在)的编号。
结点 i i i 的第1个子女的编号为 j = ( i − 1 ) ∗ m + 2 j = (i-1)*m + 2 j=(i−1)∗m+2 ,那么结点 i i i 的双亲编号为 ⌊ ( i − 2 ) / m ⌋ + 1 ( i > 1 ) \lfloor (i-2)/m \rfloor + 1\ \ (i>1) ⌊(i−2)/m⌋+1 (i>1)
(3)编号为 i i i 的结点的第 k k k 个孩子结点(若存在)的编号。
结点 i i i 的第 k k k 个孩子结点的编号为 ( i − 1 ) ∗ m + 1 + k (i-1)*m+1+k (i−1)∗m+1+k。
(4)编号为 i i i 的结点有右兄弟的条件是什么。右兄弟的编号是多少。
结点 i i i 不是其双亲的第 m m m 个子女时才有右兄弟。当结点的编号 ( i − 1 ) % m ≠ 0 (i-1)\%m \neq 0 (i−1)%m=0 时才有右兄弟 i + 1 i+1 i+1。
5.3 二叉树的遍历
二叉树的遍历是指按某条搜索路径访问树中每个结点,使得每个结点均被访问一次,而且仅被访问一次。
由于二叉树是一种非线性结构,每个结点都可能有两棵子树,因而需要寻找一种规律,使二叉树上的结点能够排列在一个线性队列上,便于遍历。
5.3.1 先序遍历
若二叉树为空,则什么也不做;否则:
- 访问根结点。
- 先序遍历左子树。
- 先序遍历右子树。
代码:
void PreOrder(BiTree T){
if(T != NULL){
visit(T);
PreOrder(T->lchild);
PreOrder(T->rchild);
}
}
5.3.2 中序遍历
若二叉树为空,则什么也不做;否则:
- 中序遍历左子树。
- 访问根结点。
- 中序遍历右子树。
代码:
void InOrder(BiTree T){
if(T != NULL){
InOrder(T->lchild);
visit(T);
InOrder(T->rchild);
}
}
5.3.3 后序遍历
若二叉树为空,则什么也不做;否则:
- 后序遍历左子树。
- 后序遍历右子树。
- 访问根结点。
代码:
void PostOrder(BiTree T){
if(T != NULL){
PostOrder(T->lchild);
PostOrder(T->rchild);
visit(T);
}
}
5.3.4 三种遍历总结
三种遍历算法中,递归遍历左右子树的顺序都是固定的,区别只是访问根结点的顺序不同。
不管采取哪种遍历算法,每个结点都被访问一次且仅访问一次。
时间复杂度都是 O ( n ) O(n) O(n)。
递归遍历中,递归工作栈的栈深恰好为树的深度,最坏情况下,二叉树是有 n n n个结点且深度为 n n n的单支树,此时空间复杂度为 O ( n ) O(n) O(n)。
5.3.5 非递归算法
利用栈来实现。
对于后序非递归遍历,当一个结点的左右子树都被访问后才会出栈。此时栈内的元素恰好是当前结点的全部祖先。
5.3.5.1 中序遍历非递归实现
代码:
void InOrder2(BitTree T){
InitStack(S);
BitTree p = T;
while(p || !IsEmpty(S)){
if(p){ // 一直向左
Push(S, p);
p = p->lchild;
} else { // 出栈 转向右子树
Pop(S, p);
visit(p);
p = p->rchild;
}
}
}
5.3.5.2 先序遍历非递归实现
代码:
void PreOrder2(BitTree T){
InitStack(S);
BiTree p = T;
while(p || !IsEmpty(S)){
if(p){
visit(p);
Push(S, p);
p = p->lchild;
} else {
Pop(S, p);
p = p->rchild;
}
}
}
5.3.5.3 后序遍历非递归实现
要保证左孩子和右孩子都已被访问并且左孩子在右孩子前访问才能访问根结点。
思路:从根结点开始,将其入栈,然后沿其左子树一直往下搜索,直到搜索到没有左孩子的结点。按相同的规则访问右子树,右子树为空或右子树刚被访问完之后,就能保证正确的访问顺序。
代码:
void PostOrder(BiTree T){
InitStack(S);
BitTree p = T;
BitTree r = NULL;
// r用来标记最近访问过的结点
while(p || !IsEmpty(S)){
if(p){
push(S, p);
p = p->lchild;
} else {
GetTop(S, p); // p = S.top(); 只是得到栈顶元素 非出栈
if(p->rchild && p->rchild != r){
// 如果右子树是r 那么表示刚刚被访问过 那么不该向右走
// 右子树存在 先向右再向左转 正常遍历 注意压栈
p = p->rchild;
Push(S, p);
p = p->lchild;
} else {
// 右子树不存在 访问该子树根结点 并标记
Pop(S, p);
visit(p);
r = p;
p = NULL;
}
}
}
}
5.3.6 层次遍历
即按二叉树从上到下、从左至右的方式依次进行访问。
借助一个队列。
代码:
void LevelQueue(BiTree T){
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);
}
}
5.3.7 由遍历序列构造二叉树
由先序序列和中序序列可以唯一确定一棵二叉树。(不断利用先序的根分割中序的左右子树)
由后续序列和中序序列可以唯一确定一棵二叉树。
由层次序列和中序序列也可确定。
5.4 线索二叉树
5.4.1 概念
以一定的规则将二叉树中的结点排列成一个线性序列,从而得到几种遍历序列,使得该序列中的每个结点(除第一个和最后一个外),都有一个直接前驱和直接后继。
传统的二叉链表存储仅能体现一种父子关系,不能直接得到结点在遍历中的前驱后后继。
链式存储的n个结点的二叉树中的空指针数为 n + 1 n+1 n+1 ,利用这些空指针来存放其前驱或后继的指针。
可以加快查找结点前驱和后继的速度。
规定:
- 若无左子树,令 l c h i l d lchild lchild 指向其前驱结点。
- 若无右子树,令 r c h i l d rchild rchild 指向其后继结点。
因此,每个结点的信息增加两个标志域标识指针域是指向左(右)孩子还是指向前驱(后继)。
结点结构: < l c h i l d , l t a g , , d a t a , r t a g , r c h i l d > <lchild,\ ltag,\ ,data,\ rtag,\ rchild> <lchild, ltag, ,data, rtag, rchild> 。
代码描述:
typedef struct ThreadNode{
ElemType data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag; // 为0表示是否指向左右孩子 为1表示是否指向前后继
}ThreadNode, *ThreadTree;
5.4.2 中序线索二叉树的构造
二叉树的线索化是将二叉链表中的空指针改为指向前驱或后继的线索。
前驱或后继的信息只能在遍历时才能得到,因此线索化一棵二叉树的本质就是遍历一次二叉树。
代码:
void InThread(ThreadTree &p, ThreadTree &pre){
if(p == NULL) return;
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);
}
void CreateInThread(ThreadTree T){
ThreadTree pre = NULL;
if(T != NULL){
InThread(T, pre);
// 需要处理遍历的最后一个结点
pre->rchild = NULL;
pre->rtag = 1;
}
}
为了方便,可以在二叉树的线索链表上也添加一个头结点 H e a d Head Head 。
Head 的左指针 l c h i l d lchild lchild 指向二叉树的根结点, r c h i l d rchild rchild 指针域的指针指向中序遍历访问的最后一个结点。
二叉树中序序列中的第一个结点的 l c h i l d lchild lchild 域指针和最后一个结点的 r c h i l d rchild rchild 域指针均指向Head。
这样就建立了一个双向线索检索表,方便从前往后或从后往前对线索二叉树进行遍历。
5.4.3 中序线索二叉树的遍历
求中序线索二叉树中中序序列下第一个和最后一个结点:
ThreadNode *FirstNode(ThreadNode *p){
while(p->ltag == 0)
p = p->lchild;
return p;
}
ThreadNode *LastNode(ThreadNode *p){
whlie(p->rtag == 0)
p = p->rchild;
return p;
}
求中序线索二叉树中结点p在中序序列下的后继和前驱:
ThreadNode *NextNode(ThreadNode *p){
if(p->rtag == 0)
return FirstNode(p->rchild);
return p->rchild;
}
ThreadNode *PreNode(ThreadNode *p){
if(p->ltag == 0)
return LastNode(p->lchild);
return p->lchild;
}
不含头结点的中序线索二叉树的中序遍历算法:
void InOrder(ThreadNode *T){
for(ThreadNode *p = FirstNode(T); p != NULL; p = NextNode(p))
visit(p);
}
5.4.4 先序线索二叉树和后续线索二叉树
代码类似,只需变动线索化构造的代码与调用线索化左右子树递归函数的位置。
先序线索二叉树中找结点的后继:
- 如果有左孩子,则左孩子就是后继。
- 如果有右孩子,则右孩子就是后继。
- 如果为叶结点,则右链域直接指示了结点的后继。
后序线索二叉树中找结点的后继:
- 若结点 x 是二叉树的根,则其后继为空。
- 若结点 x 是其双亲的右孩子,或是其双亲的左孩子且双亲的右孩子为空,则其后继为双亲。
- 若结点 x 是其双亲的左孩子,且其双亲有右子树,则其后继为双亲右子树上按后序遍历出的第一个结点。
5.5 examples - 2
e.1
给出二叉树的自下而上、从右到左的层次遍历算法。
思想:利用原有的层次遍历算法,出队的同时将各结点指针入栈,在所有结点入栈后再从栈顶依次访问即为所求。
void InvertLevel(BitTree bt){
if(bt == NULL) return;
Stack S;
Queue Q;
InitStack(S);
InitQueue(Q);
EnQueue(Q, bt);
BitTree p;
while(IsEmpty(Q) == 0){
DeQueue(Q, p);
Push(S, p);
if(p->lchild) EnQueue(Q, p->lchild);
if(p->rchild) EnQueue(Q, p->rchild);
}
while(IsEmpty(S) == 0){
Pop(S, p);
visit(p);
}
}
e.2
假设二叉树采用二叉链表存储结构,设计一个非递归算法求二叉树的高度。
设置变量 level 记录当前层数。
设置变量 last 指向当前层的最右结点。
每次层次遍历出队时与 last 指针比较,若两者相等,则层数加1。
int BtDepth(BiTree T){
if(!T) return 0;
int front = -1, rear = -1;
int last = 0, level = 0;
BiTree Q[MaxSize];
Q[++rear] = T;
BiTree p;
while(front < rear){
p = Q[++front];
if(p->lchild) Q[++rear] = p->lchild;
if(p->rchild) Q[++rear] = p->rchild;
if(front == last){
level++;
last = rear; // last 指向下层最右结点 即当前刚入队的元素
}
}
return level;
}
e.3
设一棵二叉树中各结点的值互不相同。先序遍历序列和中序遍历序列分别存于两个一维数组A[] B[]中。
建立该二叉树的二叉链表。
BiTree PreInCreat(ElemType A[], ElemType B[], int l1, int r1, int l2, int r2){
// l1 r1 分别代表先序的第一和最后一个下标
// l2 r2 分别代表中序的第一和最后一个下标
// 初始调用 l1 = l2 = 1; r1 = r2 = n;
root = (BiTNode *)malloc(sizeof BiTNode);
root->data = A[l1];
int mid = l2;
for(;B[mid] != root->data; ++mid); // 找到根结点在中序序列中的划分
int llen = mid - l2;
int rlen = r2 - mid;
// 计算左右子树的长度
if(llen) root->lchild = PreInCreat(A, B, l1 + 1, l1 + llen, l2, l2 + llen - 1);
else root->lchild = NULL;
if(rlen) root->rchild = PreInCreat(A, B, r1 - rlen + 1, r1, r2 - rlen + 1, r2);
else root->rchild = NULL;
return root;
}
e.4
判断一棵二叉树是不是完全二叉树。采用二叉链表的方式存储。
层次遍历。无论是否是空都入队。
若当前队首元素时NULL,那么判断队列后面的元素中是否会出现非NULL的元素。
bool IsComplete(BiTree T){
Queue Q;
InitQueue(Q);
if(!T) return 1; // 空树是
EnQueue(Q, T);
while(!IsEmpty(Q)){
DeQueue(Q, p);
if(p){
// 无条件入队 无论是否是NULL
EnQueue(Q, p->lchild);
EnQueue(Q, p->rchild);
} else {
// 第一次出现NULL 即可判断出结果
while(!IsEmpty(Q)){
DeQueue(Q, p);
if(p) return 0; // 结点非空
}
return 1;
}
}
return 0;
}
e.5
设一棵二叉树的各个结点的结构为 ( L L I N K , I N F O , R L I N K ) (LLINK, INFO, RLINK) (LLINK,INFO,RLINK),ROOT 为指向该二叉树根结点结构的指针,p 和 q 分别为指向该二叉树中任意两个结点的指针,使编写 A N C E S T O R ( R O O T , p , q , r ) ANCESTOR(ROOT,\ p,\ q,\ r) ANCESTOR(ROOT, p, q, r),找到 p 和 q 的最近公共祖先 r。
后序遍历访问根结点。
采用后序非递归算法,栈中存放二叉树结点的指针,当访问到某结点时,栈中所有元素均为该结点的祖先。
后序遍历必然先遍历到 p 和 q 中某个点,此时将栈复制到另一个辅助栈中。
继续遍历到另一点时,将栈中元素逐个与辅助栈中元素匹配,第一个匹配的元素就是点 p 和 q 的最近公共祖先。
代码:
typedef struct{
BiTree t;
int tag;
// tag = 0 表示只访问过左子树
// tag = 1 表示左右均被访问过
} Stack;
BiTree Ancestor(BiTree ROOT, BiTree *p, BiTree *q){
int top = 0, top1 = 0;
Stack S[MaxSize], S1[MaxSize]; // S1 表示辅助栈
BiTree bt = ROOT;
int tag = 0; // tag 标记是否访问过第一个 即是否已经使用过辅助栈
while(bt != NULL || top > 0){
// 一直向左走
while(bt != NULL){
S[++top].t = bt;
S[top].tag = 0;
bt = bt->lchild;
}
while(top != 0 && s[top].tag == 1){
// visit 环节
if(S[top].t == p || S[top].t == q){
if(tag == 0){ // 初次遇到
tag = 1; // 标记遇到第一个
int i = 1;
for(i = 1;i <= top;++i){
S1[i] = S[i];
top1 = top;
}
} else { // 两个都已经遇到
int i;
// O(n) 遍历两个栈 找到第一个匹配的公共祖先
for(i = top; i > 0; --i){
if(i <= top1 && S1[i].t == S[i].t)
return S[i].t;
}
}
}
// visit 结束
// 访问结束后弹栈
top--;
}
if(top != 0){
// 向右下走
S[top].tag = 1;
bt = S[top].t->rchild;
}
}
return NULL;
}
e.6
写出在中序线索二叉树里查找指定结点在后序的前驱结点的算法。
在后序序列中:
- 若结点 p 有右子女,则右子女是其前驱。
- 若无右子女而有左子女,则其左子女是其前驱。
- 若左右子女均无。设它在中序线索二叉树中的前驱指向某结点f。
- 若 f 有左子女,则左子女是前驱。
- 若 f 无左子女,则继续找 f 的中序下的前驱,直到找到有左子女的结点。
- 其他情况若 p 是中序第一个点,则 p 在中序后序下均无前驱。
BiThrTree InPostPre(BiThrTree t, BiThrTree p){
BiThrTree q;
if(p->rtag == 0) q = p->rchild;
else if(p->ltag == 0) q = p->lchild;
else if(p->lchild == NULL) q = NULL; // 情况4
else {
while(p->ltag == 1 && p->lchild != NULL) p = p->lchild;
if(p->ltag == 0) q = p->lcihld; // 情况3.2
else q = NULL;
}
return q;
}