基础知识点
- (一)树的基本概念
- (二)二叉树
- 二叉树的定义及其主要特性
- 二叉树的顺序存储结构和链式存储结构
- 二叉树的遍历
- 线索二叉树的基本概念和构造
- (三)树、森林
- 树的存储结构
- 森林与二叉树的转换
- 树和森林的遍历
- (四)树与二叉树的应用
- 二叉搜索树
- 平衡二叉树
- 哈夫曼(Huffman)树和哈夫曼编码树
知识框架
(一)树的基本概念
树的基本概念
树的定义:
树是N(N>=0)个结点的有限集合,N=0时,称为空树。
而任何一棵非空树应该满足,有且仅有一个根结点,当N>1时,其余结点又可以分为几个互不相交的有限集合,其本身又构成一个树(体现递归的定义),称为根结点的子树。
显然树的定义是递归的,是一种递归的数据结构。树作为一种逻辑结构,同时也是一种分层结构,具有以下两个特点:
- 树的根结点没有前驱结点,除根结点之外的所有结点有且仅有一个前驱结点。
- 树中所有结点可以有零个或者多个后继结点。
- 树适合于表示具有层次结构的数据。树中的某个结点(除了根结点之外)最多之和上一层的一个结点(其父结点)有直接关系,根结点没有直接上层结点,因此在n个结点的树中最多只有n-1条边。而树中每个结点与其下一层的零个或者多个结点(即其子女结点)有直接关系。 - 重要结论是,任何一个有N(N>=1)个结点的树都有N-1条边
基本术语
(按照上图解释)
- 祖先/子孙结点: 对K来说:根结点A到K的唯一路径上的任意结点,称为K的祖先结点。如结点B是K的祖先节点,K是B的子孙结点。
- 双亲/孩子结点: 路径上最接近K的结点E称为K的双亲结点,K是E的孩子结点。根A是树中唯一没有双亲的结点。
- 兄弟结点: 有相同双亲的结点称为兄弟节点,如K和L有相同的双亲结点E,即K和L是兄弟结点。
- 度: 树中一个结点的子结点个数称为该结点的度,树中结点最大度数称为树的度。如B的度为2,但是D的度为3,所以该树的度为3.
- 分支节点(非终端结点): 度大于0的结点称为分支结点(又称为非终端结点);度为0(没有子女结点)的结点称为叶子结点(又称终端结点)。在分支结点中,每个结点的分支数就是该节点的度。
- 结点的高度,深度,层次: 结点的层次从树根开始定义,根节点为第一层(有些教材将根节点定义为第0层),它的子结点为第2层,以此类推。 结点的深度是从根节点开始自顶向下逐层累加的。 结点的高度是从叶节点开始自底向上逐层累加的。
- 树的高度/深度: 树的高度(又称深度)是树中结点的最大层数。
- 有序树和无序树: 树中结点的子树从左到右是有次序的,不能交换,这样的树称为有序树。有序树中,一个结点其子结点从左到右顺序出现是有关联的。反之称为无序树。在上图中,如果将子结点的位置互换,则变为一棵不同的树。
- 路径和路径长度: 树中两个结点之间的路径是由这两个节点之间所经过的结点序列构成的,而路径长度是路径上所经过的边的个数。A和K的路径长度为3.路径为B,E。
- 森林: 森林是m棵互不相交的树的集合。森林的概念和树的概念十分相近,因为只要把树的根节点删掉之后就变成了森林。反之,只要给n棵独立的树加上一个结点,并且把这n棵树作为该结点的子树,该森林就变成了树
树的性质
- 树中结点数等于所有节点的度数+1.(结点数=总度数+1)
- 区分度为m的树和m叉树的区别
例如: 二叉树和度为2的有序树的区别:
度为2的树至少有3个结点,而二叉树则可以为空; 度为2的有序树的孩子结点的左右次序是相对于另一个孩子结点而言的,如果某个结点只有一个孩子结点,这个孩子结点就无需区别其左右次序,但是二叉树无论孩子数是否为2,均需要确定其左右次序, 也就是说二叉树结点次数不是相对于另一个结点而言,而是确定的。
- 度为m的树中第i层上至多有个m^(i-1)结点(i≥1),m叉树的第i层至多有m^(i-1)个结点(i≥1)
- 高度为h的m叉树至多有(m^(h−1))/(m−1)个结点
- 高度为h的m叉树至少有个结点; 高度为h,度为m的树至少有h+m+1个结点
- 具有n个结点的m叉树的最小高度为[logm(n(m−1)+1)],[]取整函数
树结点与度之间关系:
- 总结点数=n0+n1+n2……+nm
- 总分支数=1n1+2n2+3n3……+mnm
- 总结点数=总分支数+1
(二)二叉树
1. 二叉树的定义及其主要特性
- 二叉树的定义:
二叉树的一种特殊的树形结构,其特点是每个结点至多只有两颗子树(即二叉树中不存在度大于2的结点),并且二叉树的子树有左右之分,其次序不能交换颠倒(即二叉树是有序树)。 与一般的树相似,二叉树也有递归的形式定义,二叉树是 n ( n ≥ 0 )个结点的有限集合,为空树时 n = 0 。由一个根结点和连个互不相交的根的左子树和右子树组成,其中左、右子树均是二叉树。 - 二叉树的五种形态:
(a)空二叉树(b)只有根结点(c)只有左子树(d)左右子树都有(e)只有右子树
- 特殊二叉树
- 满二叉树:
一棵高度为h,且含有2^h-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]为叶子节点
- 二叉排序树: 一棵二叉树或者是空二叉树,或者是具有如下性质的二叉树:
- 左子树上所有结点的关键字均小于根节点的关键字;
- 右子树上所有结点的关键字均大于根节点的关键字。
4. 平衡二叉树:
树上任一结点的左子树和右子树的深度之差不超过1.
左子树深度为2,右子树深度为3
- 二叉树的主要特性:
- 非空二叉树上叶子结点树等于度为2的节点数加1,即 n0 = n2 + 1
- 非空二叉树第K层上最多有 2 ^(k-1),k>=1;m叉树的第i层至多有m^(i-1)个结点(i≥1)
- 高度为h的二叉树至多有 2^h-1,(h>=1)
- 对完全二叉树按从上到下,从左往右的顺序依次编号,则有:
- 当i>1时,结点i的双亲结点的编号为[i/2],即当 i为偶数时,其双亲结点编号为 i/2,该结点是其双亲结点的左孩子;即当 i为偶数时,其双亲结点编号为 (i-1) /2,该结点是其双亲结点的右孩子。
- 当2i≤n时,结点i的左孩子编号为2i,否则无左孩子;
- 当2i+1≤n时,结点i的左孩子编号为2i+1,否则无右孩子;
- 结点i所在层次(深度)为log2(i)+1;
- 具有n个(n>0)结点的完全二叉树的最小高度为[log2(n+1)]或者[log2(n)]+1,
2. 二叉树的顺序存储结构和链式存储结构
顺序存储结构
二叉树的顺序存储结构是指用一组地址连续的存储单元依次自上而下、从左到右存储完全二叉树上的结点,即将完全二叉树上编号为 i的结点存储在数组下标为i-1的数组分量中,然后通过一定方式确定结点在逻辑上的父子或兄弟关系。 0表示不存在的空结点,最坏情况下,一个高度为h且只有h个结点的单支树使用顺序结构需要占据2^h-1个存储单元。因此顺序存储结构适用于完全二叉树。
#define MaxSize 100
struct TreeNode{
ElemType value;//结点中的数据元素
bool isEmpty; //结点是否为空
};
TreeNode t[Maxsize];
定义一个长度为MaxSixe的数组t,按照从上至下,从左到右的顺序依次完成存储完全二叉树中的各个结点。
for(int i=0;i<MaxSize;i++){
t[i].isEmpty=true; //初始化所有结点标记为空
}
图示:
二叉树的顺序存储结构中,一定要把二叉树的结点编号与完全二叉树对于起来。
链式存储结构
链式结构是指用一个链表来存储二叉树,二叉树中的每个结点用链表中的一个链结点来存储。在二叉树中,结点结构通常包括若干数据域和若干指针域,则二叉链表至少包含三个域:数据域data、左指针域lchild、右指针域rchild。
//二叉树的结点(链式存储)
typedef struct BiTNode{
ElemType data; //数据域
struct BiTNode *lchild,*rchild; //左右孩子指针
};BiTNode,*BiTNode;
n个结点的二叉链表共有n+1个空链域。 (可用于构造线索二叉树)
初始化二叉树以及结点的插入
srruct ElemType{
int value;
}
typedef struct BiTNode{
ElemType data; //数据域
struct BiTNode *lchild,*rchild; //左右孩子指针
};BiTNode,*BiTNode;
//定义一棵空树
BiTree root=NULL;
//插入根结点
root=(BiTree)malloc(sizeof(BiTNode));
root->data={1};
root->lchild=NULL;
root->rchild=NULL;
//插入新结点
BiTree *p=(BiTNode *)malloc(sizeof(BiTNode));
p->data={2};
p->lchild=NULL;
p->rchild=NULL;
root->lchild=p; //作为根结点的左孩子
三叉链表构造想法,加入父结点指针parent
//二叉树的结点(链式存储)
typedef struct BiTNode{
ElemType data; //数据域
struct BiTNode *lchild,*rchild; //左,右孩子指针
struct BiTNode *parent; //父结点指针
};BiTNode,*BiTNode;
3. 二叉树的遍历
遍历:是指按照某条搜索路径访问树中的每一个结点,使得每个结点均会被访问一次,而且仅被访问一次。
先序遍历
Preorder先序遍历操作:
如果二叉树为空,则什么也不做,否则:
- 访问根结点
- 先序遍历左子树
- 先序遍历右子树
//先序遍历递归遍历算法
void PreOrder(BiTree T){
if(T != NULL){
visit(T); //访问根结点
PreOrder(T->lchild); //递归遍历左子树
PreOrder(T->rchid); //递归遍历右子树
}
}
借助栈,可将二叉树的三种递归遍历算法转换为非递归算法。
先序遍历非递归算法:和中序遍历类似,改变在于访问操作在于入栈操作的前面。
//先序遍历非递归遍历算法
void PerOrder_NonRrecurrence(BiTree T){
InitStack(s); //创建栈并初始化
BiTNode p = T; //创建遍历指针 p
while(p || !IsEmpty(s)){ //若指针 p 不空或者栈不空时循环
if(p){ //若指针 p 不空(非空左子树)
visit(p); //访问该结点
Push(s, p); //p 指向的结点入栈
p = p->lchild; //指针 p 指向左子树
}
else{ //根结点的所有左结点均入栈
Pop(s, p); //一个结点出栈
p = p->rchild;
//指针 p 指向该结点的右子树,下一个循环遍历该右子树的所有左结点
}
}
}
中序遍历
Inorder中序遍历操作:
如果二叉树为空,则什么也不做,否则:
- 中序遍历左子树
- 访问根结点
- 中序遍历右子树
//中序遍历递归遍历算法
void InOrder(BiTree T){
if(T != NULL){
InOrder(T->lchild); //递归访问左子树
visit(T); //访问根结点
InOrder(T->rchild); //递归访问右子树
}
}
中序遍历非递归算法:
先扫描(非访问操作)根结点的所有左结点并将其一一入栈,然后出栈一个结点*p (该结点没有左孩子结点或左孩子结点已经全被访问过)并访问。然后扫描该结点的右孩子结点并将其入栈,再扫描该右孩子结点的所有左结点并入栈。重复上述操作直到栈空为止。
//中序遍历非递归遍历算法
void InOrder_NonRrecurrence(BiTree T){
InitStack(s); //创建栈并初始化
BiTNode p = T; //创建遍历指针 p
while(p || !IsEmpty(s)){ //若指针 p 不空或者栈不空时循环
if(p){ //若指针 p 不空(非空左子树),则遍历左子树入栈
Push(s, p); //p 指向的结点入栈
p = p->lchild; //指针 p 指向左子树
}
else{ //根结点的所有左结点均入栈
Pop(s, p); //一个结点出栈
visit(p); //访问该结点
p = p->rchild;
//指针 p 指向该结点的右子树,下一个循环遍历该右子树的所有左结点
}
}
}
后序遍历
Postorder中序遍历操作:
如果二叉树为空,则什么也不做,否则:
- 后序遍历左子树
- 后序遍历右子树
- 访问根结点
//后序遍历递归遍历算法
void PostOrder(BiTree T){
if(T != NULL){
PostOrder(T->lchild); //递归遍历左子树
PostOrder(T->rchild); //递归遍历右子树
visit(T); //访问根结点
}
}
后序遍历非递归算法:
从根结点开始,将其入栈,然后沿其左子树一直往下搜索,直到搜索到没有左孩子的结点,但是此时不能进行出栈并访问,因为如果其有右子树,还需按相同的规则对其右子树进行处理。直至上述操作进行不下去,若栈顶元素想要出栈被访问,要么右子树为空,要么右子树刚被访问完(此时左子树早已被访问完),这样就保证了正确的访问顺序。
//后序遍历非递归遍历算法
void PostOrder_NonRrecurrence(BiTree T){
InitStack(s); //创建栈并初始化
BiTNode p = T; //创建遍历指针 p
while(p || !IsEmpty(s)){ //若指针 p 不空或者栈不空时循环
if(p){ //若指针 p 不空(非空左子树)
Push(s, p); //p 指向的结点入栈
p = p->lchild; //指针 p 指向左子树
}
else{ //向右
GetTop(s,p); //读栈顶结点(非出栈)
if(p->rchild&&p->rchild!=r)
//若右子树存在,且从未被访问过
p=p->rchild;//转向右
else{ //否则,弹出结点并访问
pop(s,p); //将结点弹出
visit(p->data);//访问该结点
r=p; //记录最近访问过的结点
p=NULL; //结点访问完后,重置p指针
}
}//else
}//while
}
层次遍历
二叉树的层次遍历,即按照箭头所指方向,按照层次1,2,3,4的顺序,横向遍历对二叉树中的结点进行访问。
进行层次遍历需要借助队列:先将二叉树根结点入队,然后出队并访问该结点,若有左子树,则将左子树根结点入队;若有右子树,则将右子树根结点入队。然后出队并对出队结点进行访问,重复上述操作直到队列为空。
//层次遍历非递归遍历算法
void LevelOrder(BiTree T){
InitQueue(Q); //创建空队列 Q
BiTree p; //创建遍历指针 p
EnQueue(Q, T); //根结点入队
while(! IsEmpty Q){ //队列非空则继续循环进行遍历
DeQueue(Q, p); //队头元素出队
visit(p); //访问出队结点
if(p->lchild != NULL) //若左子树存在
EnQueue(Q, p->Lchild); //左子树根结点入队
if(p->child != NULL) //若右子树存在
EnQueue(Q, p->rchild); //右子树根结点入队
}
}
由遍历序列构造二叉树
key:找到树的根结点,并根据中序序列划分左右子树,再找到左右子树根结点。 (需要仔细画图分析理解先后层序和中序组合所产生的结果组合)
- 先序序列+中序序列
- 后序序列+中序序列
- 层序序列+中序序列 同时:先序,后序 ,层序序列的两两组合无法唯一确定一棵二叉树。
4. 线索二叉树的基本概念和构造
基本概念
定义: (前提)N个节点的二叉树中,有n+1个空指针。这是因为每个叶节点都有两个空指针,而每一个度为1的节点有一个空指针。总的空指针数目为2n0+n1,又有n0=n2+1。意思是二倍的叶子节点加上1倍的一个孩子的节点的数目。
在二叉树线索化时,通常规定:若无左子树,则令lchild指向其前驱结点;若无右子树,则令rchlid 指向其后继结点。增加两个标志域表明当前指针域所指向对象是前、后驱结点还是左、右子树结点。
标志域的含义:
线索二叉树的存储结构:
//线索二叉树的存储结构
typedef struct ThreadNode{
ElemType data; //数据元素
struct ThreadNode *lchild,*rchild; //左右孩子指针
int ltag,rtag; //左右线索标志
} ThreadNode, *ThreadTree;
以上述结点结构构成的二叉链表作为二叉树的存储结构,称为线索链表,其中指向结点前驱和后继的指针称为线索。具有线索的二叉树称为线索二叉树。对二叉树以某种次序遍历使其变为线索二叉树的过程称为线索化。
线索二叉树的构造
二叉树的线索是将二叉树的空指针改为指向前驱或者后继的线索。而前驱或者后继的信息只有在遍历中才能得到,因此对二叉树的线索化,实质上就是遍历一次二叉树,在遍历的过程中,检查当前结点的左、右指针域是否为空,若为空,将其改为指向前驱结点或后继结点的线索。
中序线索二叉树为例
![]()
通过中序遍历对于二叉树线索化的递归算法
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); //递归,线索化右子树
}//if(p!=NULL)
}
通过中序遍历建立中序线索二叉树的主过程算法
void CreateInThread(ThreadTree T){
ThreadTree pre=NULL;
if(T!=NULL){ //非空二叉树,线索化
InThread(T,pre); //线索化二叉树
pre->rchild=NULL; //处理遍历的在最后一个结点
pre->rtag=1;
}
}
为了操作方便,在二叉树的线索链表上添加头结点,并让头结点的lchild域的指针指向二叉树的根结点,让rchild域的指针指向中序遍历时访问到的最后一个结点。反之,让中序序列的第一个结点和最后一个结点的lchild域的指针都指向头结点。这相当于为二叉树建立了一个双向线索链表,方便从后往前或者从前往后对线索二叉树进行遍历。
线索二叉树的遍历
中序线索二叉树的遍历
中序线索二叉树的结点中隐含了线索二叉树的前驱和后继信息。在对其进行遍历时,只要先找到序列中的第一个结点,然后依次找结点的后继,直至其后继为空。在中序线索二叉树中找结点后继的规律是:若其右标志为“1”,则右链是线索,指示其后继,否则遍历右子树中第一个访问的结点(右子树中最左下的结点)为其后继。
不含头结点的线索二叉树的遍历:
- 求中序线索二叉树中中序序列下的第一个结点:
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直接返回后继线索
}
- 不含头结点的中序二叉树的中序遍历算法:
void Inorder(ThreadNode *T){
for(ThreadNode *p=Firstnode(T);p!=NULL;P=Nextnode(p))
visit(p);
}
先序线索二叉树和后序线索二叉树的建立相对于只需要变动线索改造的代码段与调用线索化左右子树递归函数的位置。先序线索二叉树和后序线索二叉树的后继类似。
如何在先序线索二叉树中找结点的后继﹖如果有左孩子,则左孩绊就是其后继;如果无左孩子但有右孩子,则右孩子就是其后继;如果为叶结点,则右链域直接指示了结点的后继。
在后序线索二叉树中找结点的后继较为复杂,可分3种情况:①若结点x是二叉树的根,则其后继为空:②若结点x是其双亲的右孩子,或是其双亲的左孩子且其双亲没有右子树,则其后继即为双亲;③若结点x是其双亲的左孩子,且其双亲有右子树,则其后继为双亲的右子树上按后序遍历列出的第一个结点。图中找结点B的后继无法通过链域找到,可见在后序线索二叉树上找后继时需知道结点双亲,即需采用带标志域的三叉链表作为存储结构。