树形结构查找(BST、AVL树、RBT)

对于二叉排序树(Binary Search Tree, BST)、平衡二叉树(AVL树)以及红黑树(Red-Black Tree, RBT),要了解它们的概念、性质和相关操作等。

一、搜索树——二叉排序树(BST)

构造一棵二叉排序树的目的并不是为了排序,而是为了提高查找、插入和删除关键字的速度,二叉排序树这种非线性结构也有利千插入和删除的实现。

1. BST的概念

二叉排序树的定义:二叉排序树(也称二叉查找树、二叉搜索树)是一颗二叉树,可能为空;一颗非空的二叉排序树满足以下特征:

① 每个元素有一个关键字,并且任意两个元素的关键字都不同,因此,所有的关键字都是唯一的。【若有重复关键字,则这样的二叉树称为有重复值的二叉搜索树(binary search tree with duplicates)】
② 若左子树非空,则在根结点的左子树中,元素的关键字都小于根结点的关键字。
③ 若右子树非空,则在根结点的右子树中,元素的关键字都大于根结点的关键字。
④ 根结点的左、右子树也分别是一颗二叉排序树。

根据二叉排序树的定义,左子树结点值<根结点值<右子树结点值,因此对二叉排序树进行中序遍历,可以得到一个递增的有序序列。

2. BST的操作和实现

1)BST的查找Get

假设要查找关键字为 theKey 的元素:
① 先从根结点开始查找:如果根结点为空,那么搜索树不包含任何元索,即查找失败;
② 如果不空, 则将 theKey 与根结点的关键字进行比较:
I、如果 theKey 小于根结点的关键字,则在根结点的左子树上继续查找;
II、如果 theKey 大于根结点的关键字,则在根结点的右子树上继续查找;
III、如果 theKey 等于根结点的关键字, 则查找成功。
③ 在子树上的查找与步骤①②类似,显然这是一个递归的过程。

下面给出了BST进行查找操作的相应代码:
该过程的时间复杂性为 O(h),其中 h 是二叉排序树的高度。

BSTNode *BST_Search(BiTree T, ElemType theKey){
    while(T != NULL && theKey != T->data){	//若树空或等于根结点值,则结束循环
        if(theKey < T->data){	//小于,继续在左子树上查找
            T = T->leftChild;
        }
        else{	//大于,继续在右子树上查找
            T = T->rightChild;
        }
    }
    return T;
}

二叉排序树的查找也可用递归算法实现,递归算法比较简单,但执行效率较低。

2)BST的插入Insert

二叉排序树作为一种动态树表,其特点是树的结构通常不是一次生成的,而是在查找过程中,当树中不存在关键字值等于给定结点的关键字值时再进行插入的。

假设要在二叉排序树中插入一个新元素 thePair:
① 首先要通过查找来确定,在树中是否存在某个元素,其关键字与 thePair 的关键字相同:如果搜索成功,那么插入失败;
② 如果搜索不成功,那么就将新元素作为搜索中断结点的孩子插入二叉排序树:
I、若原二叉排序树为空,则将新元素作为根结点直接插入;
II、否则,若元素 thePair 的关键字小于搜索中断结点的值,则插入到搜索中断结点的左子树;
III、若元素 thePair 的关键字大于搜索中断结点的值,则插入到搜索中断结点的右子树。
插入的结点一定是一个新添加的叶结点,且是查找失败时的查找路径上访问的最后一个结点的左孩子或右孩子。

下面给出了BST进行插入操作的相应代码:

bool BST_Insert(BiTree &T, BSTNode *thePair){
    if(T == NULL){	//原二叉排序树为空,将新插入的元素作为根结点
        T = (BiTree)malloc(sizeof(BSTNode));
        T = thePair;
        T->leftChild = T->rightChild = NULL;
        return true;	//插入成功
    }
    else if(thePair->data = T->data){return false;}	//树中存在相同关键字的结点,插入失败
    else if(thePair->data < T->data){	//插入T的左子树
        return BST_Insert(T->leftChild, thePair);
    }
    else{	//插入T的右子树
        return BST_Insert(T->rightChild, thePair);
    }
}

如下图所示,在一棵二叉排序树中依次插入结点 28 和结点 58,虚线表示的边是其查找的路径。

3)BST的构造

从一棵空树出发,依次输入元素,将它们插入二叉排序树中的合适位置。设查找的关键字序列为{45, 24, 53, 45, 12, 24} ,则生成的二叉排序树如下图所示。

下面给出了BST进行构造操作的相应代码:

void Creat_BST(BiTree &T, BiTNode *str[], int n){
    T = NULL;	//初始时T为空树
    int i = 0;
    while(i < n){	//依次将每个关键字插入二叉排序树中
        BST_Insert(T, str[i]);
        i++;
    }
}

4)BST的删除Erase

在二叉排序树中删除一个结点时, 不能把以该结点为根的子树上的结点都删除,必须先把被删除结点从存储二叉排序树的链表上摘下,将因删除结点而断开的二叉链表重新链接起来, 同时确保二叉排序树的性质不会丢失。

假设要删除的结点是 p,我们需要考虑以下三种情况:

① p 是叶结点:直接释放该叶结点空间,若该结点是根结点,则令根为NULL。

② p 只有一棵非空子树(只有一颗左子树 or 只有一颗右子树):让 p 的子树成为 p 父结点的子树,替代 p 的位置。
【如果 p 没有父结点(即 p 是根结点),则 p 的唯一子树的根结点成为新的排序树的根结点;如果 p 有父结点 pp,则修改 pp 的指针域,使它指向 p 的唯一孩子,然后释放结点 p 。】

③ p 有两棵非空子树:先将该结点的元素替换为它的左子树的最大元素或右子树的最小元素,然后把替换元素的结点删除。
【右子树的最小关键字结点(左子树的最大关键字节点)要么没有子树,要么只有一棵子树。要删除一个左右子树都不为空的元素结点,我们的算法是:先替换,然后删除一个叶结点或一个仅有单子树的结点,即转换成了第①或第②种情况。】

要在一个结点的左子树中查找关键字最大的元素,先移动到左子树的根,然后沿着右孩子指针移动,直到右孩子指针为 NULL 的结点为止。类似地,要在一个结点的右子树中查找关键字最小的元素,先移动到右子树的根,然后沿着左孩子指针移动,直到左孩子指针为 NULL 的结点为止。

【思考】若在二叉排序树中删除并插入某结点,得到的二叉排序树是否和原来的相同?
【答】不同。

3. BST的查找效率分析

二叉排序树的查找效率,主要取决于二叉排序树的高度。假设有一棵 n 个元素的二叉排序树:

① 若二叉排序树是一个只有左(右)孩子的单支树(类似于有序的单链表),则其查找、插入和删除操作所需要的平均时间均为 O(n) ,这个性能比无序链表的相应操作好不了多少。
【在最坏情况下,即构造二叉排序树的输入序列是有序的,则会形成一个树高为 n 的倾斜的单支树,此时二叉排序树的性能显著变坏。例如:在一棵初始为空的二叉排序树中,按顺序插入一组关键字为 [1, 2, 3, … , n] 的元素,树的高度便是 n 。】

② 若二叉排序树的左、右子树的高度之差的绝对值不超过1 ,则其查找、插入和删除操作所需要的平均时间均为 O(log2n) 。

4. BST与折半查找的异同

线性结构查找(顺序、折半、分块)

① 就 查找过程 而言: 二叉排序树与折半查找相似。

② 就 平均时间性能 而言:二叉排序树上的查找和折半查找差不多。但折半查找的判定树唯一,而二叉排序树的查找不唯一,相同的关键字由于插入顺序不同,可能会生成不同的二叉排序树,如下图所示。

③ 就 维护表的有序性 而言:
I、二叉排序树无须移动结点,只需修改指针即可完成插入和删除操作,平均执行时间为 O(log2n)。
II、折半查找的对象是有序顺序表,若有插入和删除结点的操作,所花的代价是 O(n) 。
当有序表是静态查找表时,宜采用顺序表作为其存储结构,选择折半查找实现其查找操作;
当有序表是动态查找表时,则应选择二叉排序树作为其逻辑结构。

二、平衡搜索树——AVL搜索树

如果二叉搜索树(BST)的高度总是 O(log2n) ,我们就能保证查找、插入和删除的平均时间为 O(log2n) 。将最坏情况下的高度为 O(log2n) (任意结点的左、右子树高度差的绝对值不超过 1 )的树称为平衡二叉树(Balanced Binary Tree),一种比较流行的平衡二叉树是 AVL 树(AVL Tree)。
【一般默认平衡二叉树就是 AVL 树】

1. AVL搜索树的概念

1)AVL 树的定义

一棵空的二叉树是 AVL 树;如果 T 是一棵非空的二叉树,TL 和 TR 分别是其左子树和右子树,那么当 T 满足以下条件时, T 是一棵 AVL 树:
① TL 和 TR 是 AVL 树;
② |hL - hR| <= 1,其中 hL 和 hR 分别是 TL 和 TR 的高。

2)平衡因子的定义

定义结点的左子树与右子树的高度差为该结点的平衡因子 bf,即结点 x 的平衡因子 bf(x) 定义为:x 的左子树高度 - x 的右子树高度。从 AVL 树的定义可以知道,其结点的平衡因子的值只可能是 -1、0 或 1 。

3)AVL 搜索树的定义

一棵 AVL 搜索树既是二叉搜索树(BST), 也是 AVL 树【一般默认平衡二叉搜索树就是 AVL 搜索树】。

判断下图中哪些是 AVL 搜索树:

a)虽然是 AVL 树,但不是 BST,因此不是 AVL 搜索树;
b)是 AVL 树,也是 BST,因此是 AVL 搜索树;
c)虽然是 BST,但不是 AVL 树,因此不是 AVL 搜索树。

2. AVL搜索树的操作和实现

AVL 搜索树保证平衡的基本思想如下:每当在 AVL 搜索树中插入(或删除)一个结点时,
① 首先检查其插入路径上的结点是否因为此次操作而导致了不平衡。
② 若导致了不平衡,则先找到插入路径上离插入结点最近的平衡因子的绝对值大于 1 的结点 A,再对以 A 为根的子树(即最小不平衡子树),在保持二叉排序树特性的前提下,调整各结点的位置关系,使之重新达到平衡。
③ 若没有导致不平衡,则不进行后续操作。

1)AVL搜索树的插入Insert

结点 A 的不平衡情况有两类:L 型不平衡(新插入结点在 A 的左子树中) 和 R 型不平衡(新插入结点在 A 的右子树中)。在从根到新插入结点的路径上,根据 A 的孙结点,A 的不平衡情况还可以细分为:LL 型不平衡(新插入结点在 A 结点的左子树的左子树中),LR 型不平衡(新插入结点在 A 结点的左子树的右子树中), RR 型不平衡(新插入结点在 A 结点的右子树的右子树中)和 RL 型不平衡(新插入结点在 A 结点的右子树的左子树中)。

AVL 搜索树插入过程的前半部分与二叉排序树相同,但在新结点插入后,若造成查找路径上的某个结点不再平衡,则需要做出相应的调整。可将调整的规律归纳为下列 4 种情况:

① LL 平衡旋转(右单旋转)

由于在结点 A 的左孩子(L)的左子树(L)上插入了新结点,A 的平衡因子由 1 增至 2,导致以 A 为根的子树失去平衡,需要一次向右的旋转操作。将 A 的左孩子 B 向右上旋转代替 A 成为根结点,将 A 结点向右下旋转成为 B 的右子树的根结点,而 B 的原右子树则作为 A 结点的左子树。

下图为 LL 型平衡旋转的示例图,其中圆形结点旁的数值代表结点的平衡因子,矩形块表示相应结点的子树,矩形块下方的数值代表该子树的高度。

② RR 平衡旋转(左单旋转)

由于在结点 A 的右孩子(R)的右子树(R)上插入了新结点,A 的平衡因子由 -1 减至 -2,导致以 A 为根的子树失去平衡,需要一次向左的旋转操作。将 A 的右孩子 B 向左上旋转代替 A 成为根结点,将 A 结点向左下旋转成为 B 的左子树的根结点,而 B 的原左子树则作为 A 结点的右子树。下图为 RR 型平衡旋转的示例图:

③ LR 平衡旋转(先左后右双旋转)

由于在 A 的左孩子(L)的右子树(R)上插入新结点,A 的平衡因子由 1 增至 2,导致以 A 为根的子树失去平衡,需要进行两次旋转操作,先左旋转后右旋转。先将 A 结点的左孩子 B 的右子树的根结点 C 向左上旋转提升到 B 结点的位置,然后把该 C 结点向右上旋转提升到 A 结点的位置。下图为 LR 型平衡旋转的示例图:

④ RL 平衡旋转(先右后左双旋转)

由于在A 的右孩子(R)的左子树(L)上插入新结点,A 的平衡因子由 -1 减至 -2,导致以 A 为根的子树失去平衡,需要进行两次旋转操作,先右旋转后左旋转。先将 A 结点的右孩子 B 的左子树的根结点 C 向右上旋转提升到 B 结点的位置,然后把该 C 结点向左上旋转提升到 A 结点的位置。下图为 RL 型平衡旋转的示例图:

【注意: LR 和 RL 旋转时,新结点究竟是插入 C 的左子树还是插入 C 的右子树不影响旋转过程。而上图是以插入 C 的左子树中为例。】

我们把矫正 LL 和 RR 型不平衡所做的转换称为单旋转(single rotation),把矫正 LR 和 RL 型不平衡所做的转换称为双旋转(double rotation)。对 LR 型不平衡所做的双旋转可以看做 RR 旋转加 LL 旋转;而对 RL 型不平衡所做的双旋转可以看做 LL 旋转加 RR 旋转。

AVL 搜索树插入操作的简便方法:找到违反平衡条件的最小子树的根结点,调整该路径上从根结点出发的三个结点,剩余结点按照 BST 的性质进行摆放。

2)AVL搜索树的删除Erase

若删除某结点后的 AVL 搜索树不平衡,令 A 是最小不平衡子树的根结点。要恢复结点 A 的平衡,则需要根据不平衡的类型而定。如果删除发生在 A 的左子树,那么不平衡类型是 L 型;否则,不平衡类型就是 R 型。
① 假设 A 有一棵以 B 为根的左子树。根据 B 的 bf 值,可以把一个 R 型不平衡细分为 R0、R1 和 R-1 。例如,R-1 型不平衡意味着删除操作发生在 A 的右子树且 A 的左子树 B 的平衡因子为 -1 。
② 假设 A 有一棵以 B 为根的右子树。根据 B 的 bf 值,可以把一个 L 型不平衡细分为 L0、L1 和 L-1 。例如,L1 型不平衡意味着删除操作发生在 A 的左子树且 A 的右子树 B 的平衡因子为 1 。

与 AVL 搜索树的插入操作类似,以删除结点 X 为例来说明 AVL 搜索树删除操作的步骤:
I、用二叉排序树的方法对结点 X 执行删除操作。
II、若导致了不平衡,则从结点 X 开始向上回溯,找到第一个不平衡的结点 A (即最小不平衡子树)。 B 为结点 A 的高度最高的孩子结点,C 为结点 B 的高度最高的孩子结点。
III、然后对以 A 为根的子树进行平衡调整,其中 A 、B 和 C 可能的位置有以下 4 种情况:

① LL 平衡旋转(右单旋转)

(B 是 A 的左孩子, C 是 B 的左孩子)
【此时的 C 为 BL,LL 与 R1 型的旋转相同;LL 与 R0 型旋转的区别仅在于 A 和 B 最后的平衡因子】

② LR 平衡旋转(先左后右双旋转)

(B 是 A 的左孩子, C 是 B 的右孩子)
【此时的 C 为 BR,LR 与 R-1 型的旋转相同】

③ RR 平衡旋转(左单旋转)

(B 是 A 的右孩子, C 是 B 的右孩子)
【此时的 C 为 BR,RR 与 L-1 型的旋转相同;RR 与 L0 型旋转的区别仅在于 A 和 B 最后的平衡因子】

④ RL 平衡旋转(先右后左双旋转)

(B 是 A 的右孩子, C 是 B 的左孩子)
【此时的 C 为 BL,RL 与 L1 型的旋转相同】

上述四种情况与插入操作的调整方式一样。不同之处在于:
① 插入操作仅需要对以 A 为根的子树进行平衡调整;
② 而删除操作就不一样,先对以 A 为根的子树进行平衡调整,如果调整后子树的高度减 1, 则可能需要对 A 的祖先结点进行平衡调整,甚至回溯到根结点(导致树高减 1)。
即AVL搜索树最多进行一次旋转即可完成插入操作,但在删除操作时,仅用一次旋转可能还无法恢复平衡,所需要的最多旋转次数为 O(log2n) 。

3)AVL搜索树的查找Get

在 AVL 搜索树上进行查找的过程与二叉排序树的相同。因此,在查找过程中,与给定值进行比较的关键字个数不超过树的深度。

假设以 nh 表示深度为 h 的平衡二叉树中含有的最少结点数。显然,有 n0 = 0、n1 = 1、n2 = 2,并且存在规律:nh = nh-1 + nh-2 + 1。
【该结论可用于求解给定结点数的 AVL 树的查找所需的最多比较次数(或 AVL 树的最大高度)】

深度为 h 的平衡二叉树中含有的最多结点数是满二叉树的情况。

可以证明:含有 n 个结点的平衡二叉树的最大深度为 O(log2n),因此 AVL 搜索树的平均查找长度为 O(log2n) 。

三、平衡搜索树——红黑树(RBT)

RBT 和 AVL 搜索树都使用“旋转”来保持平衡。AVL 搜索树对每个插入操作最多需要一次旋转,对每个删除操作最多需要 O(log2n) 次旋转;而 RBT 对每个插入和删除操作,都只需要一次旋转。
为了保持 AVL 搜索树的平衡性,插入和删除操作后会非常频繁地调整全树整体拓扑结构,代价较大。为此在 AVL 搜索树的平衡标准上进一步放宽条件,引入了红黑树的结构。

1. RBT的概念与性质

一棵红黑树是满足如下性质的二叉排序树:
性质 ① :每个结点或是红色,或是黑色的。
性质 ② :根结点是黑色的。
性质 ③ :叶结点(虚构的外部结点,即 NULL 结点)都是黑色的。
性质 ④ :不存在两个相邻的红结点(即红结点的父结点和孩子结点均是黑色的)。
性质 ⑤ :对每个结点,从该结点到任意一个叶结点的简单路径上,所含黑结点的数量相同。
【简记:红黑树的判定:左根右,根叶黑,不红红,黑路同。】

从某结点出发(不含该结点)到达一个叶结点的任意一个简单路径上的黑结点总数称为该结点的黑高(记为 bh) ,黑高的概念是由上述性质 ⑤ 确定的。根结点的黑高称为红黑树的黑高。

与折半查找树和 B 树类似,为了便于对红黑树的实现和理解,引入了 n +1 个外部叶结点,以保证红黑树中每个结点(内部结点)的左、右孩子均非空。下图为一棵红黑树:

I、内部结点:是指树中具有子结点的结点,通常也被称为非叶子结点。例如,在红黑树中,除了最底层没有子结点的那些结点,其他所有结点都是内部结点。红黑树的内部结点存储了实际的数据,因此被用来维持树的搜索、插入和删除操作,且遵循红黑树的性质。
II、外部结点:指的是树的叶子结点,或者说是无子结点的结点。对于红黑树来说,外部结点主要是指表示空位的“NULL”结点。这些结点通常并不存储实际的数据。外部结点的颜色通常为黑色,它们在红黑树中用于方便实现树的平衡,帮助维持树的性质。

2. RBT的结论

① 结论 I :从根到叶结点的最长路径不大于最短路径的 2 倍。
【说明】由性质 ⑤,当从根到任意一个叶结点的简单路径最短时,这条路径必然全由黑结点构成。由性质 ④, 当某条路径最长时,这条路径必然是由黑结点和红结点相间构成的,此时红结点和黑结点的数量相同。

② 结论 II :有 n 个内部结点的红黑树的高度 h <= 2log2(n + 1) 。
【证明】由结论 I 可知,从根到叶结点(不含叶结点) 的任何一条简单路径上都至少有一半是黑结点,因此,根的黑高至少为 h / 2,于是有 n >= 2h/2 - 1, 即可求得结论。

可见,红黑树的“适度平衡”,由 AVL 树的“高度平衡”,降低到“任意一个结点左右子树的高度,相差不超过 2 倍”,也降低了动态操作时调整的频率。
对于一棵动态查找树,如果插入和删除操作比较少,查找操作比较多, 采用 AVL 树比较合适,否则采用红黑树更合适。但由于维护这种高度平衡所付出的代价比获得的效益大得多,红黑树的实际应用更广泛。C++ 中的 map 和 set (Java 中的 TreeMap 和 TreeSet) 以及 C++ 的 STL 中实现字典所使用的结构都是用红黑树实现的。

③ 结论 III :新插入红黑树中的结点初始着为红色。
【说明】假设新插入的结点初始着为黑色,那么这个结点所在的路径比其他路径多出一个黑结点(几乎每次插入都破坏性质 ⑤),调整起来也比较麻烦。如果插入的结点是红色的,此时所有路径上的黑结点数量不变,仅在出现连续两个红结点时才需要调整,而且这种调整也比较简单。

3. RBT的操作和实现

1)RBT的查找Get

可以使用二叉搜索树的查找代码来查找 RBT。二叉搜索树查找的复杂性为 O(h)(h 为树高),而对RBT来说,则为 O(logn)(n 为结点个数)。由于二叉搜索树、AVL 搜索树和RBT都用相同的查找代码,并且在最坏情况下 AVL 搜索树的高度是最小的。所以,在那些以查找操作为主的应用中,在最坏情况下 AVL 树的时间复杂性是最优的。

2)RBT的插入Insert

红黑树的插入过程和二叉查找树的插入过程基本类似, 不同之处在于,在红黑树中插入新结点后需要进行调整(主要通过重新着色或旋转操作进行),以满足红黑树的性质。

设结点 z 为新插入的结点。插入过程描述如下:
I、用二叉查找树插入法插入,并将结点 z 染红。若结点 z 的父结点是黑色的,无须做任何调整,此时就是一棵标准的红黑树。
II、如果结点 z 是根结点,将 z 染黑(树的黑高增 1),结束。
III、如果结点 z 不是根结点,并且 z 的父结点是红色的,则分为下面 3 种情况:

【区别在于 z 的叔结点的颜色不同。因为 z 的父结点是红色的,插入前的树是合法的,根据性质 ② 和 ④,z 的爷结点必然存在且为黑色。性质 ④ 只在 z 和 z 的父结点之间被破坏了。】

① z 的叔结点是黑色的,且 z 是一个右孩子
② z 的叔结点是黑色的,且 z 是一个左孩子

【黑叔:爷父孙调整(重组成上黑下红的上三角)。】

以上是父结点在右侧的情况(即 RR 和 RL),下图是父结点在左侧(即 LR 和 LL)的情况:
其中,每棵子树 T1、T2 、T3 和 T4 都有一个黑色根结点,且具有相同的黑高。

③ z 的叔结点是红色的

【红叔:叔父与爷色对调,爷视为新结点。】

上图中,若爷是根结点,那么最后还需要把爷染成黑色,不然就违反了性质 ② 。
以上是父结点在右侧的情况(即 RR 和 RL),下图是父结点在左侧(即 LR 和 LL)的情况:

下面给出两个 RBT 的插入操作的示例,大家可以尝试着自己画一下:

示例(I):

示例(II):

3)RBT的删除Erase【选学】

红黑树的插入操作容易导致连续的两个红结点,从而破坏性质 ④ 。而删除操作容易造成子树黑高的变化(删除黑结点会导致根结点到叶结点间的黑结点数量减少),从而破坏性质 ⑤ 。

删除过程也是先执行二叉查找树的删除方法。若待删结点有两个孩子,则不能直接删除,而要找到该结点的中序后继(或前驱)进行填补,即右子树中最小的结点(或左子树中最大的结点),然后转换为删除该后继结点。由于后继(或前驱)结点至多只有一个孩子,这样就转换为待删结点是终端结点或仅有一个孩子的情况。

最终,删除一个结点有以下 2 种情况:

① 待删结点只有右子树或左子树(非终端结点)

如果待删结点只有右子树或左子树,则只有以下两种情况,如下图所示:
【子树只有一个结点,且该结点必然是红色,否则会破坏性质 ⑤ 。】

② 待删结点没有孩子(终端结点)

I、如果该待删结点是红色的,则直接删除,无须做任何调整。
II、如果该待删结点是黑色的,又分为以下 4 种情况:

【令 x 是替代待删结点的结点(因为待删结点是终端结点,所以 x 是黑色的 NULL 结点),不平衡的类型可以根据 x 的父结点 x.p 和兄弟结点 w 的特点来划分。如果待删结点是黑色的,那么 w 就肯定不是外部结点(即待删结点的父结点一定有另一个子结点,否则会违背性质 ⑤)。】

  • 当 x 是 x.p 的右孩子时,不平衡是 R 型的,否则是 L 型的(接下来都以 R 型不平衡为例,L 型不平衡与 R 型不平衡是对称的)。
  • 如果 w 是一个黑色结点,那么不平衡是 Lb 或 Rb 型的;而如果 w 是红色结点时,不平衡是 Lr 或 Rr 型的。
  • 根据 w 的红色孩子的数量,把 Rb 型不平衡有细分为三种情况:Rb0 型 、Rb1 型 和 Rb2 型;根据 w 的右孩子中红色孩子的数量,把 Rr 型不平衡有细分为三种情况:Rr0 型 、Rr1 型 和 Rr2 型。

a)当不平衡类型是 Rb0 型时:
(R:x 为父结点的右孩子,b:x 的兄弟结点 w 是黑色,0:w 的红色孩子的数量为0。那么可以分为父结点 x.p 是红色或是黑色 2 种情况)

当不平衡类型是 Rb0 时,需要进行颜色改变的操作。上图给出了 x.p 颜色的两种可能的改变。

若改变颜色前父结点 x.p 是黑色的,颜色改变后将导致以 x.p 为根的子树缺少一个黑色结点。如果 x.p 是整棵 RBT 的根,则就不需要再做其他工作。否则,x.p 就成新的 x,x 的不平衡需要重新划分,并且在新的 x 点需要再进行调整。

若改变颜色前父结点 x.p 是红色的,则从根到 x 的外部结点的路径上,黑色结点数量增加了一个,而从根到 w 的外部结点的路径上,黑色结点数量没有改变。整棵树达到平衡。

下图情况 4 是 Lb0的不平衡:

b)当不平衡类型是 Rb1 型和 Rb2 型时:
(当不平衡类型是 Rb1 和 Rb2 时,需要进行旋转,可以分为以下 3 种情况。如下图所示。)

上图所示的紫色结点表示该结点既可能是红色,也可能是黑色。该结点的颜色在旋转后不会发生变化。因此,上图中子树的根在旋转前和旋转后的颜色保持不变。

可以证明,在旋转后,从根至 x 的外部结点的路径上,黑色结点的数量增加一个,而从根至其他外部结点路径上,黑色结点的数量没有变化。该旋转使树恢复了平衡。

下图情况 2 是 Lb1Lb2的不平衡,情况 3 是 Lb1的不平衡:


c)当不平衡类型是 Rr0 型、Rr1 型和 Rr2 型时:
(由于 x 中缺少一个黑色结点并且结点 w 是红色的,所以 wL 和 wR 都至少有一个黑色结点不是外部结点,因此, w 的孩子都是内部结点。根据 w 的右孩子 wR 中红色孩子的数量(0, 1 或 2),可以把 Rr 型不平衡划分为 3 种情况。这 3 种情况都可以通过一次旋转来获得平衡。这次旋转如下图所示。)

Rr0 型:

Rr1 型:

Rr2 型:

下图情况 1 是 Lr 型的不平衡:

4. RBT的复杂性分析

在插入或删除之后,为了恢复 RBT 的平衡,需要回到根结点至插入或删除结点的路径上来。如果一个结点除数据、左孩子、右孩子和颜色域外,还有一个双亲域,那么这种回溯很容易实现。增加双亲域的一个替代方案是:在从根结点至插入或删除节点路径上,把遇到的每个结点的指针保存到一个栈里。

通过从栈中删除指针,就可以返到根结点。对于一个 n 元素的 RBT,增加双亲域使空间需求增加了 O(n) ,而栈方法使空间需求增加了 Θ(Iogn) 。虽然栈方法在空间上很节省,但双亲域方法的运行速度更快。

在插入或删除之后,结点颜色的改变是沿着从插入或删除结点到根的方向进行的,因此时间为 O(logn) 。另一方面,每次插入(或删除)操作之后,最多需要一次旋转,就可以恢复树的平衡。每次颜色改变或旋转操作需要的时间是 Θ(1),因此插入(或删除)操作需要的总时间是 O(logn) 。

下表总结了各种搜索树的渐进时间性能:

平衡搜索树部分的内容第一次学会有点吃力,建议结合b站上的一些高播放量视频进行学习。

  • 14
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值