分节目录
数据结构(完结)
数据结构Part1 绪论与线性表
数据结构Part2 栈和队列
数据结构Part3 串
数据结构Part4 树与二叉树
数据结构Part5 图
数据结构Part6 查找
数据结构Part7 排序
第五章 数与二叉树
1.树
1.1 树的概念
概念:根节点,前驱,后继,子树,空树,路径与路径长度
节点的度:孩子节点的个数;
树的度:各节点度的最大值;
深度:从上往下数,默认从1开始;
节点的高度:从下往上数;
树得高度:树中根节点的最大深度;
描述节点关系:父节点、子节点、兄弟节点,
有序树和无序树:从左至有是有次序的,不可以交换。
森林:由m(m>=0)颗互不相交的树的集合。
定义:树是n(n>=0)个节点的有限集合,n=0时,称为空树。在任意一颗非空树中满足以下特点,
i.有且只有一个特定的节点称为根节点。
ii.当n>1时,其余节点可分为m(m>0)个互不相交的有限集合T1T2······Tm,每个集合的本身又是一颗树,并且称之为根节点的子树。
iii.根节点没有前驱节点,除根节点外的所有节点有且只有一个前驱节点。
iv.书中所有节点都有0个或多个后继节点
1.2 树的性质
考点1:节点数 = 总度数+1,(子节点+根节点)
考点2:度为m的树和m叉树的区别。
度为m的树 | m叉树 |
---|---|
任意节点的度<=m | 任意节点的度<=m |
至少有一个节点的度 = m | 允许所有节点的度都 < m |
一定是非空树,至少有m+1个节点 | 可以是空树 |
考点3:度为m的树第i层至多有mi-1个节点(i>1)
考点4:高度为h的m叉树至多有(m^h-1)/(m-1)个节点。(等差数列求和)
考点5:高度为h的m叉树至少有h个节点。高度为h,度为m的树至少有h+m-1个节点。
考点6:有n个节点的m叉树的最小高度为⌈logm(n(m-1)+1)⌉,即所有节点都有尽可能多的(m个)孩子 []为向下取整。
2.二叉树(重点)
2.1 二叉树的概念
二叉树是n (n>=0)个节点的有限有序集合:
i.或者为空二叉树,即n = 0。
ii. 或者由一个根节点和两个互不相交的被称为根的左子树和右子树组成。左子树和右子树又分别是一棵二叉树。
2.2 特殊的二叉树
1)满二叉树:一颗高为h,且含有2h-1个节点的二叉树
特点:
i.只有最后一层有叶子节点;
ii.不存在度为1得节点;
iii.按层序从1开始变好,节点i的左孩子为2i,有孩子为21+1,节点i的父节点为[i/2]。
2)完全二叉树:一颗高为h,且含有n个节点的二叉树当且仅当其每个节点都与高度为h的满二叉树中编号为1~n的节点一一对应时,称为满二叉树。
特点:
i.只有最后两层有叶子节点;
ii.最多只有一个度为1的节点;
iii.按层序从1开始变好,节点i的左孩子为2i,有孩子为21+1,节点i的父节点为[i/2];
iv. i<=[n/2]为分支节点,i>[n/2]为叶子节点。
3)二叉排序树:左子树上所有节点的关键字均小于根节点的关键字,右子树上所有节点的关键字均大于根节点的关键字,且左子树和右子树又各是一棵二叉排序树。
4)平衡二叉树:树上任一及诶但的左子树和右子树的深度之差不超过1。
2.3 树的性质
考点1:设非空二叉树中度为0、1和2的节点个数分别为n0、n1和n2,则n0= n2+ 1。叶子节点比二分支节点多一个
n = n0 + n1 + n2 = n1+ 2n2 +1 -> n0= n2+ 1
考点2:二叉树的第i层最多有2i-1个节点,m叉树的第i层最多有2m-1个节点。
考点3:高度为h的m叉树至多有2h-1个节点。
考点4:具有n个(n >0)节点的完全二叉树的高度h为 [log2n」+ 1或 [log2(n+1)」+ 1。
考点5:对于完全二叉树,可以由的节点数n = 2k推出度为0、1和2的节点个数为n0 = k、n1 = 0 或 1 和n2 = k-1。
2.4 二叉树的存储结构
2.4.1 顺序存储
定义一个长度为MaxSize的数组t,按照从上至下,从左至右的顺序依次存储完全二叉树中得各个节点。
define MaxSize 180
struct TreeNode{
ElemType value; //节点中的数据元素
bool isEmpty; //节点是否为空
};
TreeNode t[MaxSize];
for(int i=0; i<MaxSize; i++)
t[i].isEmpty=true; //初始化标记所有节点为空
若从t[1]开始储存,这样可以使数组下标与节点在二叉树中的序号一致,则有以下几点
i所在的层次 | [log2n] + 1 或 [log2(n+1)] + 1 |
---|---|
i 的父节点 | [i/2] |
i 的左孩子 | 2i |
i 的右孩子 | 2i+1 |
当存储一个普通树的话,可以使用节点将其补充为一个完全二叉树。但是这样会造成大量节点空间被浪费。
2.4.2 链式存储
struct ElemType{
string name; //节点中的数据元素
int age;
};
typedef struct BiTNode{
ElemType data; //数据域
struct BiTNode *lchild, *rchild; //左、右孩子指针
//若需要频繁的查找父节点,可添加一个指针,也称三叉链表
struct BiTNode *parent;
}BiTNode, *BiTree;
BiTree root = NULL; //定义了一个空树
//创建根节点并赋值,数据域为{"lc", 18},左右子节点为空
root = new BiTree[1]{{{"lc", 18}, NULL, NULL, NULL}};
cout<<root->data.name<<":"<<root->data.age<<endl;
2.5 二叉树的遍历(重点)
2.5.1 递归遍历
考虑到树的这种递归定义的特性(根节点,左子树,右子树),显然可以使用递归的方法进行遍历,根据根节点被访问的次序分为先序遍历,中序遍历,后序遍历三种
先序遍历:根左右(NLR)
空间复杂度O(h),第一次路过这个节点的时候就要访问这个节点
void ProOrder(BiTree T){
if(T!=NULL){
visit(T);
ProOrder(T->lchild);
ProOrder(T->rchild);
}
}
中序遍历:左根右(LNR)
第二次路过这个节点的时候就要访问这个节点
void InOrder(BiTree T){
if(T!=NULL){
ProOrder(T->lchild);
visit(T);
ProOrder(T->rchild);
}
}
后序遍历:左右根(LRN)
第三次路过这个节点的时候就要访问这个节点
void PostOrder(BiTree T){
if(T!=NULL){
ProOrder(T->lchild);
ProOrder(T->rchild);
visit(T);
}
}
递归的方法求树的深度
int treeDepth(BiTree T){
if(T == NULL){
return 0;
} else {
int l = treeDepth(T->lchild)
int l = treeDepth(T->lchild)
return l > r ? l+1 :r+1;
}
}
2.5.2 层序遍历
算法思想:
i:初始化一个辅助队列,一般使用链队列,并让根节点入队;
ii:若队列非空,则队头节点处队,访问该节点,并将其左,右子节点依次入队;
iii:重复ii,直到队列为空。
typedef struct LinkNode{
BiTNode *data; //保存指向节点的指针,节省空间
struct LinkNode *Next;
}LinkNode; //链式队列节点
typedef struct{
LinkNode *front, *rear; //队头,队尾节点
}LinkQueue;
//层序遍历
void Level0rder(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); //右孩子入队
}
}
2.5.3 由遍历序列构建二叉树
中序遍历的根节点在左右子树的中间,可用于划分左右子树
1)前序+中序
前序:根节点 左子树的前序遍历序列 右子树的前序遍历序列
中序:左子树的中序遍历序列 根节点 右子树的前序遍历序列
同色的序列长度相等,递归的寻找根节点
2)后序+中序
后序:左子树的前序遍历序列 右子树的前序遍历序列 根节点
中序:左子树的中序遍历序列 根节点 右子树的前序遍历序列
同色的序列长度相等,递归的寻找根节点
3)层序+中序
层序:根节点 左子树的根 右子树的根节点 ······
中序:左子树的中序遍历序列 根节点 右子树的前序遍历序列
先找根节点,然后通过中序遍历划分左右子树,在左右子树中重复这一步骤。
3.线索二叉树(重点)
3.1 基本概念
由n个节点组成的的二叉树,共有n+1个空链域。
3.2 三种线索二叉树
中序线索二叉树
//二叉树的节点(链式存储)
typedef struct BiTNode{
ElemType data;
struct BiTNode *lchild, *rchild;
int ltag,rtag; //左、右线索标志,ltag=1时表示lchild为线索指针指向前驱,rtag同理
}ThreadBNode, *ThreadTree;
先序线索二叉树
后序线索二叉树
手算画出线索二叉树
i.确定线索二叉树类型——中序、先序、or后序;
ii.按照对应遍历规则,确定各个节点的访问顺序,并写上编号;
iii.将n+1个空链域连上前驱、后继。
3.2 二叉树线索化
3.2.1 线索化的方法
寻找一个节点的前驱
土办法:设置两个指针a,b,令b指向a所指节点的后继节点,两个指针同步遍历。当b指向节点p时,a指向的节点即为p的前驱节点。
BiTNode *p; //p指向目标节点
BiTNode * pre = NULL; //指向当前访问节点的前驱
BiTNode * final = NULL; //用于记录最终结果
void visit(BiTNode *q){
if(q==p) final = pre;
else pre == q;
}
//由中序遍历修改的找中序前驱节点
void FindInOrderPreP(BiTree T){
if(T!=NULL){
ProOrder(T->lchild);
visit(T);
ProOrder(T->rchild);
}
}
*二叉树线索化的方法,重点在手算而不是代码。
中序线索化遍历
//全局变量pre,指向当前访问节点的前驱
ThreadNode *pre = NULL;
void visit(){
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 T){
if(T != NULL){
InThread(T->lchild); //中序遍历左子树
visit(T); //访问根节点
InThread(T->rchild); //中序遍历右子树
}
}
//中序线索化二叉树T
void CreateInThread(ThreadTree T){
pre=NULL; // pre初始为NULL
if(T !=NULL){ //非空二叉树才能线索化
InThread(T); //中序线索化二叉树
if (pre->rchild==NULL) //处理遍历的最后一个节点
pre->rtag=1;
}
}
先序线索化遍历
//先序遍历二叉树,一边遍历一遍线索化
void PreThread(ThreadTree T, ThreadTree &pre){
if(T!=NULL){
if(T->lchild == NULL){ //左子树为空,建立前驱线索
T->lchild = pre;
T->ltag = 1;
}
if(pre != NULL && pre->rchild == NULL){
pre-rchild = T; //建立前驱节点的后继线索
pre->rtag = 1;
}
pre = T; //标记当前节点成为刚刚访问过的节点
if(T->ltag == 0)
PreThread(T->lchild, pre); //递归,线索化右子树
PreThread(T->rchild, pre); //递归,线索化左子树
}//if(T!=NULL)
}
//先序线索化二叉树T
void CreatePreThread(ThreadTree T){
ThreadTree pre=NULL; //pre初始为NULL
if(T !=NULL){ //非空二叉树才能线索化
InThread(T, pre); //中序线索化二叉树
if (pre->rchild==NULL) //处理遍历的最后一个节点
pre->rtag=1;
}
}
后序线索化遍历
//先序遍历二叉树,一边遍历一遍线索化
void PostThread(ThreadTree T, ThreadTree &pre){
if(T!=NULL){
PostThread(T->lchild, pre); //递归,线索化右子树
PostThread(T->rchild, pre); //递归,线索化左子树
if(T->lchild == NULL){ //左子树为空,建立前驱线索
T->lchild = pre;
T->ltag = 1;
}
if(pre != NULL && pre->rchild == NULL){
pre-rchild = T; //建立前驱节点的后继线索
pre->rtag = 1;
}
pre = T; //标记当前节点成为刚刚访问过的节点
}//if(T!=NULL)
}
//先序线索化二叉树T
void CreatePostThread(ThreadTree T){
ThreadTree pre=NULL; //pre初始为NULL
if(T !=NULL){ //非空二叉树才能线索化
InThread(T, pre); //中序线索化二叉树
if (pre->rchild==NULL) //处理遍历的最后一个节点
pre->rtag=1;
}
}
核心:
中序/先序/后序遍历算法的改造,当访问一个节点时,连接该节点与前驱节点的线索信息
用一个指针 pre记录当前访问节点的前驱节点
易错点:
最后一个节点的rchild, rtag 的处理
先序线索化中,注意处理原地循环的问题,当ltag==0时,才能对左子树先序线索化
3.2.2 线索二叉树找前驱/后继
指定节点为*p,后继为next,前驱为pre
1)中序线索化
后继:
i.若p->rtag == 1,则next = p->rchild;
ii.若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; //rtag==1直接返回后继线索
}
前驱:
i.若p->ltag == 1,则pre = p->lchild;
ii.若p->ltag == 0,则next为以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; //rtag==1直接返回后继线索
}
//对中序线索二叉树进行逆向中序遍历
void RevInorder(ThreadNode *T){
ThreadNode *p=Lastnode(T);
for(p; p!=NULL; p=Prenode(p))
visit(p);
}
2)先序线索化
后继:
i.若p->rtag == 1,则next = p->rchild;
ii.若p->rtag == 0(则p必有右孩子),若有左孩子,则next = p->lchild,若没有左孩子,则next = p->rchild;
rtag\ltag | ltag = 0 | ltag = 1 |
---|---|---|
rtag = 0 | p->lchild | p->rchild |
rtag = 1 | p->rchild | p->rchild |
//在中序线索二叉树中找到节点p的后继节点
ThreadNode *Nextnode(ThreadNode *p){
//右节点指针不指向后继,且有左孩子
if(p->rtag==0 && p->ltag==0) return p->lchild;
else return p->rchild;
}
前驱:
i.若p->ltag == 1,则pre = p->lchild;
ii.若p->ltag == 0(则p必有左孩子),p的前驱不在p的子树内,可以采用双指针从头遍历的方式:
a.若能找到p的父节点pf,且p是左孩子,则pre = pf;
b.若能找到p的父节点pf,且p的左兄弟为空,p是右孩子,则pre = pf;
c.若能找到p的父节点pf,且p的左兄弟非空,p是右孩子,则从pf开始遍历。
若有右节点就向右,若只有左节点就向左,到叶子节点停止,此时的叶子节点即为pre。
d.若p是根节点,则p没有前驱节点。
//找到以Pf为根的子树中,最后一个被遍历的节点
ThreadNode *Lastnode(ThreadNode *pf){
while(pf->rtag==0 || pf->ltag==0){
if(pf->ratg==1) pf=pf->lchild;
else pf = pf->rchild;
}
return pf;
}
//在先序线索二叉树中找到节点p的后继节点
ThreadNode *Prenode(ThreadNode *p, ThreadNode *pf){
if(p == root) return NULL; //情况d
if(pf->ltag == 1 || pf->lchild == p) //情况a,b
return pf;
if(pf->ltag == 0 && pf->rchild == p) //情况c
return Lastnode(pf);
}
3)后序线索化
后继:
i.若p->rtag == 1,则pre = p->rchild;
ii.若p->rtag == 0(则p必有左孩子),p的后继不在p的子树内,可以采用双指针从头遍历的方式:
a.若能找到p的父节点pf,且p是右孩子,则next = pf;
b.若能找到p的父节点pf,且p的右兄弟为空,p是左孩子,则next= pf;
c.若能找到p的父节点pf,且p的右兄弟非空,p是左孩子,则从pf开始遍历。
若有左节点就向左,若只有右节点就向右,到叶子节点停止,此时的叶子节点即为next。
d.若p是根节点,则p没有后继节点。
//找到以P为根的子树中,第一个被后序遍历的节点
ThreadNode *Firstnode(ThreadNode *pf){
while(pf->rtag==0 || pf->ltag==0){
if(pf->latg==1) pf=pf->rchild;
else pf = pf->lchild;
}
return pf;
}
//在先序线索二叉树中找到节点p的后继节点
ThreadNode *Nextnode(ThreadNode *p, ThreadNode *pf){
if(p == root) return NULL; //情况d
if(pf->rtag == 1 || pf->rchild == p) //情况a,b
return pf;
if(pf->rtag == 0 && pf->lchild == p) //情况c
return Firstnode(pf);
}
前驱:
i.若p->ltag == 1,则pre = p->lchild;
ii.若p->ltag == 0(则p必有左孩子),若有右孩子,则pre = p->rchild,若没有右孩子,则pre = p->lchild;
rtag\ltag | ltag = 0 | ltag = 1 |
---|---|---|
rtag = 0 | p->rchild | p->lchild |
rtag = 1 | p->lchild | p->lchild |
//在后序线索二叉树中找到节点p的前驱节点
ThreadNode *Prenode(ThreadNode *p){
//有右孩子,且左孩子节点指针不指向前驱
if(p->rtag==0 && p->ltag==0) return p->rchild;
else return p->rchild;
}
4.一般树与森林
4.1 一般树的存储结构
概念回顾:空树、非空树。对任意非空树,有一个根,每个节点只有一个”双亲“节点,递归定义略。
4.1.1 双亲表示法
顺序存储每个节点中保存指向双亲的“指针”(静态链表)。根节点的指针域记为-1,其余节点的指针域指向父节点的位置。
#define MAX_TREE_SIZE 100 //树中最多节点数
typedef struct{ //树的节点定义
ElemType data; //数据元素
int parent; //双亲位置域
}PTNode;
typedef struct{ //树的类型定义
PTNode nodes [MAX_TREE_SIZE]; //双亲表示
int n; //节点数
}PTree;
增加节点:在数组的结尾添加一个节点的数据元素与双亲位置域;
删除节点:将要删除节点的指针域记为-2,并递归的查找其子树内的节点将其指针域全部置为-2;然后用尾部的有效节点来覆盖当前节点。
??是删除单个节点还是产出这个节点后的所有节点??
查询:递归查询,不方便。
4.1.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;
4.1.3 孩子兄弟表示法
二叉链表,按层存储,可以实现普通树与二叉树的转换。
//树的存储—孩子兄弟表示法
typedef struct CSNode{
ElemType data; //数据域
struct cSNode *firstchild, *nextsibling; //第一个孩子和右兄弟指针
}CSNode,*CSTree;
4.1.4 森林和二叉树的转换
将森林中的每棵树都用孩子兄弟表示法表示后,将树的根节点用右兄弟指针连接。
4.2 树和森林的遍历
4.2.1 树的遍历
1)先根遍历。若树非空,先访问根节点,再依次对每棵子树进行先根遍历。树的先根遍历序列与这棵树相应二叉树的先序序列相同。也称为树的深度优先遍历。
//树的先根遍历
void PreOrder(TreeNode *R){
if (R!=NULL){
visit(R); //访问根节点
while( R还有下一个子树T )
Pre0rder(T); //先根遍历下一棵子树
}
}
2)后根遍历。若树非空,先依次对每棵子树进行后根遍历,再访问根节点。树的后根遍历序列与这棵树相应二叉树的中序序列相同。也称为树的深度优先遍历。
//树的先根遍历
void PreOrder(TreeNode *R){
if (R!=NULL){
while( R还有下一个子树T )
Pre0rder(T); //先根遍历下一棵子树
visit(R); //访问根节点
}
}
3)层次遍历(用队列实现)
i.若树非空,则根节点入队
ii.若队列非空,队头元素出队并访问,同时将该元素的孩子依次入队
iii.重复ii直到队列为空
也称为树的广度优先遍历。
4.2.2 森林的遍历
1)先序遍历
i.若森林为非空,则按如下规则进行遍历:访问森林中第一棵树的根节点。
ii.先序遍历第一棵树中根节点的子树森林。
iii.先序遍历除去第一棵树之后剩余的树构成的森林。
*等同于依次对各个子树进行先根遍历。(上面递归过程不用记)
2)中序遍历
若森林为非空,则按如下规则进行遍历:
中序遍历森林中第一棵树的根节点的子树森林。访问第一棵树的根节点。
中序遍历除去第一棵树之后剩余的树构成的森林。
*等同于依次对各个子树进行后根遍历。(上面递归过程不用记)
树 | 森林 | 二叉树 |
---|---|---|
先根遍历 | 先序遍历 | 先序遍历 |
后根遍历 | 中序遍历 | 中序遍历 |
注意:对森林和普通树的算法题,先用孩子兄弟表示法将其转化为二叉树。
5.树与二叉树的应用
5.1 二叉排序树(BST)
二叉排序树,又称二叉查找树(BST,Binary Search Tree)一棵二叉树或者是空二叉树,或者是具有如下性质的二叉树:(左<根<右)
i.左子树上所有节点的关键字均小于根节点的关键字;
ii.右子树上所有节点的关键字均大于根节点的关键字;
iii.左子树和右子树又各是一棵二叉排序树。
作用:二叉排序树可用于元素的有序组织、查找特定值
//二叉排序树的节点
typedef struct BSTNode{
int key;
struct BSTNode *lchild, *rchild;
}BSTNode, *BSTree;
1)二叉排序树的查找
若树非空,目标值与根节点的值比较:若相等,则查找成功;若小于根节点,则在左子树上查找,否则在右子树上查找。
查找成功,返回节点指针;查找失败,返回NULL。
//在二叉排序树中查找值为num的节点
BSTNode *BST_Search(BSTree T,int num){
while(T != NULL && num != T->key) //若树空或等于根节点值,则结束循环
if(num < T->key) T=T->lchild; //小于,则在左子树上查找
else T=T->rchild; //大于,则在右子树上查找
}
return T;
}
//在二叉排序树中查找值为key 的节点(递归实现)
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); //在右子树中找
}
//递归实现的最坏空间复杂度为O(h)
2)二叉排序树的插入
若原二叉排序树为空,则直接插入节点;否则,若关键字k小于根节点值,则插入到左子树,若关键字k大于根节点值,则插入到右子树
//在二叉排序树插入关键字为k的新节点
int BST_Insert(BSTree &T, int k){
while(T != NULL){
if(T->key == k) return -1; //树中存在相同关键字的节点,插入失败
if(k < T->key) T=T->lchild; //小于,则在左子树上查找
else T=T->rchild; //大于,则在右子树上查找
}
T=(BSTree)malloc(sizeof(BSTNode));
T->key=k;
T->lchild=T->rchild=NULL;
return 1; //返回1,插入成功
}
//在二叉排序树插入关键字为k的新节点(递归实现)
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; //返回1,插入成功
}
else if( k==T->key) //树中存在相同关键字的节点,插入失败
return -1;
else if(k<T->key) //插入到T的左子树
return BST_Insert(T->lchild,k );
else //插入到T的右子树
return BST_Insert(T->rchild,k);
}
//递归实现的最坏空间复杂度为O(h)
3)二叉排序树的构造
//按照str[]中的关键字序列建立二叉排序树
void Creat_BST(BSTree &T,int str[] ,int n){
T=NULL; //初始时T为空树
int i=0;
while(i<n){ //依次将每个关键字插入到二叉排序树中
BST_Insert(T,str[i] );
i++;
}
}
4)二叉排序树的删除
i.若被删除节点z是叶节点,则直接删除,不会破坏二叉排序树的性质。
ii.若节点z只有一棵左子树或右子树,则让z的子树成为z父节点的子树,替代z的位置。
iii.若节点z有左、右两棵子树,则令z的直接后继(或直接前驱)替代z,然后从二叉排序树中删去这个直接后继(或直接前驱),这样就转换成了第一或第二种情况。
进行中序遍历,可以得到一个递增的有序序列。
查找长度:在查找运算中,需要对比长键字的次数称为查找长度,反映了查找操作时间复杂度。
查找成功的平均查找长度ASL (Average Search Length)
ASL =∑ (层数*本层节点数)/总节点数
查找失败 的平均查找长度ASL (Average Search Length)
ASL =∑ (节点所在的层数*节点的度数)/总节点数
5.2 平衡二叉树(重点)
平衡二叉树(Balanced Binary Tree),简称平衡树(AVL树)――树上任一节点的左子树和右子树的高度之差不超过1。节点的平衡因子=1, 0, -1
节点的平衡因子=左子树高-右子树高。
当插入节点导致及诶但不平衡时,只需调整最小不平衡子树。
A节点为最小非平衡二叉树的根节点。
LL:在A的左孩子的左子树中插入导致不平衡()
LL平衡旋转(右单旋转):
由于在节点A的左孩子(L)的左子树(L)上插入了新节点,A的平衡因子由1增至2,导致以A为根的子树失去平衡,需要一次向右的旋转操作。
将A的左孩子B向右上旋转代替A成为根节点,将A节点向右下旋转成为B的右子树的根节点,而B的原右子树则作为A节点的左子树。
//实现f向右下旋转,p向右上旋转
//其中f是p的父节点,p为左孩子,gf为f的父节点
f->lchild = p->rchild;
p->rchild = f;
gf->lchild/rchild = p;
RR:在A的右孩子的右子树中插入导致不平衡
RR平衡旋转(左单旋转):
由于在节点A的右孩子(R)的右子树(R)上插入了新节点,A的平衡因子由-1减至-2,导致以A为根的子树失去平衡,需要一次向左的旋转操作。
将A的右孩子B向左上旋转代替A成为根节点,将A节点向左下旋转成为B的左子树的根节点,而B的原左子树则作为A节点的右子树。
//实现f向左下旋转,p向左上旋转
//其中f是p的父节点,p为右孩子,gf为f的父节点
f->rchild = p->lchild;
p->lchild = f;
gf->lchild/rchild = p;
LR:在A的左孩子的右子树中插入导致不平衡
先左旋C,再右旋C。
LR平衡旋转(先左后右双旋转):
由于在A的左孩子(L)的右子树(R)上插入新节点,A的平衡因子由1增至2,导致以A为根的子树失去平衡,需要进行两次旋转操作,先左旋转后右旋转。
先将A节点的左孩子B的右子树的根节点C向左上旋转提升到B节点的位置,然后再把该c节点向右上旋转提升到A节点的位置。
RL:在A的右孩子的左子树中插入导致不平衡
RL平衡旋转(先右后左双旋转):
由于在A的右孩子(R)的左子树(L)上插入新节点,A的平衡因子由-1减至-2,导致以A为根的子树失去平衡,需要进行两次旋转操作,先右旋转后左旋转。
先将A节点的右孩子B的左子树的根节点C向右上旋转提升到B节点的位置,然后再把该C节点向左上旋转提升到A节点的位置。
假设以nk表示深度为k的平衡树中还有的最少节点树。
则有:n0 = 0, n1 = 1, n2 = 2, 且nk = nk-1 + nk-2 + 1;
可以证明含有n个节点的平衡二叉树的最大深度为o(logzn),平衡二叉树的平均查找长度为O(log2n)
5.3 哈夫曼树和哈夫曼编码(重点)
节点的权:有某种现实含义的数值(如表示节点的重要性等);
节点的带权路径长度:从树的根到该节点的路径长度(经过的边数)与该节点上权值的乘积;
树的带权路径长度:树中所有叶节点的带权路径长度之和 (WPL, Weighted Path Length)。
哈夫曼树:在含有n个带权叶节点的二叉树中,其中带权路径长度(WPL)最小的二叉树称为哈夫曼树,也称最优二叉树。
给定n个权值分别为w, w2…, w,的结点,构造哈夫曼树的算法描述如下:
i.将这n个结点分别作为n棵仅含一个结点的二叉树,构成森林F。
ii.构造一个新结点,从F中选取两棵根结点权值最小的树作为新结点的左、右子树,并且将新结点的权值置为左、右子树上根结点的权值之和。
iii.从F中删除刚才选出的两棵树,同时将新得到的树加入F中。
iv.重复步骤ii和iii直至F中只剩下一棵树为止。
性质:
i.每个初始结点最终都成为叶结点,且权值越小的结点到根结点的路径长度越大;
ii.哈夫曼树的结点总数为2n -1;
iii.哈夫曼树中不存在度为1的结点;
iv.哈夫曼树并不唯一,但WPL必然相同且为最优。
固定长度编码: 每个字符用相等长度的二进制位表示
可变长度编码:允许对不同字符用不等长的二进制位表示
前缀编码:若没有一个编码是另一个编码的前缀,则称这样的编码为前缀编码,即所有字符都是叶子节点。
由哈夫曼树得到哈夫曼编码―-字符集中的每个字符作为一个叶子结点,各个字符出现的频度作为结点的权值,根据之前介绍的方法构造哈夫曼树。
5.4 并查集
并查集(英文:Disjoint-set data structure,直译为不交集数据结构)是一种简单的集合表示,主要用于解决一些元素分组的问题。它管理一系列不相交的集合,主要有三种操作:
i.初始化:Initial(S):将集合S中的每个元素都初始化为只有一个单元素的子集合,即一个元素为一组
ii.合并:Union(S, Root1, Root2):把集合S中的子集合Root2并入子集合Root1中。要求Root1与Root2互不相交,否则不能执行。一般情况下必定满足。
iii.查找:Find(S, x):查找集合S中单元素x所在的子集合,并返回该子集合的名字。
//简单的并查集可以由数组进行表示
#define maxSize 100
int S[maxSize] //集合元素数组(双亲指针数组)
void Initial(int S[]){
for(int i = 0; i < maxSize; i++){
S[i] = -1; //每个元素独自一组
}
}
int Find(int S[], int &x){
while(S[x] >= 0) //循环寻找x的根
x = S[x];
return x; //根的S[]小于0
}
void Union(int S[], int Root1, int Root2){
S[Root2] = Root1; 将Root2的根连接在Root1根下面
}