目录
- 一、树(Tree)的概念
- 1.1、树的基本定义
- 1.2、基本术语
- 1.2、树的性质
- 二、二叉树
- 2.1、二叉树的定义
- 2.2、特殊二叉树
- 2.2.1、满二叉树
- 2.2.2、完全二叉树
- 2.2.3、二叉排序树
- 2.2.4、平衡二叉树
- .3、二叉树的性质
- 2.4、二叉树存储的实现
- 2.4.1、顺序存储
- 2.4.2、链式存储
- 三、二叉树的遍历和线索二叉树
- 3.1、先中后序遍历
- 3.1.1、先序遍历(NLR)
- 3.1.2、中序遍历(LNR)
- 3.1.3、后序遍历(LRN)
- 3.2、层序遍历
- 3.3、由遍历序列构造二叉树
- 3.4、线索二叉树的概念及作用
- 3.5、二叉树的线索化
- 3.5.1、中序线索化及存储
- 3.5.1、先序线索化的存储
- 3.5.1、后序线索化的存储
- 3.6、在线索二叉树中找前驱和后继
- 3.6.1、中序线索化中找到指定结点*p的前驱后继
- 3.6.2、先序线索化中找到指定结点*p的前驱后继
- 3.6.3、后序线索化中找到指定结点*p的前驱后继
- 四、树和森林
- 4.1、树的存储结构
- 4.1.1、双亲表示法
- 4.1.2、孩子表示法
- 4.1.3、孩子兄弟表示法
- 4.2、树和森林的遍历
- 4.2.1、树的先根遍历
- 4.2.2、树的后根遍历
- 4.2.3、树的层序遍历(队列实现)
- 4.2.4、森林的遍历
- 五、应用
- 5.1、二叉排序树
- 5.1.1、二叉排序树的常见操作
- (1)查找
- (2)插入
- (3)构造
- (4)删除
- 5.2、平衡二叉树(Balanced Binary Tree)
- 5.2.1、平衡二叉树的插入
- (1)LL
- (2)RR
- (3)LR
- (4)RL
- 5.3、哈夫曼树
- 5.3.1、哈夫曼树的概念
- 5.3.2、哈夫曼树的构造
- 5.3.3、哈夫曼编码
一、树(Tree)的概念
1.1、树的基本定义
树:是n(n>=0)个结点的有限集合,是一种逻辑结构,当n=0时,为空树,非空树满足:
- 有且仅有一个特定的称为根的结点;
- 当n>1时,其余结点可分为m(m>0)个互不相交的有限集合T1、T2、T3、…、Tm,其中每个集合本身又是一棵树,并且称为根节点的子树。
- 没有后继的结点称为叶结点(或终端结点);
- 有后继的结点称为分支结点(或非终端结点);
- 除了根结点外,任何一个结点都有且仅有一个前驱;
- 每个结点可以有0个或多个后继。
树是一种递归的数据结构。
1.2、基本术语
结点关系网:
- 祖先结点:自己之上的都是祖先结点;
- 子孙节点:自己之下的都是子孙结点;
- 双亲结点:和自己相连的上一个就是双亲结点;
- 孩子结点:和自己相连的下面的结点;
- 兄弟结点:和自己同一双亲结点的;
- 堂兄弟结点:和自己同一层的。
结点的属性:
- 结点的层次(深度):从上往下数(从根结点往下发散)
- 结点的高度:从下往上数(理解:一个树杈有多高,是相对地面来说)
- 树的高度:拢共的层数(根节点为第一层)
- 结点的度:是指该结点有多少个孩子(分支)
- 树的度:是各结点度的最大值
A的度:3 ;B的度:2 ;C的度:1 ;D的度:3
树的度就等于最大的结点度的个数:3
树的深度:4
有序树: 逻辑上看,树中结点的各子树从左到右是有顺序的,不能互换;
无序树: 逻辑上看,树中结点的各子树从左到右没有顺序,可以互换。
1.2、树的性质
①树的总结点数 = 总的度数 + 1;(上图:3+2+1+3+2+0+0+1+0+0+1 = 13)
②度为m的树和m叉树的区别:
度为m是指树中各结点最大的度数(并未限制),m叉树是指树中每个结点最多只能有m个孩子的树(做出了限制);
③度为m的树的第i层至多有
个结点;
④高度为h的m叉树最多有个结点;(实际就是公比为m的等比数列的前h项的和)
⑤高度为h的m叉树至少有h个结点(假设每一层就一个,有h层),高度为h、度为m的树至少有h+m-1个结点(假设就一个度为m的结点,其余结点的度全为1,最后一个结点的度为0);
⑥具有n个结点的m叉树的最小高度为(对应④,思想就是在n>m的情况下,把每一层都装满再增加下一层)
二、二叉树
2.1、二叉树的定义
二叉树是n(n>=0)个结点的有限集合:
- n=0时,为空二叉树;
- 由一个根结点和两个互不相交的被称为根的左子树和右子树组成,左子树和右子树也分别是一颗二叉树。
特点:
- 每个结点至多只有两棵树;
- 左右子树不能颠倒,二叉树为有序树;
- 二叉树可以是空集合,根可以有空的左子树和空的右子树。
2.2、特殊二叉树
2.2.1、满二叉树
定义:一颗深度为k且每层都有个结点的二叉树称为满二叉树。
特点:
- 每一层的结点树都达到最大;
- 叶子全在最底层;
- 按层序1开始编号,结点i的左孩子为2i,右孩子为2i+1,结点i的父节点为[i/2]([ ]为取整)。
2.2.2、完全二叉树
定义:深度为k的具有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号为1~n的结点一一对应时,称之为满二叉树。
特点:
- 最多只有一个度为1的结点,当n为偶数时,完全二叉树最后一个孩子为左孩子,当n为奇数时,完全二叉树最后一个孩子为右孩子;
- 只有最后两层可能有叶子;
- 按层序1开始编号,结点i的左孩子为2i,右孩子为2i+1,结点i的父节点为[i/2]([ ]为取整);
- i<=[n/2]的结点为分支结点,i>[n/2]的结点为叶子结点。
2.2.3、二叉排序树
概念:一颗二叉树或是空二叉树,或者是具有如下性质的二叉树:
- 左子树是上所有结点的关键字均小于根结点的关键字;
- 右子树上所有结点的关键字均大于根结点的关键字;
- 左子树和右子树各是一颗二叉排序树。
2.2.4、平衡二叉树
概念:树上任一结点的左子树和右子树的深度之差不超过1。
.3、二叉树的性质
- 叶子结点 = 二分支结点 + 1,非空二叉树中度为0、1、2的节点个数为n0、n1、n2,那么n0 = n2 + 1 ;
- 二叉树第k层至多有个结点,m叉树的第k层至多有个结点;
- 高度为h的二叉树至多有个结点(满二叉树),高度为h的m叉树最多有个结点;
- 具有n个结点的完全二叉树的高度h为或者;
- 对于完全二叉树,可以由总结点数n推出度为0、1、2的结点的个数n0、n1、n2:
①由于n0 = n2 + 1,所以n0+n2=2*n2+1,n0+n2就为一个奇数;
②因为n0+n1+n2=n,会有以下两种情况:
a.当n为奇数时,由于n0+n2为奇数,所以得到n1为奇数,而完全二叉树度为1的结点最多只有1个,当n1为奇数时,n1就等于1,那么n1=n/2,n2=n/2 -1;
b.当n为偶数时,n1=0,那么n0=(n+1)/2,n2=(n+1)/2 -1。
2.4、二叉树存储的实现
2.4.1、顺序存储
二叉树的顺序存储中,一定要把二叉树的结点和结点编号与完全二叉树对应起来。
最坏情况:高度为h的只有h个结点的单支树(所有结点都只有右孩子),也至少需要个存储单元,因为在进行顺序存储时,必须用“虚结点”将一颗二叉树补成一颗完全二叉树来存储,否则无法确认结点之间的关系,这样就会造成存储空间的浪费,如下:
常见的基本操作:
- i的左孩子:2i;
- i的右孩子:2i+1
- i的父节点:[i/2]
- i所在的层次:或者
常见的判断条件: - 判断i是否有左孩子:2i<=n(满足就有)
- 判断i是否有右孩子:2i+1<=n
- 判断i是否有分支结点:i>n/2
#define MaxSize 100
struct TreeNode{
ElemType value;//结点中的数据元素
bool isEmpty;//结点是否为空
}
main(){
TreeNode t[MaxSize];
for(int i=0;i<MaxSize;i++){
t[i].isEmpty = true;
}
}
2.4.2、链式存储
设计不同的结点结构可以构成不同形式的链式存储结构。由二叉树的定义可知,二叉树的结点由一个数据元素和分别指向左、右子树的两个分支构成,则表示二叉树的链表中的结点至少包含3个域:数据域,左指针域和右指针域。利用这种结点结构所得的二叉树的存储结构称为二叉链表,如下图所示,容易证明,在具有n个结点的二叉链表中有n+1个空链域:
struct ElemType{
int value;
};
typedef struct Bitnode{
ElemType data;//数据域
struct Bitnode *lchild, *rchild;//左、右孩子指针
}Bitnode,*Bittree;
//定义一颗空树
struct root = NULL;
//插入根结点
root = (Bittree)malloc(sizeof(Bitnode));
root->deta = {1};
root->lchild=NULL;
root->rchild=NULL;
//插入新结点
Bitnode *p = (Bittree)malloc(sizeof(Bitnode));
p->data={2};
p->lchilid=NULL;
p->rchild=NULL;
root->lchild = p;//结点p作为根结点root的左孩子
三、二叉树的遍历和线索二叉树
遍历:按照某种次序把所有结点都访问一遍;
层次遍历:基于树的层次特性确定的次序规则,从上到下,从左到右访问。
二叉树的递归特性:
- 为空二叉树;
- 结构为:根结点+左子树+右子树的二叉树
3.1、先中后序遍历
3.1.1、先序遍历(NLR)
遍历次序为:根结点、左子树、右子树
typedef struct Bitnode{
int data;
struct Bitnode *lchild, *rchild;
}Bitnode, *Bittree;
//先序遍历
void PreOrder(Bittree T){
if(T!=NULL){
visit(T);//访问根结点
PreOrder(T->lchild);//递归遍历左子树
PreOrder(T->rchild);//递归遍历右子树
}
}
3.1.2、中序遍历(LNR)
遍历此次为:左子树、根结点、右子树
typedef struct Bitnode{
int data;
struct Bitnode *lchild, *rchild;
}Bitnodr, *Bittree;
//中序遍历
void Inorder(Bittree T){
if(T!=NULL){
InOrder(T->lchild);
visit(T);
InOrder(T->rchild);
}
}
3.1.3、后序遍历(LRN)
遍历次序为:左子树、右子树、根结点
typedef struct Bitnode{
int data;
struct Bitnode *lchild, *rchild;
}Birnode, *Bittree;
//后序遍历
void BackOrder(Bittree T){
if(T!=NULL){
BackOrder(T->lchild);
BackOrder(T->rchild);
visit(T);
}
}
3.2、层序遍历
算法思想:
- 初始化一个辅助队列;
- 根结点入队;
- 若根结点非空,则对头结点出队,访问该结点,并将孩子插入队尾(如果有的话);
- 重复3直至队列为空。
//二叉树的结点(链式存储)
typedef struct Bitnode{
int data;
struct Bitnode *lchild, *rchild;
}Bitnode, *Bittree;
//辅助队列结点(链式存储)
typedef struct Linknode{
Bitnode *data;
Linknode *next;
}Linknode;
typedef struct{
Linknode *front, *rear;
}LinkQueue;
//层序遍历
void LevelOrder(Bittree T){
LinkQueue(Q);
InitQueue(Q);//初始化辅助队列
Bittree 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);//右孩子入队
}
}
3.3、由遍历序列构造二叉树
- 已知二叉树的前序遍历和中序遍历,可以确定一颗二叉树;
- 已知二叉树的中序遍历和后序遍历,可以确定一颗二叉树;
- 已知二叉树的层序遍历和中序遍历,可以确定一颗二叉树;
- 已知二叉树的前序遍历和后序遍历,无法确定一颗二叉树。
主要是可以确定根结点的位置,就能推出左右子树有哪些结点。
3.4、线索二叉树的概念及作用
- n个结点的二叉树,有n+1个空链域,可用来记录前驱、后继的信息。指向前驱、后继的指针被称为“线索”,形成的二叉树被称为线索二叉树;
- 线索二叉树的结点在原本二叉树的基础上,新增了左右线索标志tag,当tag0时,表示指针指向孩子,当tag1时,表示指针指向“线索”(也就是指向前驱或者后继):
ltag1时,表示lchild指向前驱,ltag0时,表示lchild指向左孩子;
rtag1时,表示lchild指向后继,rtag0时,表示lchild指向右孩子。
typedef struct ThreadNode{
int data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag;//左、右线索标志
}ThreadNode, *ThreadTree;
3.5、二叉树的线索化
在二叉树的结点上加上线索的二叉树被称为线索二叉树,对二叉树以某种遍历方式(先序、中序、后序、层序)进行遍历,使其变为线索二叉树的过程叫做二叉树的线索化。
3.5.1、中序线索化及存储
typedef struct ThreadNode{
int data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag; // 左、右线索标志
}ThreadNode, *ThreadTree;
//全局变量pre, 指向当前访问的结点的前驱
TreadNode *pre=NULL;
void InThread(ThreadTree T){
if(T!=NULL){
InThread(T->lchild); //中序遍历左子树
visit(T); //访问根节点
InThread(T->rchild); //中序遍历右子树
}
}
void visit(ThreadNode *q){
if(q->lchid = NULL){ //左子树为空,建立前驱线索
q->lchild = pre;
q->ltag = 1;
}
if(pre!=NULL && pre->rchild = NULL){
pre->rchild = q; //建立前驱结点的后继线索
pre->rtag = 1;
}
pre = q;
}
//中序线索化二叉树T
void CreateInThread(ThreadTree T){
pre = NULL; //pre初始为NULL
if(T!=NULL);{ //非空二叉树才能进行线索化
InThread(T); //中序线索化二叉树
if(pre->rchild == NULL)
pre->rtag=1; //处理遍历的最后一个结点
}
}
3.5.1、先序线索化的存储
typedef struct ThreadNode{
int data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag; // 左、右线索标志
}ThreadNode, *ThreadTree;
//全局变量pre, 指向当前访问的结点的前驱
TreadNode *pre=NULL;
//先序遍历二叉树,一边遍历一边线索化
void PreThread(ThreadTree T){
if(T!=NULL){
visit(T);
if(T->ltag == 0) //重点☆:先序线索化中,只有当lchild==0时,才能对左子树线索化,lchild不是前驱线索
PreThread(T->lchild);
PreThread(T->rchild);
}
}
void visit(ThreadNode *q){
if(q->lchid = NULL){ //左子树为空,建立前驱线索
q->lchild = pre;
q->ltag = 1;
}
if(pre!=NULL && pre->rchild = NULL){
pre->rchild = q; //建立前驱结点的后继线索
pre->rtag = 1;
}
pre = q;
}
//先序线索化二叉树T
void CreateInThread(ThreadTree T){
pre = NULL; //pre初始为NULL
if(T!=NULL);{ //非空二叉树才能进行线索化
PreThread(T); //先序线索化二叉树
if(pre->rchild == NULL)
pre->rtag=1; //处理遍历的最后一个结点
}
}
3.5.1、后序线索化的存储
typedef struct ThreadNode{
int data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag; // 左、右线索标志
}ThreadNode, *ThreadTree;
//全局变量pre, 指向当前访问的结点的前驱
TreadNode *pre=NULL;
//后序遍历二叉树,一边遍历一边线索化
void PostThread(ThreadTree T){
if(T!=NULL){
PostThread(T->lchild);
PostThread(T->rchild);
visit(T); //访问根节点
}
}
void visit(ThreadNode *q){
if(q->lchid = NULL){ //左子树为空,建立前驱线索
q->lchild = pre;
q->ltag = 1;
}
if(pre!=NULL && pre->rchild = NULL){
pre->rchild = q; //建立前驱结点的后继线索
pre->rtag = 1;
}
pre = q;
}
//后序线索化二叉树T
void CreateInThread(ThreadTree T){
pre = NULL; //pre初始为NULL
if(T!=NULL);{ //非空二叉树才能进行线索化
PostThread(T); //后序线索化二叉树
if(pre->rchild == NULL)
pre->rtag=1; //处理遍历的最后一个结点
}
}
3.6、在线索二叉树中找前驱和后继
3.6.1、中序线索化中找到指定结点*p的前驱后继
- 若p->rtag==1,则next = p->rchild;
- 若p->rtag==0,则后继next为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;
}
// 对中序线索二叉树进行中序循环(非递归方法实现)
void InOrder(ThreadNode *T){
for(ThreadNode *p=FirstNode(T); p!=NULL; p=NextNode(p)){
visit(p);
}
}
- 若p->ltag==1,则pre = p->lchild;
- 若p->ltag==0,则pre为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 LastNode(p->lchild);
else
return p->lchild;
}
// 对中序线索二叉树进行中序循环(非递归方法实现)
void RevOrder(ThreadNode *T){
for(ThreadNode *p=LastNode(T); p!=NULL; p=PreNode(p))
visit(p);
}
3.6.2、先序线索化中找到指定结点*p的前驱后继
-
若p->rtag==1,则next=p->child;
-
若p->rtag==0:
①若p有左孩子,则先序后继为左孩子;
②若p没有左孩子,则先序后继为右孩子。 -
前提:改用三叉链表,可以找到结点*p的父结点;
-
如果能找到p的父结点,且p是左孩子,则父结点为p的前驱;
如果能找到p的父结点,且p是右孩子,其左兄弟为空,则父结点为其前驱;
如果能找到p的父结点,且p是右孩子,其左兄弟非空,则p的前驱为左兄弟子树中最后一个被遍历的结点。 -
如果p是根结点,则p没有先序前驱。
3.6.3、后序线索化中找到指定结点*p的前驱后继
-
若p->ltag==1,则pre=p->lchild;
-
若p->ltag==:
①若p有右孩子,则后续前驱为右孩子;
②若p没有右孩子,则后续前驱为左孩子。 -
前提:改用三叉链表,可以找到p的父结点;
-
如果能找到p的父结点,且p是右孩子,则父结点为其后续后继;
如果能找到p的父结点,且p是左孩子,p的右兄弟为空,则父结点为其后续后继;
如果能找到p的父结点,且p是左孩子,p的右兄弟为非空,则其后续后继为右兄弟子树中第一个被后序遍历的结点; -
若p是根结点,则p没有后续后继。
四、树和森林
森林是m(m>=0)颗互不相交的树的集合。
4.1、树的存储结构
4.1.1、双亲表示法
顺序存储:每个结点中保存着指向双亲的指针。
#define MaxSize 100
//定义树的结点
typedef struct{
int data;
int parent;//双亲位置域
}PTnode;
//定义树的类型
typedef struct{
PTnode nodes[MaxSize];//双亲表示
int n, r;//结点数和根的位置
}PTree;
- 采用数组的形式,把根结点固定在数组下标为0的位置上,且用-1表示根结点没有父结点;
- 不需要按照层序遍历的顺序去排列,物理上可以乱序;
- 新增数据元素时,无需按照逻辑上的次序存储,写入所增加元素的值,并记录与双亲的关系即可;
A为根结点的数据,其没有父结点,所以用-1表示,数组下标从0开始,所以A在数组中第一位,其下标为0;
B、C、D的父结点为A,A的下标为0;E、F的父结点为B,B是数组中第二位,下标为1;G的父结点为C,C是数组中第三位,下标为2;H、I、J的父结点为D,D是数组中第四位,下标为3;…以此类推 - 删除结点时,要把所删除结点的双亲指针设为-1,表示这个位置为空,最后将结点数n–;
- 可以把尾部数据移动上来填充要被删除的元素,最后结点数n–;
- 如果要删除的是一颗子树的结点,那么要将这颗子树的所有结点都删掉,此时要找到孩子结点,就要用到查询操作;
- 查询:双亲表示法用来查结点的双亲很方便,但查找孩子就只能从头到尾进行遍历来比对。
4.1.2、孩子表示法
顺序+链式存储
顺序存储每个结点,每个结点中保存着孩子链表头指针。
#define MaxSize 100
//只保存了各个孩子的数组下标
struct CTnode{
int child;//孩子在数组中的位置
struct CTnode *next;//下一个孩子
};
//保存结点实际数据
typedef struct{
ElemType data;
struct CTnode *firstchild;//第一个孩子
}CTbox;
typedef strct{
CTbox nodes[MaxSize];
int n,r;//结点数和根的位置
}CTree;
图中^符号指其无孩子。
4.1.3、孩子兄弟表示法
链式存储
typedef struct CSnode{
char data;
struct CSnode *firstchild, *nextsibling;//第一个孩子和右兄弟
};
- 这种链式存储和二叉链表相似;
- 可以把firstchild看成lchild,把nextsibling看成rchild;
- 孩子结点就当做左孩子结点,兄弟结点就当做右孩子结点;
- 用二叉树的操作来处理。
4.2、树和森林的遍历
树的遍历是指用某种访问方式访问树中每个结点,且仅访问一次。
4.2.1、树的先根遍历
若树非空,先访问根结点,再一次对每颗子树进行先根遍历;树的先根遍历和二叉树先序遍历的序列相同。
void PreOrder(TreeNode R){
if (R!=NULL){
visit(R);//访问根结点
while(R还有下一个子树T)
PreOrder(T);
}
}
对这个树进行先根遍历,序列为:ABEFCDG
4.2.2、树的后根遍历
若树非空,先子树再根结点,树的后根遍历序列与二叉树的中序遍历序列相同。
void PostOrder(TreeNode *R){
if(R!=NULL){
while(R还有下一个子树T)
PostOrder(T); //后跟遍历下一个子树
visit(R); //访问根节点
}
}
对这棵树进行后根遍历,序列为:EFFBCGDA
4.2.3、树的层序遍历(队列实现)
- 若树非空,则根结点入队;
- 若队列非空,对头元素出队并访问,同时将该元素的孩子依次入队;
- 重复以上操作直至队列变空。
4.2.4、森林的遍历
森林的遍历有两种方法:
法一,先序遍历森林。若森林非空,则按照如下规则进行遍历:
- 访问森林中第一棵树的根结点;
- 先序遍历第一棵树中根结点的子树森林;
- 先序遍历除去第一棵树之后剩余的树构成的森林。
法二,后序遍历森林。若森林非空,则按照如下规则进行遍历: - 后序遍历森林中第一棵树的根结点的子树森林;
- 访问第一棵树的根结点;
- 后序遍历除了第一棵树之后剩余的树构成的森林。
上图森林的先序遍历序列为:ABCDEFGHI
后序遍历序列为:BCDAFEHIG
当森林转换为二叉树时,其第一棵树的子树森林转换为左子树,剩余树的森林转换为右子树,可知森林的先序遍历和后序遍历分别对应着相应二叉树的先序遍历和中序遍历。
五、应用
5.1、二叉排序树
二叉排序树,又称二叉查找树(BST, Binary Srarch Tree),或是一棵空树,又或是具有下列特性的二叉树:
- 若左子树非空,则左子树上所有结点的值均小于根结点的值;
- 若右子树非空,则右子树上所有结点的值均大于根结点的值;
- 左、右子树也分别是一棵二叉排序树;
- 左子树结点值<根结点值<右子树结点值;
- 进行中序遍历(LNR),可以获得一个递增的有序序列。
5.1.1、二叉排序树的常见操作
(1)查找
- 若树非空,目标值与根结点进行比较:
若相等,则查找成功,就为根结点;
若小于根结点,则在左子树上查找;
若大于根结点,则在右子树上查找。 - 查找成功,返回结点指针,失败则返回NULL。
typedef struct BSTnode{
int key;
struct BSTnode *lchild, *rchild;
}BSTnode, *BSTree;
//在二叉排序树中查找值为key的结点(非递归)
//最坏时间复杂度:O(1)
BSTnode *BSTSearch(BSTree T, int value){
if(T==NULL)
return NULL;
if(value == T->key)
return T;
else if(value<T->key)
return BSTSearch(T->lchild, value);
else return BSTSearch(T->rchild, value);
}
在查找运算中,需要比对关键字的次数被称为查找长度,反映了查找操作时间复杂度。
(2)插入
- 若原二叉排序树为空,则直接插入到结点,否则:
- 若关键字k大于根结点,则插入到右子树;
- 若关键字小于根结点,则插入到左子树;
typedef struct BSTIn(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 BSTIn(T->lchild, k);
else return BSTIn(T->rchild, k);
}
(3)构造
//按照str[]中关键字序列建立二叉排序树
void Creat_BST(BSTree &T, int str[], int n){
T==NULL;//初始为空树
int i = 0;
while(i<n){
BSTIn(T, str[i]);//依次将数组中的每个关键字插入到树中
i++;
}
}
(4)删除
先找到目标点:
- 若被删除的结点z是叶结点则直接删除,不会破坏二叉排序树的性质;
- 若结点z只有一棵左子树或者右子树,则让z的子树成为z父结点的子树,替代z的位置;
- 若结点z有左、右子树,则令z的直接后继(或直接前驱)替代z,然后从二叉排序树中删除这个直接后继(或直接前驱),将情况转换位第一种或第二种情况。
/*从二叉排序树中删除结点p,并重接它的左或右子树。*/
bool Delete(BiTree *p){
BiTree q, s;
if(p->rchild == NULL){
//右子树为空则只需重接它的左子树
q = *p;
*p = (*p)->lchild;
free(q);
}else if((*p)->lchild == NULL){
//左子树为空则只需重接它的右子树
q = *p;
*p = (*p)->rchild;
free(q);
}else{
//左右子树均不空
q = *p;
s = (*p)->lchild; //先转左
while(s->rchild){//然后向右到尽头,找待删结点的前驱
q = s;
s = s->rchild;
}
//此时s指向被删结点的直接前驱,p指向s的父母节点
p->data = s->data; //被删除结点的值替换成它的直接前驱的值
if(q != *p){
q->rchild = s->lchild; //重接q的右子树
}else{
q->lchild = s->lchild; //重接q的左子树
}
pree(s);
}
return TRUE;
}
/*
若二叉排序树T中存在关键字等于key的数据元素时,则删除该数据元素结点,
并返回TRUE;否则返回FALSE
*/
bool DeleteBST(BiTree *T, int key){
if(!*T){
return FALSE;
}else{
if(key == (*T)->data){
//找到关键字等于key的数据元素
return Delete(T);
}else if(key < (*T) -> data){
return DeleteBST((*T) -> lchild, key);
}else{
return DeleteBST((*T) -> rchild, key);
}
}
}
5.2、平衡二叉树(Balanced Binary Tree)
平衡二叉树,简称平衡树(AVL),其上任一结点的左子树和右子树的高度差不超过1。
结点的平衡因子(BF, Balance Factor) = 左子树高 - 右子树高。
//平衡二叉树结点
typedef struct AVLnode{
int key;//数据域
int balance;//平衡因子
struct AVLnode *lchild, *rchild;
}AVLnode, *AVLTree;
5.2.1、平衡二叉树的插入
二叉排序树保证平衡的基本思想如下:每当在二叉排序树中插入(或删除)一个结点时,首先检查其插入路径上的结点是否因为此次操作而导致了不平衡。若导致了不平衡,则先找到插入路径上离插入结点最近的平衡因子的绝对值大于1的结点A,再对以A为根的子树,在保持二叉排序树特性的前提下,调整各结点的位置关系,使之重新达到平衡。
注意: 每次调整的对象都是最小不平衡子树,即以插入路径上离插入结点最近的平衡因子的绝对值大于1的结点作为根的子树。下图中虚线框内为最小不平衡子树:
(1)LL
LL平衡旋转(右单旋转)。由于在结点A的左孩子(L)的左子树(L)上插入了新结点,A的平衡因子由1变成了2,导致以A为根的子树失去平衡,需要一次向右的旋转操作。将A的左孩子B向右上旋转代替A成为根结点,将A结点向右下旋转成为B的右子树的根结点,而B的原右子树成为A的左子树。图中,结点旁的数值代表结点的平衡因子,而用方块代表相应结点的子树,下方数值代表该子树的高度。
(2)RR
RR平衡旋转(左单旋转)。由于在结点A的右孩子(R)的右子树(R)上插入了新结点,A的平衡因子由-1变成了-2,导致以A为根的子树失去平衡,需要一次向左的旋转操作。将A的右孩子B向左上旋转代替A成为新的根结点,将A结点向左下旋转成为结点B的左孩子的根结点,而B的原左子树成为A的右子树。
(3)LR
LR平衡旋转(先左后右双旋转)。由于在A的左孩子(L)的右子树®上插入新结点,A的平衡因子由1增至2,导致以A为根的子树失去平衡,需要进行两次旋转操作,先左旋转后右旋转。先将A结点的左孩子B的右子树的根结点C向左上旋转提升到B结点的位置(即进行一次RR平衡旋转(左单旋转)),然后再把该C结点向右上旋转提升到A结点的位置(即进行一次LL平衡旋转(右单旋转))。
(4)RL
RL平衡旋转(先右后左双旋转)。由于在A的右孩子®的左子树(L)上插入新结点,A的平衡因子由-1减至-2,导致以A为根的子树失去平衡,需要进行两次旋转操作,先右旋转后左旋转。先将A结点的右孩子B的左子树的根结点C向右上旋转提升到B结点的位置(即进行一次LL平衡旋转(右单旋转)),然后再把该C结点向左上旋转提升到A结点的位置(即进行一次RR平衡旋转(左单旋转))。
注意: LR和RL旋转时,新结点究竟是插入C的左子树还是插入C的右子树不影响旋转过程,而上图中是以插入C的左子树中为例。
示例分析:
5.3、哈夫曼树
5.3.1、哈夫曼树的概念
在许多应用中,树中结点常常被赋予一个表示某种意义的数值,称为该结点的权。从树的根结点到任意结点的路径长度(经过的边数)与该结点上权值的乘积,称为该结点的带权路径长度。树中所有叶结点的带权路径长度之和称为该树的带权路径长度,记为:
式中,wi是第i个叶结点所带的权值,li是该叶结点到根结点的路径长度。
在含有n个带权叶结点的二叉树中,其中带权路径长度(WPL)最小的二叉树成为哈夫曼树,也称最优二叉树。例如,下图的3棵、、二叉树都有4个带权叶结点,分别带权7、5、2、4,它们的带权路径长度分别为:
a.72+52+22+42=36
b.42+73+53+21=46
c.71+52+23+43=35
其中(a)的带权路径长度最小,可以验证,它为哈夫曼树。
5.3.2、哈夫曼树的构造
构造步骤:
- 先把有权值的叶子结点按照从大到(逆序也可)的顺序排列成一个有序序列;
- 取最后两个最小权值的结点作为一个新结点的两个子结点,注意相对较小的为左孩子;
- 用第2步构造的新结点替换它的两个子结点,新结点的权值为两个子结点权值之和,插入到有序序列中,注意排序;
- 重复步骤2和步骤3,直到根结点出现。
5.3.3、哈夫曼编码
哈夫曼编码是一种被广泛应用而且非常有效的数据压缩编码。
假设六个字母的频率为A 27,B 8,C 15,D 15,E 30,F 5,合起来正好是
100%。那就意味着,我们完全可以重新按照赫夫曼树来规划它们。
下图左图为构造赫夫曼树的过程的权值显示。右图为将权值左分支改为0,右分支改为1后的赫夫曼树。
这棵哈夫曼树的WPL为:
WPL=2*(15+27+30)+152+54+8*4=241
此时,我们对这六个字母用其从树根到叶子所经过路径的0或1来编码,可以得到如下表所示的定义:
若没有一个编码是另一个编码的前缀,则称这样的编码为前缀编码。
注意:
0和1究竟是表示左子树还是右子树没有明确规定。左、右孩子结点的顺序是任意的,所以构造出的哈夫曼树并不唯一,但各哈夫曼树的带权路径长度WPL相同且为最优。此外,如有若干权值相同的结点,则构造出的哈夫曼树更可能不同,但WPL必然相同且是最优的。
每个初始结点最终都成为叶结点,且权值越小的结点到根结点的路径长度越大;
哈夫曼树的结点总数为2n-1;
哈夫曼树中不存在度为1的结点;