8.3 动态查找表
动态查找表的特点:表结构是在查找过程中动态生成的,即如果存在其关键字等于给定值的记录,则查找成功返回;否则插入给定值对应的记录。
8.3.1 动态查找表的类型定义
8.3.2二叉排序树和平衡二叉树
1. 二叉排序树
二叉排序树又称二叉查找树,他或是一棵空二叉树,或是具有如下性质的二叉树:
- 如果其左子树不为空,则左子树上所有结点的值均小于根结点的值;
- 如果其右子树不为空,则左子树上所有结点的值均大于根结点的值;
- 左右子树也都是二叉排序树。
中序遍历二叉排序树可以得到一个按关键字有序的序列。折半查找判定树就是一棵二叉排序树。
示例:
动态查找表的二叉链表存储结构如下:
//动态查找表的二叉链表存储结构
typedef struct BSTNode{
RedType data;//数据域
struct BSTNode *lchild,*rchild;//左、右孩子指针域
}BSTNode;
typedef BSTNode *BSTree;
二叉排序树的查找:
当二叉排序树是空树时,查找失败,返回空指针;当k等于根记录结点的关键字时,查找成功,返回该结点指针。
当k小于根记录结点的关键字时,在左子树中继续查找;否则在右子树中继续查找。
算法:
//二叉排序树上的查找
BSTNode SearchBST(BSTree T,ElemType k){
if(T==NULL) return NULL;//如果为空树则返回空
else if(k==T->data.key) return T;//如果域根结点关键字相等则返回根结点
else if(k<T->data.key) return SearchBST(T->lchild,k);//若小于根结点关键字则递归查找左子树
else return SearchBST(T->rchild,k);//若大于根结点关键字则递归查找右子树
}
二叉排序树的插入:
二叉排序树的结构通常不是一次生成的,而是在查找过程中,当树中不存在关键字等于给定值的记录结点时再进行插入。新插入的结点一定是一个新添加的叶子结点而是在查找不成功时查找路径上访问的最后一个结点的左孩子或右孩子。
当二叉树为空时,将p所指向的结点作为根结点插入;当p所指向结点的关键字等于根结点的关键字时,给出相应信息。
当p所指向结点关键字小于根结点关键字时,将其插入到根结点的左子树当中;当p所指向结点的关键字大于根结点关键字时,则将其插入到根结点的右子树中。
当找到插入位置后,在二叉排序树中插入结点操作只是修改相应指针,而不需要移动其他记录。
算法:
//二叉排序树上的插入
void InsertBST(BSTree &T,RedType r){
if(!(SearchBST(T,r.key))){
BSTNode *s=new BSTNode;
s->data=r;
s->lchild=NULL;
s->rchild=NULL;
Insert(T,s);//如果二叉排序树上不存在r则将r的值赋给结点s再插入
}
cout<<"已有关键字相同结点,不再插入!"<<endl;//若存在则返回相应信息
}
void Insert(BSTree &T,BSTNode *p){
if(T==NULL) p=T;//若为空树则将p作为根结点
else if(p->data.key==T->data.key){
cout<<"已有关键字相同结点,不再插入!"<<endl;
exit(1);
}//若等于根结点的值直接返回存在信息
else if(p->data.key<T->data.key)
Insert(T->lchild,p);//若小于根结点,则递归对左子树进行插入
else
Insert(T->rchild,p);//否则递归对右子树进行插入
}
二叉排序树的构造:
从空树出发,经过一系列查找插入后,可以生成一棵二叉排序树。设n个待插入记录存放在r[n]中,依次取出每个记录r[i],重复执行:创建一个数据域为r[i]的结点,令该结点左右指针域均为空。用插入算法将其插入到二叉排序树中。
因为中序遍历二叉排序树可以得到一个有序序列,那么将一个无序序列构建成二叉排序树的过程就是对其进行有序排序的过程。示例:
算法:
//二叉排序树的构造
void CreatBST(BSTree &T,RedType r[],int n){
for(int i=0;i<n;++i){
BSTNode s=new BSTNode;
s->data=r[i];
s->lchild=NULL;
s->rchild=NULL;//构建数据域为r[i]的结点s
InsertBST(T,s);//将s插入到二叉树T中
}
}
二叉排序树的删除:
进行删除操作的三种情况:
p为待删除结点:
- p结点为叶子,既没有左子树也没有右子树。
如果p结点为左孩子,只需要将p的双亲f左指针域设为空,p为右孩子时同理; - p结点只有左子树PL或右子树PR。
如果p是f的左孩子,则只需要将PL或PR直接置为其双亲的左子树,p为右孩子时同理; - p结点左右孩子PL,PR均非空。
首先,寻找p结点的中序后继q(大于p结点值的最小者),并在查找过程中用fq作为q结点的双亲进行跟踪;p结点的中序后继q一定是其右子树中最左下面的结点,即他无左子树。
然后,将删除p结点的操作转换为删除q结点的操作,即在释放q结点之前将其数据复制到p结点中,就相当于删除了p结点。
算法:
//二叉排序树的删除
void DeleteBST(BSTree &T,ElemType k){//找到值为k的结点,并记录其双亲
BSTNode *parent=NULL;//parent指向p的双亲,初始值为NULL
BSTNode *p=T;//从根开始查找关键字为k的待删除结点
while(p){
if(k==p->data.key) break;//找到后跳出while循环
parent=p;//parent指向p成为下一次循环p的双亲
if(k<p->data.key) p=p->lchild;
else p=p->rchild;//若k小于p的关键字则p指向p的左孩子否则指向p的右孩子
}
if(!p){
cout<<"关键字等于k的记录不存在!";//如果没有关键字为k的结点则报错
exit(1);
}
else Delete(T,p,parent);//如果有则进行删除操作
}
void Delete(BSTree &T,BSTNode *p,BSTNode *f){
if(!(p->lchild)&&!(p->rchild)){
if(f->lchild==p) f->lchild==NULL;
else f->rchild==NULL;
delete p;
}//如果p是叶子结点,直接删除p并置f的相应孩子结点指针域为空
else if(!(p->rchild)){
if(f->lchild==p) f->lchild=p->lchild;
else f->rchild=p->lchild;
delete p;
}/*如果p只有左孩子,则p若为f的左孩子让p的左孩子做f的左孩子,
若p为f的右孩子让p的左孩子做f的右孩子 */
else if(!(p->lchild)){
if(f->lchild==p) f->lchild=p->rchild;
else f->rchild=p->rchild;
delete p;
}/*如果p只有右孩子,则p若为f的左孩子让p的右孩子做f的左孩子,
若p为f的右孩子让p的右孩子做f的右孩子*/
else{//如果p既有左孩子又有右孩子
BSTNode *fq=p;
BSTNode *q=p->rchild;
while(q->lchild!=NULL){
fq=q;
q=q->lchild;
}//定位p的右子树最左下方结点为q(q没有左孩子),q的双亲为fq
p->data=q->data;//将p的关键字改为q的关键字
if(fq==p) fq->rchild=q->rchild;
//如果q的双亲结点是p则直接让q的右孩子成为1其双亲结点的右孩子
else fq->lchild=q->rchild;
//如果q的双亲不是p,就让q的右孩子变成其双亲的左孩子
delete q;
}
}
二叉排序树查找性能分析:
比较次数正好是给定值结点在二叉排序树中的层数,最少比较一次,最多不超过树的深度。
含有n个结点的二叉排序树不唯一,其形状取决于各个记录插入到二叉排序树的先后顺序。
平均时间性能:
最好的情况,二叉树的形态是均匀的(平衡),则有n个结点的二叉树深度为,平均查找长度与log2 n成正比,近似于折半查找。最坏的情况,二叉树成为了一棵斜树,其深度为n,则其平均查找长度和(n+1)/2成正比,和顺序查找相同。一般地,二叉排序树的查找性能在O(log2 n)和O(n)之间。因此为了更好的查找性能,需要构造一棵平衡的二叉树。
表的维护性能:
二叉排序树无需移动结点,只需修改指针即可。平均执行时间为O(log2 n)。而折半查找所使用的为顺序表,若进行插入删除操作,则时间代价为O(n)。
2.平衡二叉树(AVL树)
平衡二叉树又称AVL树,它或是一棵空树,或是具有如下特点的二叉树:
- 根结点的左子树和右子树深度最多差1;
- 根结点的左子树和右子树也是平衡二叉树。
通常,只要二叉树的深度为O(log2 n),就认为他是平衡的。
结点的平衡因子BF:该结点左子树深度减去其右子树深度。在平衡二叉树上所有节点的BF只能是1/0/-1。
最小不平衡子树:至在平衡二叉树的构造过程中,以距离插入结点最近的,且平衡因子绝对值大于1的结点为根的子树。
AVL树的存储结构如下:
//平衡二叉树的存储结构
typedef struct BSTNode{
RedType data;//数据域
int bf;//平衡因子
struct BSTNode *lchild,*rchild;//左、右孩子指针域
}BSTNode;
typedef BSTNode *BSTree;
平衡二叉树调整规律:
都是对最小不平衡子树的调整
- LL型平衡调整(单向右旋平衡处理)
B为A的左子树的根结点,将结点X插入到B的左子树BL上,导致结点A的平衡因子由1变2。
此时,将支撑点由A改成B,进行顺时针旋转。旋转后,结点A和B的右子树BR发生冲突,按照“旋转优先”的原则,将A结点作为B结点的右孩子,BR成为A结点的左子树。如图:
其中BL、BR、AL深度均为h。 - RR型平衡调整(单向左旋转平衡处理)
B为A的右子树的根结点,将结点X插入到B的右子树BR上,导致结点A的平衡因子由-1变-2。
此时,将支撑点由A改成B,进行逆时针旋转。旋转后,结点A和B的左子树BL发生冲突,按照“旋转优先”的原则,将A结点作为B结点的左孩子,BL成为A结点的右子树。如图:
其中BL、BR、AL深度均为h。 - LR型平衡调整(双向旋转(先左后右)平衡处理)
将结点X插入到根结点左孩子的右子树上,导致结点A的平衡因子由1变2。
第一次旋转:A不动,调整A的左子树。将支撑结点B调整到结点C处,进行逆时针旋转。旋转后,结点B和结点C的左子树CL发生冲突,按照“旋转优先”的原则,结点B作为结点C的左孩子,结点C的左子树作为结点B的右子树。
第二次旋转:调整最小不平衡子树,将支撑结点A调整到结点C处,进行顺时针旋转。旋转后,结点A和结点C的右子树CR发生冲突,按照“旋转优先”的原则,结点A作为结点C的右孩子,结点C的右子树作为结点A的左子树。
其中BL、AR的深度为h,CL、CR的深度为h-1。 - RL平衡调整(双向旋转(先右后左)平衡处理)
将结点X插入到根结点右孩子的左子树上,导致结点A的平衡因子由-1变-2。
第一次旋转:A不动,调整A的左子树。将支撑结点B调整到结点C处,进行顺时针旋转。旋转后,结点B和结点C的右子树CR发生冲突,按照“旋转优先”的原则,结点B作为结点C的右孩子,结点C的右子树作为结点B的左子树
第二次旋转:调整最小不平衡子树,将支撑结点A调整到结点C处,进行逆时针旋转。旋转后,结点A和结点C的左子树CL发生冲突,按照“旋转优先”的原则,结点A作为结点C的左孩子,结点C的左子树作为结点A的右子树。
其中BL、AR的深度为h,CL、CR的深度为h-1。
下面是一个将无序序列构造为平衡二叉树的示例:
平衡二叉树旋转的实现:
- LL型
代码://LL型 void LL_Rotate(BSTree &p){ BSTNode *lc=p->rchild;//lc为p的左孩子 p->lchild=lc->rchild;//让lc的右子树成为p的左子树 lc->rchild=p;//让p成为lc的右孩子 p=lc;//p重新指向根结点 }
- RR型
代码://RR型 void RR_Rotate(BSTree &p){ BSTNode *lc=p->rchild;//lc为p的右孩子 p->rchild=lc->lchild;//让lc的左子树变成p的右子树 lc->lchild=p;//让p变成lc的左孩子 p=lc;//p重新指向根结点 }
- LR型
代码:
LR和RL中的case EH那句始终没太弄明白,等我搞明白了在回来进行修改。//LR型 #define LH +1//左子树高 #define EH 0//等高 #define RH -1//右子树高 void LeftBalance(BSTree &T){ BSTNode *lc=T->lchild;//lc是T的左孩子 switch(lc->bf){ case LH://如果lc的左子树高那么就是LL型 T->bf=lc->bf=EH;//调整T和lc的平衡因子 LL_Rotate(T);//对T做LL型的旋转 break; case RH://如果lc是右子树高那么是LR型的 BSTNode *rd=lc->rchild;//rd是lc的右孩子 switch(rd->bf){//判定rd的平衡因子的值,以修改T及lc的平衡因子 case LH: T->bf=RH;lc->bf=EH;break; //如果rd的左子树高,则T结点在旋转后会变成右子树高,lc结点变为等高 case EH: T->bf=lc->bf=EH;break; //如果rd等高 case RH: T->bf=EH;lc->bf=LH;break; //如果rd的右子树高,则T结点在旋转后会变为等高,lc结点变为左子树高 } rd->bf=EH;//旋转后rd等高 RR_Rotate(T->lchild);//对T的左孩子做RR旋转 LL_Rotate(T);//对T做LL旋转 } }
平衡二叉树的插入:
- 如果T为空,则插入记录r的新节点作为T的根结点,树的深度加1;
- 如果r.key等于T的根结点的关键字,则不进行插入;
- 如果r.key小于T的根结点的关键字,且T的左子树中不存在关键字和r.key相等的结点,则插入记录r的新节点在T的左子树上,并当插入后的左子树深度增加1时,分别做以下处理:
*当T的根结点的bf为-1时,将根结点的bf改为0,T的深度不变;
*当T的根结点的bf为0时,将根结点的bf改为1,T的深度加1;
*当T的根结点的bf为1时,若T的左子树bf为1,则作LL旋转,旋转后将根结点与其右子树的根结点bf改为0,树的深度不变,若T左子树的bf为-1,则做LR旋转,旋转后修改其根结点和左右子树结点的BF,树的深度不变; - 如果r.key大于T的根结点的关键字,且T的右子树中不存在关键字和r.key相等的结点,则插入记录r的新节点在T的右子树上,并当插入后右子树神父增加1时,分别就不同情况处理,与3.中所述相对称。
算法:
//平衡二叉树的插入
#define LH +1//左子树高
#define EH 0//等高
#define RH -1//右子树高
int Insert_AVL(BSTree &T,RedType r,int &taller){//taller标记树是否长高
if(!T){
T=new BSTNode;
T->data=r;
T->lchild=T->rchild=NULL;
T->bf=EH;
taller=1;
}//如果T为空树,之间让根结点数据域为r,高度为1
else{
if(r.key==T.data.key){
taller=0;
return 0;
}//若r同根结点数据相同,则不进行插入
if(r.key<T.data.key){
if(!Insert_AVL(T->lchild,r,taller)) return 0;
//如果左子树存在与r相同的结点,则不进行插入
if(taller)//已经将r插入到左子树,且左子树长高了
switch(T->bf){//检查T的平衡度
case LH://插入前左子树比右子树高
LeftBalance(T);//判断并旋转
taller=0;//T没长高
break;
case EH://插入前左右子树一样高
T->bf=LH;//插入后左子树比右子树高
taller=1;//T长高了
break;
case RH://插入前右子树比左子树高
T->bf=EH;//插入后左右子树高度相等
taller=0;//T高度没变
break;
}
}
else{
if(!Insert_AVL(T->rchild,r,taller)) return 0;
//如果右子树存在与r相同的结点,则不进行插入
if(taller)//已经将r插入到右子树,且右子树长高了
switch(T->bf){//检查T的平衡度
case LH://插入前左子树比右子树高
T->bf=EH;//插入后左右子树高度相等
taller=0;//T没长高
break;
case EH://插入前左右子树一样高
T->bf=RH;//插入后左子树比右子树高
taller=1;//T长高了
break;
case RH://插入前右子树比左子树高
RightBalance(T);//判断并旋转
taller=0;//T没长高
break;
}
}
}
return 1;//插入成功
}
书上该代码有一处错误,在本代码中已经改正。
平衡二叉树的查找性能分析:查找时间复杂度为O(log2 n)。