一、二叉排序树(Binary Sort Tree)
又称二叉查找树,是一种对排序和查找都很有用的特殊二叉树。
1)二叉排序树的定义
二叉排序树或是一棵空树,或是具有下列性质的二叉树:
(1)若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
(2)若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
(3)它的左、右子树也分别为二叉排序树。
二叉排序树是递归定义的,由定义可以得出二叉排序树的一个重要性质:中序遍历一棵二叉排序树时可以得到一个结点值递增的有序序列。
下面给出二叉排序树的二叉链表存储表示:
typedef struct{
KeyType key; //关键字项
InfoType otherinfo; //其他数据项
}ElemType;
typedef struct BSTNode{
ElemType data; //每个结点的数据域包括关键字项和其他数据项
struct BSTNode *lchild,*rchild; //左右孩子指针
}BSTNode,*BSTree;
2)二叉排序树的查找
算法步骤:
(1)若树空,则查找失败,返回空指针;
(2)若树非空,将给定值key与根结点的关键字T->data.key进行比较:
>若key等于T->data.key,则查找成功,返回根结点地址;
>若key小于T->data.key,则递归查找左子树;
>若key大于T->data.key,则递归查找右子树。
代码:
BSTree SearchBST(BSTree T,KeyType key){
if((!T)||key==T->data.key) return T;
else if(key<T->data.key) return SearchBST(T->lchild,key);
else return SearchBST(T->rchild,key);
}
算法分析:
根据查找过程可以知道,在二叉排序树上查找关键字等于给定值的过程,恰好是走了一条从根结点到该结点的路径的过程,这与折半查找是类似的,但不同的是,同样的数据,可能建出不同形态的二叉排序树,最好的情况是建成的二叉排序树与折半查找的判定树相同,最坏的情况是二叉排序树被建成一棵单支树,平均来说,可以认为
二叉排序树上的查找和折半查找相差不大,但就维护表的有序性而言,二叉排序树更加有效,插入’删除操作更加方便。
3)二叉排序树的插入
二叉排序树的插入操作是以查找为基础的,将关键字为key的结点插入树中,需要从根结点向下查找,书中不存在关键字与key相等的结点时才进行插入。新插入的结点一定是一个新添加的叶子结点,并且是查找不成功时查找路径上访问的最后一个结点的左孩子或右孩子结点。
算法步骤
(1)若二叉排序树为空,则带插入结点*S作为根结点插入到空树中。
(2)若二叉排序树非空,则将key与根结点的关键字T->data.key进行比较
>若key小于T->data.key,则将*S插入左子树;
>若key大于T->data.key,则将*S插入右子树;
代码:
void InsertBST(BSTree &T,ElemType e){
if(!T){
S=new BSTNode;
S->data=e;
s->lchild=S->rchild=NULL;
T=S;
}
else if(e.key<T->data.key) InsertBST(T->lchild,e);
else if(e.key>T->data.key) InsertBST(T->rchild,e);
}
算法分析
基本过程是查找,时间复杂度也同样,是O(log2n)。
4)二叉排序树的创建
二叉排序树的创建从控的二叉排序树开始,每输入一个结点,经过查找操作,将节点插入到当前二叉排序树的合适位置。
算法步骤
(1)将二叉排序树T初始化为空树;
(2)读入一个关键字为key的结点;
(3)如果读入的关键字key不是输入结束标志,则循环执行以下操作:
>将此结点插入二叉排序树T中;
>读入一个关键字为key的结点。
代码:
void CreatBST(BSTree &T){
T=NULL;
cin>>e;
while(e.key!=ENDFLAG){ //ENDFLAG为自定义的输入结束标志
InsertBST(T,e); //将此结点插入二叉排序树中
cin>>e;
}
}
算法分析
插入一个结点的算法时间复杂度为O(log2n),n个结点的插入,时间复杂度为O(nlog2n)。
5)二叉排序树的删除
算法步骤
首先从树的根结点开始查找关键字为key的待删结点,如果树中不存在此结点,则不做任何操作;否则,假设被删结点为*p,其双亲结点为*f,PL和PR分别表示其左子树和右子树,设*p是*f的左孩子(右孩子情况类似)。具体可分为如下三种情况:
(1)*p结点为叶子结点,直接删除即可:f->lchild=NULL;
(2)*p结点只有左子树PL或右子树PR,此时只要令PL或PR直接称为*f的左子树即可:f->lchild=p->lchild(或f->lchild=p->rchild);
(3)*p结点左右子树均不为空。有两种处理方法:
1>从*p结点左孩子开始向右出发走到尽头的结点为*s,令*p的左子树为*f的左子树,而*p的右子树为*s的右子树:f->lchild=p->lchild;s->rchild=p->rchild;
2>令*p的直接前驱(或直接后继)替代*p,然后再从二叉排序树中删去它的直接前驱(或直接后继)。当以直接前驱*s替代*p时,由于*s只有左子树SL(也可能没有),则在删去*s后,只要令SL为*s双亲*q的右子树即可:p->data=s->data;q->rchild=s->rchild;
由于前一种处理方法可能增加树的深度,下面的代码使用第二种方法。
代码(伪):
void DeleteBST(BSTree &T,KeyType key){
p=T;
f=NULL;
while(p){ //查找关键字等于key的结点*p
if(p->data.key==key)break;
f=p;
if(p->data.key==key>key) p=p->lchild;
else p=p->rchild;
}
if(!p) return; //找不到被删结点返回
q=p;
if((p->lchild)&&(p->rchild)){ //被删结点左右子树均不为空
s=p->lchild;
while(s->rchild){
q=s;
s=s->rchild; //向右到尽头
}
p->data=s->data;
if(q!=p) q->rchild=s->lchild; //s是p的左孩子向右走到尽头
else q->lchild=s->lchild; //s恰好是p的左孩子
delete s;
return;
}
else if(!p->rchild){
p=p->lchild;
}
else if(!p->lchild){
p=p->rchild;
}
if(!f) T=p;
else if(q==f->lchild) f->lchild=p;
else f->rchild=p;
delete q;
}
算法分析
删除过程也是查找,时间复杂度也是O(log2n)。
二、平衡二叉树(Balanced Binary Tree 或 Height-Balanced Tree 或 AVL树)
1)平衡二叉树的定义
平衡二叉树是一种特殊的二叉排序树,定义如下:
平衡二叉树或是空树,或是具有如下特征的二叉排序树:
(1)左子树和右子树的深度之差的绝对值不超过1;
(2)左右子树也是平衡二叉树。
平衡因子:结点左子树和右子树的深度之差。
2)平衡二叉树的平衡调整方法
创建平衡二叉树之初是按二叉排序树的创建处理的,若插入结点后破坏了平衡二叉树的特性,则需要对平衡二叉树进行调整。
调整方法是:找到离插入结点最近且平衡因子绝对值超过1的祖先结点,以该结点为根的子树称为最小不平衡子树,可将重新平衡的范围局限于这棵子树。
一般失去平衡后进行调整的规律可归纳为如下四种,以示意图形式给出:
关键是时刻记住结点间的大小关系,这样就能明白旋转后的树怎么接。
实际应用时要找准离插入结点最近且平衡因子绝对值超过1的祖先结点。
三、B-树
前面的线性表的查找以及二叉排序树、二叉平衡树查找等查找方法适用于存储在计算机内存中较小的文件,统称为内查找法。内查找法都以结点为单位进行查找,这样需要反复地进行内、外存的交换,是很费时的。而磁盘管理系统中的目录管理以及数据库系统中的索引组织多数都采用B-树这种适用于外查找的平衡多叉树。
1)B-树的定义
一棵m阶的B-树,或为空树,或为满足下列特性的m叉树:
(1)树中每个结点至多有m棵子树;
(2)若根结点不是叶子节点,则至少有两棵子树;
(3)除根之外的所有非终端结点至少有不少于m/2棵子树;
(4)所有的叶子结点都出现在同一层次上,并且不带信息,通常称为失败结点。(失败结点并不存在,指向这些结点的指针为空。引入失败结点是为了便于分析B-树的查找性能);
(5)所有的非终端结点最多有m-1个关键字,结构如下图所示:
n | p0 | k1 | p1 | k2 | p2 | ... | kn | pn |
其中,ki为关键字,且ki<k(i+1);pi为指向子树根结点的指针,且指针p(i-1)所指子树中所有系欸但的关键字均小于ki,pn所指子树中所有结点的关键字均大于kn。具体实现时,为记录其双亲结点,B-树的存储结构通常增加一个parent指针,指向其双亲结点。
B-树具有平衡、有序、多路的特点
2)B-树的查找
查找过程与二叉排序树类似,先从根开始,根据大小关系,确定要查找的记录所在结点,然后在目标结点进行顺序查找。查找到叶子结点则说明书中不存在要查找的记录,查找失败。
假设结点类型定义如下:
#define m 3 //B-树的阶,暂设为3
typedef struct BTNode{
int keynum; //结点中关键字的个数,即结点的大小
struct BTNode *parent; //指向双亲结点
KeyType k[m+1]; //关键字向量,0号单元未用
strcut BTNode *ptr[m+1];//子树指针向量
Record *recptr[m+1]; //记录指针向量,0号单元未用
}BTNode,*BTree;
typedef struct{
BTNode *pt; //指向找到的结点
int i; //1..m,在结点中的关键字序号
int tag; //1:查找成功,0:查找失败
}Result; //B-树的查找结果类型
算法步骤
将给定值key与根结点的各个关键字进行比较,由于该关键字序列是有序的,所以查找时可采用顺序查找,也可采用折半查找。查找时:
(1)若key=ki,查找成功;
(2)若key<k1,则顺着指针p0所指向的子树继续向下查找;
(3)若ki<key<k(i+1);则顺着指针pi所指向的子树继续向下查找;
(4)若key>kj,(1<=j<=m-1),则顺着指针pj所指向的子树继续向下查找。
如果在自上而下的查找过程中,找到了置为key的关键字,则查找成功;如果直到叶子节结点也未找到,则查找失败。
代码(伪):
Result SearchBTree(BTree T,KeyType key){
p=T;
q=NULL;
found=false;
i=0;
while(p&&!found){
i=Search(p,key); //在p->k[1..keynum]中查找i,使得:p->k[i]<=key<p->[i+1]
if(i>0&&p->k[i]==key) found=true;
else {
q=p;
p=p->ptr[i];
}
if(found)return(p,i,1);
else return(q,i,0); //查找失败,返回key的插入位置信息
}
}
3)B-树的插入
算法步骤:
(1)在B-树中查找给定关键字的纪录,若查找成功,则插入操作失败;否则将新记录作为空指针ap插入到查找失败的叶子结点的上一层结点(由q指向)中;
(2)若插入新纪录和空指针后,q指向的结点的关键字个数未超过m-1,则插入操作成功,否则转入步骤(3);
(3)一该结点的第m/2(向上取整)个关键字k(m/2)为拆分点,将该结点分成3个部分:k(m/2)左边部分、k(m/2)、k(m/2)右边部分。k(m/2)左边部分仍然保留在原结点中;k(m/2)右边部分存放在一个新创建的结点(由ap指向)中;关键字值为k(m/2)的记录和指针ap插入到q的双亲结点中。因q的双亲结点增加一个新的记录,所以必须对q的双亲结点重复(2)和(3)的操作,以此类推,直至由q指向的结点是根结点,转入步骤(4);
(4)由于根结点无双亲,则由其分裂产生的两个结点的指针ap和q,以及关键字为k(m/2)的记录构成一个新的根结点。此时,B-的高度增加1。
代码(伪):
(代码中描述的q和i是由查找函数SearchBTree返回的信息而得)
Status InsertBTree(BTree &T,KeyType key,BTree q,int i){
x=key;
ap=NULl;
finished=false;
while(q&&!finished){
Insert(q,i,x,ap) //将x和ap分别插入到q->key[i+1]和q->ptr[i+1]
if(q->keynum<m) finished=true; //插入完成
else{ //分裂结点*q
s=ceil((m+1)/2); //取上界
split(q,s,ap) //分裂成s左,s,s右三个部分,将q->k[s+1..m],q->ptr[s..m]和q->recptr[s+1..m]移入新结点*ap
x=q->k[s];
q=q->parent;
if(q) i=Search(q,x); //在双亲结点中查找x的插入位置
}
}
if(!finished) //T是空树,或者根结点已分裂为结点*q和*ap
newRoot(T,q,x,ap); //生成含信息(T,x,ap)的新的根结点*T,原T和ap为子树指针
}
四、B+树
B+树是一种B-树变形的树,更适合用于文件索引系统。
1)B+树和B-树的差异
一棵m阶的B+树和m阶的B-树的差异在于:
(1)有n棵子树的结点中含有n个关键字;
(2)所有的叶子结点中包含了全部关键字的信息,以及指向含这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接;
(3)所有的非终端节点可以看成是索引部分,结点中仅含有其子树(根结点)中的最大(或最小)关键字
通常在B+树上有两个头指针,一个指向根结点,另一个指向关键字最小的叶子结点。
B+树最底层的叶子结点才是真实数据,其他祖先结点数据可以作为数据范围参考。
注:本文所有内容均来源于《数据结构(C语言第二版)》(严蔚敏老师著)