目录
一.二叉排序树
二叉排序树,又称二叉查找树(BST,Binary Search Tree)。一棵二叉树或者是空二叉树,或者是具有如下性质的二叉树:
左子树上所有结点的关键字均小于根结点的关键字;右子树上所有结点的关键字均大于根结点的关键字;左子树和右子树又各是一棵二叉排序树。
左子树结点值<根结点值<右子树结点值,因为这样的特性,对某棵树进行中序遍历,可以得到一个递增的有序序列。
1.二叉排序树的查找
若树非空,目标值与根结点的值比较:若相等,则查找成功;若小于根结点,则在左子树上查找,否则在右子树上查找。查找成功,返回结点指针;查找失败返回NULL。
//二叉排序树结点
typedef struct BSTNode{
int key;
struct BSTNode *lchild,*rchild;
}BSTNode,*BSTree;
//在二叉排序树中查找值为key的结点
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;
}
//最坏空间复杂度O(1)
//递归实现
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大于根结点值,则插入到右子树。
typedef struct BSTNode{
int key;
struct BSTNode *lchild,*rchild;
}BSTNode,*BSTree;
//在二叉排序树插入关键字为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 0;
else if(k<T->key) //插入到T的左子树
return BST_Insert(T->lchild,k);
else //插入到T的右子树
return BST_Insert(T->rchild,k);
}
//最坏空间复杂度O(h)
//非递归实现
int BST_Insert(BSTree &T, int k) {
BSTree parent = NULL; // 记录父节点
BSTree current = T; // 当前节点
// 寻找插入位置,直到当前节点为空
while (current != NULL) {
parent = current;
if (k == current->key) {
// 树中已存在相同关键字的节点,插入失败
return 0;
} else if (k < current->key) {
// 插入到左子树
current = current->lchild;
} else {
// 插入到右子树
current = current->rchild;
}
}
// 创建新节点
BSTree newNode = (BSTree)malloc(sizeof(BSTNode));
if (newNode == NULL) {
// 内存分配失败
return -1;
}
newNode->key = k;
newNode->lchild = newNode->rchild = NULL;
// 空树,新节点为根节点
if (parent == NULL) {
T = newNode;
} else if (k < parent->key) {
// 将新节点插入到父节点的左子树
parent->lchild = newNode;
} else {
// 将新节点插入到父节点的右子树
parent->rchild = newNode;
}
return 1; // 插入成功
}
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.二叉树的删除
① 若被删除结点z是叶结点,则直接删除,不会破坏二叉排序树的性质(左子树结点值<根结点值<右子树结点值)。
② 若结点z只有一棵左子树或右子树,则让z的子树成为z父结点的子树,替代z的位置。
z的左子树替代z的位置:
z的右子树替代z的位置:
③ 若结点z有左、右两棵子树,则令z的直接后继(或直接前驱)替代z,然后从二叉排序树中删去这个直接后继(或直接前驱),这样就转换成了第一或第二种情况。
若用直接后继替代:
由于要保证“左子树结点值<根结点值<右子树结点值”,可以从要删除的结点的右子树中找到值最小的结点(右子树当中按照中序遍历第1个被访问的结点)替代当前要删除的结点。所以找到z的右子树中最左下结点(该结点一定没有左子树)替代z。
例如下图,删除50结点,用其右子树中最左下结点替代,也就是60替代,而60的位置则由其右子树替代,因为60作为最左下结点,一定没有左子树,所以按照 ② 的方法来。
若用直接前驱替代:
用直接前驱替代当前删除的结点,就是用当前删除结点的左子树中最大的值替代当前删除结点,也就是z的左子树中最右下的结点。
最右下的结点一定没有右子树,可能有左子树,若最右下的结点有左子树,则用其左子树替代最右下的结点即可。
5.二叉排序树的查找效率
在上一节中,已经学到了查找长度:在查找运算中,需要对比关键字的次数称为查找长度,反映了查找操作时间复杂度。
查找成功的平均查找长度ASL:
对于下面的二叉排序树,若要查找50这个数据元素,则只需要对比1次(1*1),若要查找26这个数据元素,则需要对比2次,66同理(2*2),依次类推:
ASL=(1*1+2*2+3*4+4*1)/8=2.625
对于下面这棵树同理:
ASL=(1*1+2*2+3*1+4*1+5*1+6*1+7*1)/8=3.75
所以不难发现,进行结点值的对比时,对比的次数不会超过这个树的高度(h)。最坏情况:每个结点只有一个分支,树高h=结点数n。平均查找长度=O(n)。最好情况:n个结点的二叉树最小高度为或
),平均查找长度=
。
在二叉排序树的构建中,尽可能让左右子树保持平衡,也就是树上任一结点的左子树和右子树的深度之差不超过1。能够提高二叉排序树的查找效率,使其平均查找长度达到最小,即。这样的二叉树就是后面会讲到的平衡二叉树。
查找失败的平均查找长度ASL:
对于下面这棵树,可能出现9种查找失败的可能,上面一层占7个,下面一层占两个:
ASL=(3*7+4*2)/9=3.22
注:到达失败结点时所查找的长度等于它上面的结点所在的层数。
同理下面这棵树查找失败的平均查找长度(ASL):
ASL=(2*3+3+4+5+6+7*2)/9=4.22
补充:
1.对二叉排序树进行中序遍历可得到有序序列。
2.当插入的关键字有序时,二叉排序树会形成一个长链,此时深度最大。并且在此种情况下进行查找,可能需要比较每个结点的关键字。
3.五个不同结点构造的二叉查找树的形态共有( )种?
有多少种二叉查找树,其实就是有多少种二叉树。5个结点,则最少有3层,最多有5层。
当有5层时,形态大致如下:
每一层都只有1个结点,并且这个结点可以是父结点的左孩子也可以是右孩子,所以总共有:
2*2*2*2=16种可能。
当有3层时,3个结点占上面两层,剩下的2个结点随便占下面4个坑都可以:
=6种
当有4层时,分析如下:
当第5个结点放在根结点下方时,总共有2*2*2=8种形态(每一层有两种变化方式)
当第5个结点放在第二层结点下方时,也总共有8种形态,理由同上。
但当第5个结点放在第三层结点下方时,则会出现重复,所以这种情况可能的形态为8/2=4
总共的形态有:16+6+8+8+4=42种
二.平衡二叉树
平衡二叉树(Balanced Binary Tree),简称平衡树(AVL树)--- 树上任一结点的左子树和右子树的
高度之差不超过1。
注:平衡二叉树不一定非要是二叉排序树,平衡二叉树只是改进二叉排序树查找效率的方法,理论上平衡二叉树只要满足“任一结点的左子树和右子树的高度之差不超过1”,都称为平衡二叉树,下面我们讲的其实是基于二叉排序树(左子树<根<右子树)的平衡二叉树。
结点的平衡因子=左子树高-右子树高。
例如下图50这个结点,左子树高度=2,右子树高度=3,所以他的平衡因子=2-3(-1),其他结点计算方法类似。
注:平衡二叉树结点的平衡因子的值只可能是-1、0或1。只要有任一结点的平衡因子绝对值大于1,
就不是平衡二叉树。
若AVL的某结点的左右孩子的平衡因子都为0,那么能不能说明左右子树高度相等呢?不能
例如下图,左右子树平衡因子都为0,但是高度不同,根结点的平衡因子为1
1.平衡二叉树的插入
在二叉排序树中插入新结点后,如何保持平衡?
在下图中,当插入67这个结点后,其祖先的平衡因子都受到了影响。如何恢复平衡?
具体方法是:从插入点往回找到第一个不平衡结点,他是离新插入结点最近的不平衡结点,调整以该结点为根的子树,该子树被称为"最小不平衡子树"。
在插入操作中,只要将最小不平衡子树调整平衡,则其他祖先结点都会恢复平衡。
那么如何调整最小不平衡子树呢?分为四种情况:
LL:在A的左孩子的左子树中插入导致不平衡。
RR:在A的右孩子的右子树中插入导致不平衡。
LR:在A的左孩子的右子树中插入导致不平衡。
RL:在A的右孩子的左子树中插入导致不平衡。
LL:在A的左孩子的左子树中插入导致不平衡。
如下图所示,BL这棵子树的高度是H,A的左子树的高度为H+1,由于BL插入了新结点,所以A的左子树的高度为H+2,而A的右子树的高度为H没变,所以A的平衡因子变为了2,A结点变为了不平衡结点。
如何恢复平衡:
LL平衡旋转(右单旋转)。将A的左孩子B向右上旋转代替A成为根结点,将A结点向右下旋转成为B的右子树的根结点,而B的原右子树则作为A结点的左子树。
由于需要满足BL<B<BR<A<AR:
如下图所示,B成为根节点,A成为B的右子树的根节点:
由于B的左子树BL<B,所以BL作为B的左子树:
原本BR是在B的右边的,但是BR的右孩子已经为A了,又要满足B<BR<A,所以BR成为A的右子树:
AR不变,继续挂在A的右边即可:
最后,二叉排序树恢复平衡:
RR:在A的右孩子的左子树中插入导致不平衡。
如下图所示,以A为根结点的子树变为了“最小不平衡子树"。
如何恢复平衡:
RR平衡旋转(左单旋转)。将A的右孩子B向左上旋转代替A成为根结点,将A结点向左下旋转成为B的左子树的根结点,而B的原左子树则作为A结点的右子树。
由于需要满足:AL<A<BL<B<BR:
如下图所示,将B作为根节点,A作为B的左子树的根节点:
B<BR,所以BR作为B的右孩子:
由于AL<A,所以AL作为A的左孩子;BL之前是B的左孩子,但是B的左孩子被A占了,又要满足A<BL<B,所以BL作为A的右孩子:
最后结果如下,二叉排序树保持了平衡,并且保证了左子树结点值<根结点值<右子树结点值:
LR:在A的左孩子的右子树中插入导致不平衡。
如何恢复平衡:
LR平衡旋转(先左后右双旋转)。由于在A的左孩子(L)的右子树(R)上插入新结点,A的平衡因子由1增至2,导致以A为根的子树失去平衡,需要进行两次旋转操作,先左旋转后右旋转。先将A结点的左孩子B的右子树的根结点C向左上旋转提升到B结点的位置,然后再把该C结点向右上旋转提升到A结点的位置。
首先将C左上旋,方法和上面讲的左上旋一样:
由于结点A依然为不平衡结点,所以继续将A右旋:
若是插入到以CL为根节点的子树中也是同理,先左旋C,再右旋C:
RL:在A的右孩子的左子树中插入导致不平衡。
如何恢复平衡:
RL平衡旋转(先右后左双旋转)。由于在A的右孩子(R)的左子树(L)上插入新结点,A的平衡因子由-1减至-2,导致以A为根的子树失去平衡,需要进行两次旋转操作,先右旋转后左旋转。先将A结点的右孩子B的左子树的根结点C向右上旋转提升到B结点的位置,然后再把该C结点向左上旋转提升到A结点的位置。
首先将C右上旋替代B结点的位置:
此时A结点依然为不平衡结点,再将C结点左上旋,替代A结点的位置:
当然,新结点插入CR也是同理:
总结:
只有左孩子才能进行右上旋,只有右孩子才能进行左上旋。
练习:
若在一个平衡二叉树中插入90:
插入新结点后,从下至上依次检查其祖先结点,遇到的第一个平衡因子的绝对值大于1的结点,就是第一个不平衡的结点。如下图所示,以66为根的子树就是最小不平衡子树。
可以发现,导致这棵子树不平衡的原因是:在其右孩子的右子树上插入了新结点(RR)。
处理的方法是右子树的根节点左上旋。
这棵树恢复为了平衡二叉树,并且符合左子树结点值<根结点值<右子树结点值。
若在如下平衡二叉树中插入63这个结点:
由于是RL型:这棵树不平衡的原因是在右孩子的左子树中插入了新结点:
先将左子树的根节点右上旋:
再将根节点左上旋:
自己练习一下:
2.平衡二叉树的查找效率
若树高为h,则最坏情况下,查找一个关键字最多需要对比h次,即查找操作的时间复杂度不可能超过 O(h)。
由于平衡二叉树树上任一结点的左子树和右子树的高度之差不超过1。我们假设以表示深度为h的平衡树中含有的最少结点数。若h=0,n0=0;h=1,则n1=1;若h=2,n2=2;以此类推:
n3=4,n4=7,n5=12,若一棵平衡二叉树n=9(结点数=9),那么他的高度最大只有可能是4,因为如果平衡二叉树的树高为5,那么这种平衡二叉树至少需要12个结点。
可以观察到,所有非叶结点的平衡因子均为1,就是平衡二叉树满足平衡的最少结点情况:
公式:
基于公式,含有n个结点的平衡二叉树的最大深度为,平衡二叉树的平均査找长度为
(这里只是说数量级)。
3.平衡二叉树的删除
插入新结点后,要保持二叉排序树的特性不变(左<中<右)。若插入新结点导致不平衡,则需要调整平衡。删除结点同理,删除结点后,要保持二叉排序树的特性不变。若删除结点导致不平衡,则需要调整平衡。
删除平衡二叉树结点的具体步骤:
① 按照删除二叉排序树结点的方法,删除结点。上面讲过:
• 若删除的结点是叶子,直接删。
•若删除的结点只有一个子树,用子树顶替删除位置
•若删除的结点有两棵子树,用前驱(或后继)结点顶替,并转换为对前驱(或后继)结点的删除。
② 从被删除结点开始,从下至上找到最小不平衡子树。如果祖先都没有出现不平衡结点(|平衡因子|>1),说明删除结点并没有影响平衡,不用调整二叉树。
③ 若有最小不平衡子树,那就找最小不平衡子树下,并且确定该子树下树高最高子树k,再从k中找到树高最高的子树m。
如图所示,以80为根节点的子树是树高最高的子树,也就是k;以90为根节点的子树是k下树高最高的子树,也就是m。(下面的讲解就用k,m替代了)
④ 根据m的位置,调整平衡(LL/RR/LR/RL,跟插入时调整平衡的方法是一样的)。
⑤ 如果不平衡向上传导,继续②。因为对最小不平衡子树的旋转可能导致树变矮,从而导致上层祖先不平衡(不平衡向上传递)。
具体看下面例子:
1.若想从下面的平衡二叉树中删除9这个数据元素:
① 9是叶子结点,直接删除。
② 向上查看9数据元素的祖先结点是否出现不平衡。发现祖先结点都没有因为9数据元素的删除而出现不平衡。结束此次删除操作。
2.若想从下面的平衡二叉树中删除55这个数据元素:
① 55是叶子结点,直接删除。
② 删除55后,向上找到最小的不平衡子树。
③ 找到k,m,根据m的位置调整平衡。下图中,m是以90为根节点的子树。90是75这个数据元素右孩子的右孩子,所以调整的方法是RR。
调整后的二叉树如下图所示:
④检查该树,其祖先并没有出现不平衡,所以至此删除操作结束。
3.若想从下面的二叉树中删除32结点:
① 因为32是叶子结点,所以直接删除。
② 删除32后,以44为根节点的子树成为最小不平衡子树。
③ 找到k,m,k是78,m是50,由于50是44这个数据元素的右子树的左孩子,所以调整平衡的方法为RL。m先进行右旋,再进行左旋。
右旋后的二叉树如下图所示:
再进行左旋,得到最终的二叉树:
④ 可以看到,这次平衡的调整,并没有导致其祖先出现不平衡,所以此次删除操作结束。
4.若想从下面的平衡二叉树中删除32数据元素:
下面这棵树的右子树,就是例3中的平衡二叉树,删除32数据元素的操作和例3相同。
调整平衡后,结果和例3相同:
由于该子树的树高变矮了,导致不平衡性向上传导,以33为根节点树的左子树高度为5,右子树高度为3,33成为不平衡结点:
① 找到k,m,如下图所示,k是10,m是20,因为20是33的左子树的右孩子,所以调整平衡的方法为LR。也就是先进行左旋,再进行右旋。
② 左旋后,二叉树如下图所示:
③ 继续右旋, 最后得到的二叉树如下图所示,该树为平衡二叉树,删除操作结束。
5.若想删除二叉树中75元素:
① 按照二叉排序树的删除规则删除75节点,由于被删除结点有左右子树,用前驱结点顶替(复制数据即可),并转化为对前驱结点的删除。(当然也可以用后继结点,这里先将前驱)
75的前驱就是75的左子树中最右下角的结点。如下图所示,60是75的直接前驱:
用60替代75,并将删除操作转化为对60这个数据元素的删除:
由于60只有一棵子树,所以用子树替代60的位置即可:
② 找到最小不平衡子树,并确定k和m。下图中,k是80,m是90,由于90是60的右子树的右孩子,所以调整平衡的操作是RR:
对80这个数据元素进行左单旋后,最终得到一棵平衡二叉树,至此删除操作结束:
① 对后继结点替代删除结点,也就是用被删除结点的右子树最左下角的数据元素替代被删除结点。下图中就是用77替代被删除元素(复制数据元素的值即可)。
并将删除操作转化为对77这个结点的删除,由于77结点时叶子节点,所以直接删除即可。撒删除77结点后,80结点成为不平衡结点。
② 最小不平衡子树的k是90,m是95或85都可以,这里先选择95。由于95是80的右子树的右孩子,所以调整平衡的操作为RR。
③ 对90这个数据元素进行左单旋之后,最小不平衡子树变为了平衡子树,并且不平衡的特性没有向上传导,删除操作结束。
若选择85作为m:
② 由于85是80的右子树的左孩子,所以调整平衡的方法是RL:
85结点先进行右旋,得到如下二叉树:
在将85结点左旋,得到如下二叉树:
③ 最终得到了平衡二叉树,并且不平衡性没有向上传导,所以删除操作结束。
注:平衡二叉树的删除操作时间复杂度为
例题:
1.高度为3的平衡二叉树的形态共有多少种?
高度为3的平衡二叉树有以下3种可能:① 根结点左子树高度为2,根结点右子树高度为2;
② 根结点左子树高度为2,根结点右子树高度为1;③ 根结点左子树高度为1,根结点右子树高度为2。
高度为2的平衡二叉树有以下3种可能:
所以总共有:3*3+3+3=15种
2.在任意一棵非空平衡二叉树(AVL树)T1中,删除某结点v之后形成平衡二叉树T2,再将v插入T2形成平衡二叉树 T3。下列关于T1与T3的叙述中,正确的是()
Ⅰ.若v是T1的叶结点,则T1与T3可能不相同
Ⅱ.若v不是T1的叶结点,则T1与T3一定不相同
Ⅲ.若v不是T1的叶结点,则T1与T3一定相同
I.正确,例如:
Ⅱ,Ⅲ错误,例如:
下图中“2”删除又插入后,得到了不一样的平衡二叉树。
下图中“2”删除又插入后,得到了相同的平衡二叉树。
三.红黑树
红黑树与二叉排序树,平衡二叉树都是用于查找的二叉树。从下图可以看到,红黑树在插入,删除,查找方面的优异程度和平衡二叉树相同,那红黑树相比于平衡二叉树有什么优势呢?
平衡二叉树要求任何结点左子树右子树的树高差不超过1,而插入/删除很容易破坏“平衡”特性,需要频繁调整树的形态。如:插入操作导致不平衡,则需要先计算平衡因子,找到最小不平衡子树(时间开销大),再进行 LL/RR/LR/RL 调整。
相比之下,红黑树的插入/删除很多时候不会破坏“红黑”特性,无需频繁调整树的形态。即便需要调整,一般都可以在常数级时间内完成。
平衡二叉树适用于以查为主、很少插入/删除的场景。红黑树适用于频繁插入、删除的场景,实用性更强。
1.红黑树的概念
红黑树是一棵二叉排序树,其左子树的结点值<根结点值<右子树结点值。相比于普通的二叉树,红黑树多了以下特性:
① 每个结点或是红色,或是黑色的。
② 根节点是黑色的。③ 叶结点(外部结点、NULL结点,也叫失败结点)均是黑色的。
④ 不存在两个相邻的红结点(即红结点的父节点和孩子结点均是黑色)。
⑤对每个结点,从该结点到任一叶结点的简单路径上,所含黑结点的数目相同。
struct RBnode{
int key; // 关键字的值
RBnode* parent; // 父节点指针
RBnode* lChild; // 左孩子指针
RBnede* rChild; // 右孩子指针
int color; //结点颜色,如:可用0/1表示黑/红,也可用枚举型enum表示颜色
}
//平衡二叉树中对每个结点需要标注平衡因子,而红黑树则不用。
补充:结点的黑高(bh)----从某结点出发(不含该结点)到达任一空叶结点的路径上黑结点总数。
若根节点黑高为h的红黑树,内部结点数(关键字)至少有多少个?
内部结点数最少的情况----总共h层黑结点的满树形态。为什么一定要是满树形态,因为如果这样才能满足"对每个结点,从该结点到任一叶结点的简单路径上,所含黑结点的数目相同"。
如下图所示,该红黑树根节点黑高=2时,内部结点数最少的情况。
同理,若跟节点黑高=3,那么内部节点最少的情况如下:
所以,若根节点黑高为h,内部结点数(关键字)最少有个(满树的结点数)。
由红黑树的特性可以推出以下性质:
1.从根节点到叶节点的最长路径不大于最短路径的2倍。
最短路径为全黑,最长路径就是红黑节点交替(因为红色节点不能连续),每条路径的黑色节点相同,则最长路径、刚好是最短路径的2倍。
路径:从根节点到叶子节点的路径长度就是路径上经过的节点的个数。
也就是说,红黑树中任何左子树和右子树的高度差,不会超过两倍。平衡二叉树左子树和右子树的高度差不超过1,可以看到平衡二叉树要求更加严格,所以平衡二叉树在插入新结点时,平衡的特性更加容易被破坏。每一次破坏都需要经过时间开销调整。而红黑树的特性没那么容易被破坏,所以更加高效。
2.红黑树的红结点数目最大可以是黑结点数目的2倍。
3.有n个内部节点(关键字)的红黑树高度
。所以也可以推出红黑树查找操作时间复杂度=
。(查找效率与AVL树(平衡二叉树)同等数量级)。
证明:
若红黑树总高度=h,则根节点黑高>=h/2,因为不能出现相邻的两个红节点。又因为上面推导过的内部节点数
,所以:
2.红黑树的查找
与BST(二叉排序树)、AVL(平衡二叉树)相同,从根出发,左小右大,若查找到一个空叶节点,则查找失败。
3.红黑树的插入
红黑树插入的规则如下:
1.先进行查找,确定插入位置(原理同二叉排序树),插入新结点:
•若新结点为根:那么使其为黑色。
•若新结点非根:使其为红色。因为要保证从某结点到任一叶结点的简单路径上,所含黑结点的数目相同。如果插入新结点,并使其为黑色,那么一定会破坏这一特性。
•若插入新结点后依然满足红黑树定义,则插入结束。
•若插入新结点后不满足红黑树定义,需要调整,使其重新满足红黑树定义。怎么调整呢?
如上图所示,若新结点的父亲的兄弟结点是黑色:那么需要进行旋转+染色。
若新结点的父亲的兄弟结点是红色:那么需要染色+变新。
看不懂没关系,现在来举例,一定10个例子都看一下:
从一棵空的红黑树开始,插入:20,10,5,30,40,57,3,2,4,35,25,18,22,23,24,19,18
① 新结点20是根节点,使其为黑色:
② 新结点10比20更小,所以插在20的左边:
③ 新结点5比10小,所以插在10的左边,插入后发现,该二叉树违反了红黑树中“不存在两个相邻的红结点”的规定,怎么解决?
由于其父结点的兄弟是黑结点,并且该结点插入的方式是LL型(爷节点的左子树的左孩子)。所以进行的操作是:
•将父结点进行右单旋,也就是将父结点换到爷结点的位置:
•最后将父结点和爷结点进行染色,父结点是红色就染成黑色,爷结点时黑色染成红色(反之同理)。
④ 新结点30比20大,所以插到20的右边,新结点的加入违反了红黑树中“不存在两个相邻的红结点”的规定。
30的父结点的兄弟5是红结点,怎么处理:
•将父结点,父结点的兄弟,爷结点都进行染色,即颜色翻转:
• 将爷结点视为新的结点,按照插入新结点的方法处理该结点。由于该结点是根结点,那么直接染成黑色即可。
⑤ 新结点40比30大,所以插在30的右边,下图的二叉树不满足红黑树的特点:
由于其父结点的兄弟结点是黑结点,并且40结点是爷结点的右子树的右孩子,即RR型:
•父结点30先进行左单旋,让父结点替代爷结点的位置:
•对刚刚处理的父结点和爷结点进行颜色翻转:
⑥ 新结点57比40大,所以插到40右边:
由于新结点父结点的兄弟结点时红结点,所以进行的操作是“染色+变新”:
•对父结点,父结点的兄弟结点和爷结点都进行颜色翻转:
•将爷结点视为新插入的结点,也就是30视作新插入的结点,由于新插入的结点没有破坏红黑树的特性,也没有再根结点的位置,所以不作变动。
⑦ 新结点3插入到5的左边,由于没有破坏红黑树的特性,所以不用处理。
⑧插入新节点2的操作同理,由于其父节点的兄弟结点是黑结点(黑叔),处理如下:
⑨ 插入新节点4,由于其父节点的兄弟结点是红结点(红叔),处理如下:
由于新节点满足红黑树的特性,不做调整。
如果爷结点作为新节点破坏了红黑树的特性,那么就再看他的父节点的兄弟结点是红/黑,重复一次刚才的处理过程,也就是:
如果他的父节点的兄弟结点是红结点,那么就将"叔父爷"三个结点颜色翻转,然后再将爷结点视为新节点进行下一步判断。
如果他的父节点的兄弟结点是黑结点,那么就根据(LL,LR,RL,RR)进行旋转,并且将父节点和爷结点的颜色翻转。
⑩ 插入新节点23,由于是LR型,所以:
首先进行左旋,再进行右旋,得到如下树:
最后将新节点(23)与其爷结点(25)进行颜色翻转:
RL型同理,首先右旋,再左旋,然后将将新节点与其爷结点进行颜色翻转。
总结一下这些情况:
4.红黑树的删除
红黑树的删除操作中注意以下几点即可:
①红黑树删除操作的时间复杂度=O(log2n)
②在红黑树中删除结点的处理方式和“二叉排序树的删除”一样
③按②删除结点后,可能破坏“红黑树特性”,此时需要调整结点颜色、位置,使其再次满足“红黑树特性”。
红黑树与平衡二叉树的对比:
1.红黑树与平衡二叉树都是自平衡二叉树:在插入和删除时能自动调整以保持其定义的平衡性。
2.
3.两者的查找,插入,删除操作的时间复杂度为O(log2n)。
4.红黑树任意一个结点的左右子树高度(含叶结点)之比不超过2,平衡二叉树任意一个结点左右子树的高度差不超过1。
四.B树
在之前学习的二叉查找树(BST)中,二叉树是这样定义的:
//二叉排序树结点
typedef struct BSTNode{
int key;
struct BSTNode *lchild,*rchild;
}BSTNode,*BSTree;
由二叉查找树可以演变为m叉查找树,下图所示:
在上图的5叉查找树中,一个结点最少有一个关键字,2个分叉;最多有4个关键字,5个分叉。结点内的关键字排列是有序的(从小到大,从大到小)。画红圈的失败结点表示的范围是{15,22}。如果要查找的关键字落在这个范围,则查找失败。
//5叉排序树的结点定义
struct Node{
ElemType keys[4]; //最多4个关键字
struct Node* child[5]; //最多5个孩子
int num; //结点中有几个关键字
};
在5叉查找树中查找关键字9与二叉查找树的查找过程类似:
首先将关键字9与五叉树中的第一个结点进行对比:9<22,所以往22左边继续查找。5<9<11,若关键字9存在,那么一定在5的右边结点,11的左边结点。在该结点中依次扫描关键字(这里演示的是顺序查找,当然也可以用折半查找),若有9这个关键字,则查找成功。
查找关键字41:在最后一层的结点中顺序查找时,由于40<41,所以查找指针右移,又由于41<42,查找指针会指向42左边的指针所指的位置,到达失败结点,查找失败。
若五叉树的每一个结点仅保留一个关键字,那么在保留相同多关键字的情况下,每个结点内关键字越少,树就越高,要查更多层结点,效率就低了。
如何保证m叉查找树的查找效率呢?
策略一:m叉查找树中,规定除了根节点外,任何结点至少有(向上取整)个分叉,即至少含有
个关键字。为什么排除了根节点:如果整个树只有1个元素,根节点只有两个分叉,保证不了
个分叉。
例如,对于5叉排序树,规定除了根节点外,任何结点都至少有3个分叉,2个关键字。
如下图所示,这棵树满足了策略一,但是查找效率也不高。这是因为树不平衡,导致树高很高,查找结点时要对比很多层结点。
策略二:m叉查找树中,规定对于任何一个结点,其所有子树的高度都要相同。平衡二叉树中规定左右子树高度差不超过1,但是这个特性在m叉树中实现起来比较麻烦,所以这里设定:”对于任何一个结点,其所有子树的高度都要相同"。由于这一特性,该m叉树的失败结点一定在同一层。
若满足策略一,二,那么这棵树就被称为B树,例如上图,就是一棵5阶B树。
1.B树的定义
B树,又称多路平衡查找树,B树中所有结点的孩子个数的最大值称为B树的阶,通常用m表示。一棵m阶B树或为空树,或为满足如下特性的m叉树:
(1) 树中每个结点至多有m棵子树,即至多含有m-1个关键字。
(2) 若根结点不是终端结点,则至少有两棵子树。因为要保证根结点在内所有结点的绝对平衡。
(3) 除根结点外的所有非叶结点至少有棵子树,即至少含有
个关键字。
(4) 所有的叶结点都出现在同一层次上,并且不带信息(可以视为外部结点或类似于折半查找判定树的查找失败结点,实际上这些结点不存在,指向这些结点的指针为空)。(5) 所有非叶结点的结构如下:
其中,Ki(i =1,2,…, n)为结点的关键字,且满足K1<K2 <...<Kn;Pi(i= 0,…n)为指向子树根结点的指针,且指针Pi-1所指子树中所有结点的关键字均小于Ki,Pi所指子树中所有结点的关键字均大于Ki,n
为结点中关键字的个数。
m阶B树的核心特性:
(1) 根节点的子树数∈[2,m],关键字数∈[1,m-1]。其他结点的子树数∈
;关键字数∈
。
(2) 对任一结点,其所有子树高度都相同。
(3) 关键字的值:子树0<关键字1<子树1<关键字2<子树2<....(类比二叉查找树 左<中<右)。
2.B树的高度
含n个关键字的m阶B树,最小高度、最大高度是多少?
注:大部分教材在计算B树的高时,都是不包括叶子结点(失败结点)的。
最小高度:
若要让B树的高度最小,在关键字数量不变的情况下,应该让每棵树尽可能满。对于m阶B树而言,每个结点最多有m-1个关键字以及m个分叉,则:
注:(1+m+m^2+m^3+m^4....m^(h-1)),利用等比数列求和公式计算。
最大高度:
最大高度---让各层的分叉尽可能的少,即根节点只有2个分叉,其他结点只有个分叉。各层结点至少有:第一层 1、第二层 2、第三层
.... 第h层
,第h+1层共有叶子结点(失败结点):
个(第h+1层是叶子结点,则该树有h层)。
为什么n个关键字的B树有n+1个叶子结点?因为n个关键字把(-∞,+∞)分为了n+1个区域,这n+1个区域对应n+1种失败的情况,即n+1个失败节点(叶子结点)。
最大高度也可以从另外一层考虑:
记k=,第一层最少可以有1个关键字,对应两个分叉。那么第二层就有2个结点,每个结点至少含k-1个关键字,所以第二层总共有2(k-1)个关键字。第二层有两个结点,每个结点有k个分叉,所以第三层有2k个结点,每个结点至少有k-1个关键字,所以第三层总共有2k(k-1)个关键字。依次类推:
也就是将表格中"最少关键字数"一列相加。
若关键字总数少于这个值,则高度一定小于h,因此 ,
,和刚才的结果相同。
3.B树的插入
以5阶B树为例---结点关键字的个数,即2<=n<=4
① 插入4个元素后,若想继续插入关键字80,就会导致根结点中关键字的个数超过上限:
对于这种情况,就需要将当前的结点分为两个结点:
从中间位置(
)将其中的关键字分为两部分,左部分包含的关键字放在原结点中,右部分包含的关键字放到新结点中,中间位置(
)的结点插入原结点的父结点。
② 插入90这个数据元素,按"查找"的方式确定插入位置。
注:新元素一定是插入到最底层“终端节点”。如果90插入到以下位置,那么各个失败结点就不属于同一层了,不满足B树的特性。
③ 插入99元素同理,接下来插入88这个数据元素, 插入操作导致当前结点的关键字数超过上限:
所以找到
元素,将这一元素提到父结点中,左右两部分分别放到不同的结点中:
④ 插入70结点, 当前结点的关键字也超过了上限,于是将
元素80提到父结点中,并且要保证父结点中的关键字序列有序。
可以观察到,若要保证关键字序列有序,将
元素提到父结点时,需要将该关键字放到指向所属结点的指针的右边。
⑤ 其余同理,当根结点中的关键字再次超过上限时,就将父结点的
元素80,提到上一级结点当中,并且将左右两个部分作为80的左右两边的元素:
根结点可以只包含一个关键字,其他结点的关键字2<<4,所以符合B树的特性。
注:插入关键字的初始位置是最底层叶结点,但可能因系欸但分裂而被转移到父结点中。
例如下图,若新插入的结点是49,那么49刚好是第
个结点,也就是第3个结点,如下图所示:
那么49会被移动到上面:
4.B树的删除
① 以上一步得到的B树为例,若要删除60这个关键字,则直接删除即可,因为60是终端节点。删除60这个数据元素后,该结点的关键字个数没有低于下限(
)。
注:一定要注意结点中关键字个数是否低于下限。
②若此次要删除80这个关键字,可以用直接前驱或直接后继来替代被删除的关键字。因为80这个被删除关键字是非终端节点。
先看用直接前驱替代被删除关键字:
直接前驱:当前关键字左侧指针所指子树中“最右下”的元素。在该B树中为77:
用直接后继替代被删除关键字:
直接后继:当前关键字右侧指针所指子树中“最左下”的元素。在该B树中为82:
用82替代被删除节点,并且将82后的元素前移即可。
所以,对非终端结点关键字的删除,必然可以转化为对终端结点的删除操作。
③ 若在该B树中删除38这个关键字,那么当前结点的关键字个数低于下限,要怎么处理:
• 若被删除关键字所在结点删除前的关键字个数低于下限,且与此结点右(或左)兄弟结点的关键字个数还很宽裕,则需要调整该结点、右(或左)兄弟结点及其双亲结点。
具体做法是:用70这个数据元素顶替49这个数据元素,再将49移到被删除结点的位置。
④ 若删除90这个关键字,删除该关键字后,该关键字所属结点少于下限:
• 如果像 ③ 一样借右边结点,那么右边结点也会少于下限,所以这次借左边结点的关键字。具体做法:找到90的前驱结点88替代90这个关键字:
再用88的前驱结点87替代88的位置:
⑤ 若想删除49这个关键字,删除该关键字后,该节点的关键字数小于2,也不能借右兄弟结点中的关键字,怎么办?
这样的情况,就将两个结点进行合并操作,具体做法就是将关键字删除后与左(或右)兄弟结点及双亲结点中的关键字进行合并:
进行完如下操作后,包含73关键字的结点中的关键字个数少于下限,所以进行同样的操作,合并该结点和他的兄弟结点以及双亲结点中的关键字:
得到以下树:
由于根结点中没有关键字了,所以可以把根结点删除,并且将红圈中的结点作为新结点:
所以:
在合并过程中,双亲结点中的关键字个数会减1。若其双亲结点是根结点且关键字个数减少至0(根结点关键字个数为1时,有2棵子树),则直接将根结点删除,合并后的新结点成为根;若双亲结点不是根结点,且关键字个数减少到
(低于下限),则又要与它自己的兄弟结点进行调整或合并操作,并重复上述步骤,直至符合B树的要求为止。
可以观察到,若被删结点是叶结点,那么会导致叶结点变化,若被删结点不是叶结点,则要先将被删结点和它的前驱和后继交换,最终转换为删除叶结点,所以删除操作一定会导致叶结点变化。
总结:
1.兄弟够借。若被删除关键字所在结点删除前的关键字个数低于下限,且与此结点右(或左)兄弟结点的关键字个数还很宽裕,则需要调整该结点、右(或左)兄弟结点及其双亲结点(父子换位法)。
2.兄弟不够借。若被删除关键字所在结点删除前的关键字个数低于下限,且此时与该结点相邻的左、右兄弟结点的关键字个数均=
,则将关键字删除后与左(或右)兄弟结点及双亲结点中的关键字进行合并。
3.什么时候情况下结点分裂会导致树高增1?
•插入一个元素引起B 树结点分裂后,只要从根结点到该元素插入位置的路径上至少有一个结点未满,B树就不会长高,例如下图,是一棵3阶B树:
•只有当结点的分裂传到根结点,并使根结点也分裂时,才会导致树高增1:
例题:
1.在一棵高度为2的5阶B树中,所含关键字的个数至少是()
注:从分支结点数入手。
对于5阶B树,根结点的分支数最少为2(关键字数最少为1),其他非叶结点的分支数最少为
(关键字数最少为2),因此关键字个数最少的情况如下图所示(叶节点不计入高度)。
答案:5
2.在一棵有15个关键字的4阶B树中,含关键字的结点个数最多的是()
A.5 B.6 C.10 D.15
解答:
关键字数量不变,要求结点数量最多,即要求每个结点中含关键字的数量最少。根据4阶B树的定义,根结点最少含1个关键字,非根结点中最少含[4/2]-1=1个关键字,所以每个结点中关键字数量最少都为1个,即每个结点都有2个分支,类似于排序二叉树,而15个结点正好可以构造一个4层的4阶B树,使得终端结点全在第四层,符合B树的定义,故选D。
3.在下图所示的5阶B树T中,删除关键字 260 之后需要进行必要的调整,得到新的B树T。下列选项中,不可能是T根结点中关键字序列的是()
A.60,90,280 B.60,90,350 C.60,85,110,350 D.60,90,110,350
答案:D
情况1:用前驱110替代,并用<100,110>的兄弟结点<70,80,85>进行填充:
情况2:用后继280替代:结点<300>不满足5阶B树定义且左右兄弟都不能借,结点<300>可以和右兄弟<400,500>以及关键字350合并为一个新结点<300,350,400,500>
情况3:用后继280替代,结点<300>也可以和左兄弟<100,110>以及关键字280合并为一个新结点<100,110,280,300>
所以不可能的情况为D
快速解法:只看D也不对,若<60,90,110,350>作为根结点,则在90和110之间只有100这一个数据,显然不不符合5阶B树的定义。
五.B+树
如下图所示,B+树和分块查找是很类似的,如下图所示最下面一层的结点为叶子结点,一个叶子结点可以包含多个关键字:
注:4阶B+树表示每个结点最多可以有4棵子树。
分块查找中“索引表”中保存每个分块的最大关键字和分块的存储区间。而B+树的上一级分块也是保存下一级分块的最大关键字。
一棵m阶的B+树需要满足以下条件:
1.每个分支结点最多有m棵子树(孩子结点)。
2.非叶根结点至少有两棵子树,其他每个分支结点至少有
棵子树。例如下图,中间的树就不是B+树,因为他的根结点不是叶子结点,但其只有一棵子树。
3.结点的子树个数与关键字个数相等。若上一级结点有3个关键字,则该结点对应三棵子树,这3个关键字分别是其下三棵子树中的最大关键字的值。
对比B树,若一个结点有2个关键字,则会对应三个分支,也就是三棵子树:
4.所有叶结点包含全部关键字及指向相应记录的指针,叶结点中将关键字按大小顺序排列,并且相邻叶结点按大小顺序相互链接起来。
5.所有分支结点中仅包含它的各个子结点中关键字的最大值及指向其子结点的指针。
1.B+树的查找
查找成功:
遍历根结点,由于15>9,所以继续查找15左边连接的结点:
遍历下一级的结点发现,有9这个关键字,但是这并不是我们要查找的目标,必须找到最下面一层的叶子节点,才能找到9这个关键字对应的记录。
从左往右依次遍历叶子结点,在叶子结点中找到9这个关键字,通过这一项保存的指针信息,就可以找到9关键字对应的记录:
查找失败:
若关键字7存在,一定存储在9所指的结点中:
遍历下一级结点,当遍历到8时,仍没有找到7这个关键字,并且这已经最后一层结点了,所以确定查找失败:
所以,在B+树中,无论查找成功与否,最终一定都要走到最下面一层结点。
相比之下,在B树的查找中,查找可能停在任何一层。
•通过指针p顺序查找:
除了可以从根节点往下查找外,也可以从p指针所指位置顺序查找。如下图所示,若要查找9这个关键字,指针p从左往右顺序遍历,直到找到9这个数据元素,再依据这一项找到对应记录即可。
2.B+树与B树的对比
(1)在B+树中,结点中的n个关键字对应n棵子树,而在B树中结点中的n个关键字对应n+1棵子树。
(2)对于m阶B树:根结点的关键字数
,其他结点的关键字数
对于m阶B+数:根结点的关键字数
,其他结点的关键字数
。
注:上面讲过,B+树可以只有一个根节点,即根节点是叶子
若根节点是非叶节点,那么一定至少有两个:
(3)在B+树中,叶结点包含全部关键字,非叶结点中出现过的关键字也会出现在叶结点中。
在B树中,各结点中包含的关键字是不重复的。
(4)在B+树中,叶结点包含信息,所有非叶结点仅起索引作用,非叶结点中的每个索引项只含有对应子树的最大关键字和指向该子树的指针,不含有该关键字对应记录的存储地址。
在B树中,B树的结点中包含了关键字对应的记录的存储地址。
(5)B+树支持顺序查找和随机查找,而B树仅支持随机查找。由于B+树的所有叶结点中包含了全部的关键字信息,且叶结点本身依关键字从小到大顺序链接美因茨可以进行顺序查找,而B树不支持顺序查找。
(6)B树和B+树都可用于文件索引结构,但B+树更适合做数据库索引和文件索引,因为它的磁盘读/写代价更低。下面是说明。
说明:
B+树中的结点其实是放在磁盘中的,也就是外存中。操作系统对于磁盘的读写是以磁盘块为单位的,所以B+树的不同结点存放在不同的磁盘块中。
若现在要查找42这一关键字,首先系统会将根结点所在的磁盘块读入内存。根据根结点的信息,42应该是在56所指的分块中找,所以系统会将 56所指的分块 所在的磁盘块读入内存,根据这个磁盘块中的信息,继续寻找目标关键字所在的磁盘块,再在磁盘块中读出目标关键字的记录信息即可。
对于B树也一样,每个结点都放在不同的磁盘块中,每查找一层结点都需要进行读磁盘(将某个磁盘块读入内存)的操作。
由于磁盘是一种慢速设备,每一次读磁盘的操作时间开销都比较大,所以如果B+树高度越高,查找某个关键字所需要的时间开销也大。
如何减少树的高度?
可以让每个结点保存尽可能多的关键字,也就是每个磁盘块中包含尽可能多的关键字,这样就使得B+树的阶更大(分支尽可能多),树更矮,读磁盘的次数更少,查找效率提高。
重点:
由于磁盘块的内存容量是固定的,所以B+树中非叶结点不含有关键字对应记录的存储地址,只包含对应子树的最大关键字和指向该子树的指针,就能留下更多的空间存放更多关键字,这样就保证树尽可能矮。而B树中,每个结点都包含了对应的记录的存储地址,这就会导致有限内存空间的磁盘块中,能存放的关键字更少,树自然就高了。
像MySql这类关系型数据库,其“索引”的功能就是通过B+树完成的。
总结: