数据结构——第五章 树
第五章 树
5.1 树的基本概念
5.1.1 树的定义和基本术语
-
空树:结点数为0的树
-
非空树:有且仅有一个根结点;
没有后继的结点称为“叶子结点”或终端结点,有后继的结点称为“分支节点”或非终端结点
除了根结点外,任何一个结点都有且仅有一个前驱,每个结点可以有0个或多个后继
-
树是n个结点的有限集合;n=0时称为空树;
-
任意一棵非空树中应满足:有且仅有一个特定的成为根的结点
当n>1时,其余结点可分为m个互不相交的有限集合,其中每个集合本身又是一棵树,并且称为根结点的子树
-
结点之间的关系描述
孩子、双亲结点:结点的子树的根为该结点的孩子,该结点称为孩子的双亲
兄弟结点:具有共同的双亲
堂兄弟结点:双亲在同一层的结点
祖先结点:从根到该节结点所经分支上的所有结点
子孙结点:以某结点为根的子树中的任一结点
-
两个结点之间的路径:只能从上往下;路径长度:经过几条边
-
结点、树的属性描述
结点的层次(深度)——从上往下数(默认从1开始)
结点的高度——从下往上数;树的高度(深度)——总共多少层
结点的度——有几个孩子(分支);树的度——各结点的度的最大值
-
有序树VS无序树:逻辑上看,各子树是否有序,位置是否可互换
-
树VS森林:由m(m>=0)个互不相交的树组成森林
5.1.2 树的性质
-
树中的结点数等于所有结点的度数之和加1
-
度为m的树、m叉树的区别
树的度(度为m的数)——各结点的度的最大值
m叉树(m叉树)——每个结点最多只能有m个孩子
-
度为m的树、m叉树第i层上至多有m^i-1个结点
-
高度为h的m叉树至多有(m^h-1)/(m-1)个结点(等比数列求和)
-
高度为h的m叉树至少有h个结点
高度为h的、度为m的树至少有h+m-1个结点
-
具有n个结点的m叉树的最小高度为[log以m为底(n(m-1)+1)]
5.2 二叉树的概念
5.2.1 二叉树的定义和基本术语
-
二叉树是n个结点的有限合集;或者为空二叉树m=0
或者由一个根结点和两个互不相交的成为根的左子树和右子树组成,左子树和右子树有分别是一棵二叉树
-
特点:每个结点至多只有两棵子树;左右子树不能颠倒(有序树)
-
几个特殊的二叉树满二叉树
- 一棵深度为k且有2的k次方-1个结点的二叉树
- 完全二叉树:深度为k的具有n个结点的二叉树,当且仅当每一个编号都与深度k的满二叉树一一对应
- 二叉排序树:左子树上的结点的关键字均小于根结点的关键字,右子树大于
- 平衡二叉树:树上任一结点的左子树和右子树的深度之差不超过1
5.2.2 二叉树的性质
-
二叉树:n 0=n 2+1,度为0的结点比度为1的多一个结点
第i层至多有2(i-1)个结点;高度为h的二叉树至多有2h-1个结点
-
完全二叉树:具有n个结点的完全二叉树的高度h为[log 2 (n+1)]或[log 2 n]+1
完全二叉树可以由的结点数n推出为0、1和2的结点个数为n 0、n 1和n 2(突破点:完全二叉树最多只会有一个度为1的结点)
5.2.3 二叉树的存储结构
*二叉树的顺序存储
#define MaxSize 100
struct TreeNode{
ElemType value; //结点中的数据元素
bool isEmpty; //结点是否为空
}
main(){
TreeNode t[MaxSize];
for (int i=0; i<MaxSize; i++){ //初始化时所有结点标记为空
t[i].isEmpty = true;
}
}
-
t[0]:可以让第一个位置空缺,保证数组下标和结点编号一致
t[1]:根结点位置
-
几个重要常考的基本操作:i的左孩子——2 i;i的右孩子——2 i+1;i的父结点——[i/2];i所在的层次——[log 2 (n+1)]或[log 2 n]+1
-
完全二叉树中共有n个结点,则:判断i是否有左孩子——2 i<=n;是否有右孩子——2 i+1<=n;是否是叶子/分支结点——i>[n/2]
-
二叉树的顺序存储中,一定要把二叉树的结点编号与完全二叉树对应起来
-
非完全二叉树中共有n个结点,则:判断i是否有左孩子和右孩子——通过isEmpty判断
-
结论:二叉树的顺序存储结构,只适合存取完全二叉树
*二叉树的链式存储
typedef struct BiTnode{
ElemType data; //数据域
struct BiTNode *lchild, *rchild; //左、右孩子指针
}BiTNode, *BiTree;
- n个结点的二叉链表共有n+1个空链域
struct ElemType{
int value; //定义每一个ElemType里只有一个int型的变量
};
typedef struct BiTnode{
ElemType data; //数据域
struct BiTNode *lchild, *rchild; //左、右孩子指针
}BiTNode, *BiTree;
//定义一棵空树
BiTree root = NULL;
//插入根节点
root = (BiTree) malloc (sizeof(BiTNode));
root -> data = {1};
root -> lchild = NULL;
root -> rchild = NULL;
//插入新结点
BiTNode *p = (BiTree) malloc (sizeof(BiTNode));
p -> data = {2};
p -> lchild = NULL;
p -> rchild = NULL;
root -> lchild = p; //作为根节点的左孩子
-
找到指定结点p的左右孩子——运用左右孩子指针
-
找到指定结点p的父结点——只能从根开始遍历寻找(复杂)或运用三叉链表
typedef struct BiTnode{
ElemType data; //数据域
struct BiTNode *lchild, *rchild; //左、右孩子指针
struct BiTNode *parent; //父节点指针
}BiTNode, *BiTree;
5.3 二叉树的遍历
5.3.1二叉树的先中后序遍历
-
遍历:按照某种次序把所有结点都访问一遍
层次遍历:基于树的层次特性确定的次序规则
-
先序遍历(DLR根左右)、中序遍历(LDR左根右)、后序遍历(LRD左右根)
-
二叉树的遍历(手算):分支结点逐层展开法
-
算数表达式的“分析树”:先序遍历——前缀表达式;中序遍历——中缀表达式(要加界限符);后序遍历——后缀表达式
*先序遍历
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); //递归遍历右子树
}
}
*后序遍历
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); //访问根结点
}
}
-
遍历思想:想象补上空结点,从根结点出发,画一条路
如果左边还没有走的路,优先往左走;走到路的尽头(空结点)就往回走
如果左边没路了,就往右边走;如果左、右边都没路了,则往上走
-
每一个结点都会被路过三次:先序遍历——第一次路过时访问结点;中序遍历——第二次路过时访问结点;后序遍历——第三次路过时访问结点
*求树的深度(应用)
int treeDept(BitTree T){
if(T==NULL)
return 0;
else{
int l=treeDept(T->lchild);
int r=treeDept(T->rchild);
return l>r ? l+1:r+1 //树的深度=Max(左子树深度,右子树深度)+
}
}
- 树的深度:左子树和右子树中高的加根结点的高度
5.3.2 二叉树的层序遍历
- 算法思想:
- 初始化一个辅助队列
- 根结点入队
- 若队列非空,则队头结点出队,并将其左右孩子插入队尾(若有)
- 重复步骤3直至队列为空
*层序遍历
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);
}
}
//二叉树的结点(链式存储)
typedef struct BiTNode{
char data;
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
//链式队列结点
typedef sruct LinkNode{
BiTNode *data;
struct LinkNode *next;
}LinkNode;
type struct{
LinkNode *front,*rear; //队头、队尾指针
}
5.3.3 由遍历序列构造二叉树
- 一个前序、中序和后序遍历序列可能对应多种二叉树形态
- 前序、后序、层序+中序遍历序列可以构造出二叉树
*前序+中序遍历序列
- 前序遍历:(根结点)(左子树的前序遍历序列)(右子树的前序遍历序列)
- 中序遍历:(左子树的中序遍历)(根结点)(右子树的中序遍历)
*后序+中序遍历序列
- 后序遍历:(左子树的前序遍历序列)(右子树的前序遍历序列)(根结点)
- 中序遍历:(左子树的中序遍历)(根结点)(右子树的中序遍历)
*层序+中序遍历序列
- 层序遍历:(根结点)(左子树的根)(右子树的根)…
- 中序遍历:(左子树的中序遍历)(根结点)(右子树的中序遍历)
5.3.4 线索二叉树的概念
-
二叉树的中序遍历都必须从根结点出发,若从一指定结点开始无法进行中序遍历
-
找到指定结点p在中序遍历中的前驱,思路:
从根结点出发,重新进行中序遍历,指针q记录当前访问的结点,指针pre记录上一个结点
当qp时,pre为前驱;当prep时,q为后继
-
缺点:不方便,遍历操作必须从根开始
*中序线索二叉树
- n个结点的二叉树,有n+1个空链域(在没有孩子的结点上)!可用来记录前驱、后继的信息
- 指向前驱、后继的指针称为“线索”,前驱线索由左孩子充当,后继线索由右孩子充当
- 线索二叉树的存储结构
//二叉树的结点(对比)
typedef struct BiTNode{
ElemType data;
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
//线索二叉树的结点
typedef struct ThreadNode{
ElemType data;
struct ThreadNode *lchild,*rchild;
int ltag,rtag //左右线索标志
}ThreadNode,*ThreadTree;
-
tag0,表示指针指向孩子;tag1,表示指针是“线索”
-
中序线索二叉树——线索指向中序前驱、中序后继(以中序遍历方法其后继结点)
-
先序、后序线索二叉树与中序类似
5.3.5 二叉树的线索化
*一般方法找到中序前驱
//中序遍历
void InOrder(BiTree T){ //最好改一个函数名findPre
if(T!=NULL){
InOrder(T->lchild);
visit(T);
InOrder(T->rchild);
}
}
//访问结点q
void visit(BiTNode *q){
if(p==q)
final=pre;
else
pre=q;
}
//辅助全局变量,用于查找结点p的前驱
BiTNode *p; //p指向目标结点
BiTNode *pre=NULL; //指向当前访问结点的前驱
BiTNode *final=NULL; //全局变量;用于记录最终结果
*中序线索化
- 初步建成的树ltag、rtag=0
- 左孩子和右孩子为空时,建立前驱后继指针,并令tag=1
- 最后检查pre的rchild是否为NULL,如果是,则令rtag=1
//全局变量pre,指向当前访问结点的前驱
ThreadNode *pre=NULL;
//中序线索化二叉树T
void CreatInThread(ThreadTree T){
pre=NULL;
if(T!=NULL){
InThread(T);
if(pre->rchild==NULL)
pre->rtag=1;
}
}
//线索二叉树结点
typedef struct ThreadNode{
ElemType data;
struct ThreadNode *lchild,*rchild;
int ltag,rtag; //左、右线索标志
}ThreadNode,*ThreadTree;
//中序遍历二叉树,一边遍历一遍线索化
void InThread(ThreadTree T){
if(T!=NULL){
InTread(T->lchild);
visit(T);
InTread(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;
}
- 另外一种写法
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 CreatInThread(ThreadTree T){
ThreadTree pre=NULL; //区别:定义为CreatInThread的局部变量;InThread加引用,才能让其局部变量起到全局变量的作用
if(T!=NULL){
InThread(T,pre); //线索化二叉树
pre->rchild=NULL; //处理遍历的最后一个结点;线索化遍历完一定是最后一个右孩子,可以不用判断
pre->rtag=1;
}
}
*先序线索化
//全局变量pre,指向当前访问结点的前驱
TheardNode *pre=NULL;
//先序线索化二叉树T(同中序线索化)
void CreatPreThread(ThreadTree T){
pre=NULL;
if(T!=NULL){
InThread(T);
if(pre->rchild==NULL)
pre->rtag=1;
}
}
//先序遍历二叉树,一边遍历一遍线索化
void PreThread(ThreadTree T){
if(T!=NULL){
visit(T);
if(T->ltag==0) //不是前驱节点时,才遍历左子树;否则会一直转圈循环
InTread(T->lchild);
InTread(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;
}
- 另一种写法
void PreThread(ThreadTree p,ThreadTree &pre){
if(p!=NULL){
if(p->lchild==NULL){ //visit函数
p->lchild=pre;
p=>ltag=1;
}
if(pre!=NULL&&pre->rchild==NULL){
pre->rchild=p;
pre->rtag=1;
}
pre=p;
if(T->ltag==0) //左子树访问
PreThread(p->lchild,pre);
PreThread(p->rchild,pre); //右子树访问
}
}
//先序线索化二叉树T
void CreatPreThread(ThreadTree T){
ThreadTree pre=NULL;
if(T!=NULL){
PreThread(T,pre);
if(pre->rchild==NULL)
pre->rtag=1;
}
}
*后序线索化
//全局变量pre,指向当前访问结点的前驱
ThreadNode *pre=NULL;
//后序线索化二叉树T
void CreatPostThread(ThreadTree T){
pre=NULL;
if(T!=NULL){
PostThread(T);
if(pre->rchild==NULL)
pre->rtag=1;
}
}
//后序遍历二叉树,一边遍历一遍线索化
void PostThread(ThreadTree T){
if(T!=NULL){
InTread(T->lchild);
InTread(T->rchild);
visit(T); //访问结点q时,左子树眼镜处理完,不会出现转圈问题
}
}
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;
}
- 另一种写法相同
5.3.6 线索二叉树——找前驱/后继
*中序线索二叉树——找后继
-
若``p->rtag==1
,说明其没有右孩子,线索化后
next=p->rchild` -
若
p->rtag==0
,说明有右孩子,求其中序遍历的第一个访问结点,即next=p的右子树最左下结点
//找到以p为根的子树中,第一个被遍历的结点
TheardNode *Firstnode(TheardNode *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;
}
//对中序线索二叉树进行中序遍历(利用线索实现的非递归算法)
void Inorder(ThreadNode *T){
for(ThreadNode *p=Firstnode(T);p!=NULL;p=Nextnode(p))
visit(p);
}
- 不需要递归调用函数,时间复杂度为O(1)
*中序线索二叉树——找前驱
-
若
p->ltag==1
,说明其没有左孩子,线索化后next=p->lchild
-
若
p->ltag==0
,说明有左孩子,即next=p的左子树最右下结点
//找到以p为根的子树中,最后一个被遍历的结点
TheardNode *Lastnode(TheardNode *p){
while(p->rtag==0) //循环找到最右下的结点(不一定是叶结点)
p=p->rchild;
return p;
}
//在中序线索二叉树中找到结点p的前驱节点
ThreadNode *Prenode(ThreadNode *p){
if(p->ltag==0)
return Lastnode(p->lchild);
else
return p->lchild;
}
//对中序线索二叉树进行逆向中序遍历
void RevInorder(ThreadNode *T){
for(ThreadNode *p=Lastnode(T);p!=NULL;p=Prenode(p))
visit(p);
}
*先序线索二叉树——找后继
-
若
p->rtag==1
,说明其没有右孩子,线索化后next=p->rchild
-
若
p->rtag==0
,说明有右孩子,(根左右)但不清楚左孩子若p有左孩子,则先序后继为左孩子;若p没有左孩子,则先序后继为右孩子
*先序线索二叉树——找前驱
-
若
p->ltag==1
,说明其没有左孩子,线索化后next=p->lchild
-
若
p->ltag==0
,说明有左孩子,(根左右)左右子树都是其后继,只能从头开始先序遍历 -
改为三叉链表,增加指向其父结点的指针:
- p为其父节点的左孩子,前驱是p的父节点
- p为其父节点的右孩子,且p的父节点没有左孩子,前驱是p的父节点
- p为其父节点的右孩子,且p的父节点有左孩子,前驱是左兄弟子树的最后一个先序遍历结点(根节点出发,先往右,右没有往左,找到最后一层的结点)
- p没有父节点,为根结点,则没有先序前驱
*后序线索二叉树——找前驱
-
若
p->ltag==1
,说明其没有左孩子,线索化后next=p->lchild
-
若
p->ltag==0
,说明有左孩子,(左右根)但不清楚右孩子若p有右孩子,则后序前驱为右孩子;若p没有右孩子,则后序前驱为左孩子
*后序线索二叉树——找后继
-
若
p->ltag==1
,说明其没有左孩子,线索化后next=p->lchild
-
若
p->ltag==0
,说明有左孩子,(左右根)左右子树都是其前驱,只能从头开始先序遍历 -
改为三叉链表,增加指向其父结点的指针:
- p为其父节点的右孩子,后继是p的父节点
- p为其父节点的左孩子,且p的父节点没有右孩子,后继是p的父节点
- p为其父节点的左孩子,且p的父节点有右孩子,后继是右兄弟子树的第一个后序遍历结点(根节点出发,先往左,左没有往右,找到最后一层的结点)
- p没有父节点,为根结点,则没有后序后继
-
总结:先序线索二叉树找前驱、后序线索二叉树找后继只能用三叉链表或再根结点遍历寻找
5.4 树、森林
5.4.1 树的存储结构
- 树是一种递归定义的数据结构
*双亲表示法(顺序存储)
- 每个结点中保存指向双亲的“指针”
- 根结点固定存储在0;指针-1表示没有双亲
#define MAX_TREE_SIZE 100 //树中最多结点数
typedef struct{ //树的结点定义
ElemType data;
int parent; //双亲位置域
}PTNode;
typedef struct{ //树的类型定义
PTNode nodes[MAX_TREE_SIZE]; //双亲表示
int n; //结点数
}PTree;
-
增:新增数据元素,无需按逻辑上的次序存储
-
删(叶子结点): 将伪指针域设置为-1;用尾部的数据填补,需要更改结点数n
-
删(子树):同时删除它的子孙结点,需用到查找
-
查询:查指定结点的双亲用指针;查指定结点的孩子只能从头遍历,指针-1法产生的空数据会导致遍历更慢
-
二叉树的顺序存储中,一定要把二叉树的结点编号与完全二叉树对应起来
i的左孩子——2i;i的右孩子——2i+1;i的父节点——[i/2]
*孩子表示法(顺序+链式存储)
- 顺序存储各个节点,每个结点中保存孩子链表头指针
struct 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;
*孩子兄弟表示法(链式存储)
typedef struct CSNode{
ElemType data; //数据域
struct CSNode *firstchild, *nextsibling; //第一个孩子和右兄弟指针, *firstchild 看作左指针,*nextsibling看作右指针
}CSNode. *CSTree;
- 用孩子兄弟表示法存储的树在物理上呈现出“二叉树”的样子
*森林和二叉树的转换
- 类似孩子兄弟表示法——左孩子右兄弟
- 森林转换为二叉树:各个树的根结点视为兄弟关系
- 二叉树转换为森林:本质是用二叉链表存储森林
5.4.2 树和森林的遍历
*树的先根遍历
- 先根遍历:若树非空,先访问根节点,再依次对每棵子树进行先根遍历;(与对应二叉树的先序遍历序列相同)
void PreOrder(TreeNode *R){
if(R!=NULL){
visit(R); //访问根节点
while(R还有下一个子树T)
PreOrder(T); //先跟遍历下一个子树
}
}
*树的后根遍历
- 后根遍历:若树非空,先依次对每棵子树进行后根遍历,最后再返回根节点(与对应二叉树的中序遍历序列相同)
void PostOrder(TreeNode *R){
if(R!=NULL){
while(R还有下一个子树T)
PostOrder(T); //后跟遍历下一个子树
visit(R); //访问根节点
}
}
*树的层序遍历
-
层序遍历(队列实现)
- 若树非空,则根结点入队
- 若队列非空,队头元素出队并访问,同时将该元素的孩子依次入队
- 重复以上操作直至队尾为空
-
树的先序遍历和后序遍历——深度优先遍历;树的层序遍历——广度优先遍历
-
树的后根遍历序列与这棵树相应二叉树的中序序列相同
*森林的先序遍历
-
依次对各个树进行先根遍历
-
也可以先转换成与之对应的二叉树,对二叉树进行先序遍历
*森林的中序遍历
-
依次对各个树进行后根遍历
-
也可以先转换成与之对应的二叉树,对二叉树进行中序遍历
-
树——森林——二叉树
- 先根遍历——先序遍历——先序遍历
- 后根遍历——中序遍历——中序遍历
5.5 二叉树的应用
5.5.1 二叉排序树
*二叉排序树的定义
-
二叉排序树,又称二叉查找树
左子树所有结点关键字均小于根结点的关键字;右子树所有结点关键字均大于根结点的关键字
左子树和右子树又各是一棵二叉排序树
-
左子树结点值<根结点值<右子树结点值
-
进行中序遍历,可以得到一个递增的有序序列
*二叉排序树的查找
-
若树非空,目标值与根结点的值比较:
若相等,则查找成功
若小于根结点,则在左子树上查找,否则在右子树上查找
typedef struct BSTNode{
int key;
struct BSTNode *lchild, *rchild;
}BSTNode, *BSTree;
//在二叉排序树中查找值为key的结点(非递归)
//最坏空间复杂度:O(1)
BSTNode *BST_Search(BSTree T, int key){
while(T!=NULL && key!=T->key){ //若树空或等于跟结点值,则结束循环
if(key<T->key) //值小于根结点值,在左子树上查找
T = T->lchild;
else //值大于根结点值,在右子树上查找
T = T->rchild;
}
return T;
}
//在二叉排序树中查找值为key的结点(递归)
//最坏空间复杂度:O(h)
BSTNode *BSTSearch(BSTree T, int key){
if(T == NULL)
return NULL; //查找失败
if(key == T->key)
return T; //查找成功
else if(key < T->key)
return BSTSearch(T->lchild, key); //在左子树中查找
else
return BSTSearch(T->rchild, key); //在右子树上查找
}
*二叉排序树的插入
-
若原二叉树为空,则直接插入结点;否则:
若关键字小于根结点值,则插入到左子树
若关键字大于根节点值,则插入到右子树
-
若存在相同关键字的结点,插入失败
-
新插入的结点一定是叶子结
//在二叉排序树中插入关键字为k的新结点(递归)
//最坏空间复杂度:O(h)
int BST_Insert(BSTree &T, int k){ //此处参数为引用类型
if(T==NULL){ //原树为空,新插入的结点为根结点
T = (BSTree)malloc(sizeof(BSTNode));
T->key = k;
T->lchild = T->rchild = NULL;
return 1; //插入成功
}
else if(K == T->key) //树中存在相同关键字的结点,插入失败
return 0;
else if(k < T->key)
return BST_Insert(T->lchild,k);
else
return BST_Insert(T->rchild,k);
}
*二叉排序树的构造
//按照str[]中的关键字序列建立二叉排序树
void Crear_BST(BSTree &T, int str[], int n){
T = NULL; //初始时T为空树
int i=0;
while(i<n){
BST_Insert(T,str[i]); //依次将每个关键字插入到二叉排序树中
i++;
}
}
- 不同关键字序列可能得相同的二叉排序树
*二叉排序树的删除
-
先搜索找到目标结点
-
若被删除结点z是叶结点,则直接删除,不会破坏二叉排序树的性质
-
若结点z只有一棵左子树或右子树,则让z的子树称为z父节点的子树,替代z的位置
-
若结点z有左右两棵子树,则令z的直接后继(或直接前驱)替代z,然后从二叉排序树中删去这个直接后继(或直接前驱),这样就转换成了第一或第二种情况
(z的后继:z的右子树中最左下的结点;该节点一定没有左子树)
(z的前驱:z的左子树中最右下的结点;该节点一定没有右子树)
-
*查找效率分析
-
查找长度——在查找运算中,需要对比关键字的次数;反映了查找操作时间复杂度
-
查找成功的平均查找长度ASL:(总对比次数)/(总结点数)
-
若树高为h,则找到最下层的一个结点需要对比h次,时间复杂度为0(h)
最好情况:n个结点的二叉树最小高度为[log2n]+1;平均查找长度O(log2n)
最坏情况:每个结点只有一个分支,树高h等于结点数n;平均查找长度O(n)
-
平衡二叉树:树上的任一结点的左子树和右子树的深度之差不超过1(最好情况)
-
查找失败的平均查找长度ASL:补充失败结点后(总对比次数)/(总空结点数)
5.5.2 平衡二叉树
*平衡二叉树的定义
-
平衡二叉树,简称平衡树(AVL树)
树上的任一结点的左子树和右子树的高度之差不能超过1
-
结点的平衡因子=左子树高-右子树高
平衡二叉树结点的平衡因子的值只可能是-1,0或1
//平衡二叉树结点
typedef struct AVLNode{
int key; //数据域
int balance; //平衡因子
struct AVLNode *lchild; *rchild;
}AVLNode, *AVLTree;
*平衡二叉树的插入
-
调整插入后的“不平衡”问题:
从插入点往回找到第一个不平衡结点,调整以该节点为根的子树
每次调整的对象都是“最小不平衡子树”
-
调整最小不平衡子树
目标:恢复平衡;保持二叉排序树
-
LL: 在A结点的左孩子的左子树中插入导致不平衡
调整: A的左孩子B结点右上旋,A结点右下旋称为B的右子树的根结点,而B的右子树作为A的左子树
代码思路:指针f指向A结点,指针p指向B结点(A的左孩子),指针gf指向A结点的父节点 1. f->lchild=p->rchild; //A的左子树变成(指向)B的右孩子 2. p->rchild=f; //B的右孩子指向A 3. gf->lchild/rchild=p; //A的父节点变成B的父节点
-
RR: 在A结点的右孩子的右子树中插入导致不平衡
调整: A的右孩子B结点左上旋,A结点左下旋称为B的左子树的根结点,而B的左子树作为A的右子树
代码思路:指针f指向A结点,指针p指向B结点(A的右孩子),指针gf指向A结点的父节点 1. f->rchild=p->lchild; 2. p->lchild=f; 3. gf->lchild/rchild=p;
-
LR: 在A结点的左孩子的右子树中插入导致不平衡
调整: A的左孩子B的右孩子C的根结点,先左上旋提升到B,再右上旋提升到A
-
RL: 在A结点的右孩子的左子树中插入导致不平衡
调整: A的右孩子B的左孩子C的根结点,先右上旋提升到B,再左上旋提升到A
-
-
只有左孩子才能右上旋,只有右孩子才能左上旋
*查找效率分析
- 高为h的平衡二叉树最少有几个结点——递推求解
- 平衡二叉树最大深度为0(logn),平均查找长度/查找的时间复杂度为O(logn)
5.5.3 哈弗曼树
*带权路径长度
- 结点的权:有某种实现含义的数值(如:表示结点的重要性)
- 结点的带权路径长度:从根结点到该结点的路径长度(经过的边数)与该结点的权值的乘积
- 树的带权路径长度:树中所有叶结点的带权路径长度之和,记为WPL = ∑ wi li (n;i=1)
式中, wi是第 i个叶子结点的权值,li是根结点到第 i个叶子结点的路径长度
*哈弗曼树的定义
- 在含有n个带权叶子结点的二叉树中,带权路径长度 (WPL) 最小的二叉树称为哈夫曼树,也称为最优二叉树
*哈弗曼树的构造
- 给定n个权值分别为 w 1 , w 2 , . . . , w n的结点,构造哈弗曼树的算法描述如下:
- 将这n结点分别作为n棵仅含有一个结点的二叉树,构成森林F
- 构造一个新结点,从F中选取两棵根结点权值最小的树作为新结点的左、右子树,并且将新结点的权值置为左、右子树上根结点的权值之和
- 从 F中删除选出的两棵树,同时将新得到的树加入森林 F中
- 重复第 2、3 步骤,直到森林 F中只剩下一棵树为止
- 哈弗曼树的性质:
- 每个初始结点最终都成为叶子结点,且权值越小的结点到根结点的路径长度越大
- 构造过程共新建结点n-1个,因此哈夫曼树的结点总数为2n-1个
- 哈夫曼树中不存在度为 1 的结点;每次构造新树都选择两棵树作为左右子树
- 哈弗曼树并不唯一,但WPL必然相同且为最优
*哈弗曼编码
-
由哈弗曼树得到哈弗曼编码
字符集中的每个字符作为一个叶子结点,各个字符出现的频度作为结点的权值,再构造哈弗曼树
-
固定长度编码——每个字符用相同长度的二进制为表示——ASCII编码
-
可变长度编码——允许对不同字符用不等长的二进制位表示——哈弗曼编码
-
前缀编码——没有一个编码是另一个编码的前缀