《算法导论》学习分享——13. 红黑树

13. 红黑树


二叉搜索树的各种操作时间复杂度为 O ( h ) \Omicron(h) O(h),但是树的高度h不可控,最差的情况为n。
平衡搜索树能将树的高度控制在 O ( lg ⁡ ( n ) ) \Omicron(\lg(n)) O(lg(n))
常见的平衡搜索树有:

  • AVL树
  • 红黑树
  • treap (tree + heap,给每个元素随机的priority,从而间接的实现随机插入,使得树的期望高度为 O ( lg ⁡ ( n ) ) \Omicron(\lg(n)) O(lg(n))
  • B树 (十八章介绍)
    • 2-3-4树

本章重点讨论红黑树,以及习题中出现的AVL树。AVL树较为简单所以先介绍AVL树。

AVL树

AVL树由Adel’son-Vel’skii & Landis 在1962年发明,由他们的名字命名。
比较一般的二叉搜索树,每个结点额外存储它的高度,nil的高度为0,其他节点的高度为Height(x) = max(Height(x->left), Height(x->right)) + 1。每个结点两个孩子的高度差不能超过1。
当右子树的高度比左子树的高度高时,我们称right-heavy(如下图),反之这称left-heavy。

AVL树

AVL树的各种查询操作都和普通二叉搜索树无异,所以我们只需关注会改变树结构的Insert操作和Delete操作。
在一般的Insert或者Delete操作后,结点的高度会变,所以有可能会违反AVL树的性质,这时候我们需要引入一个操作Rotate(旋转)

Rotate

旋转分为两种,左旋(LEFT-ROTATE)和右旋(RIGHT-ROTATE)。下图右测变为左侧的情况称为对x结点左旋,左侧变为右侧的情况称为对y结点做右旋。

Rotate

我们可以发现旋转操作并不会影响二叉搜索树的性质。
旋转前后都会保持α <= x <= β <= y <= γ
下面是左旋的伪代码,右旋的操作和左旋对称,不再赘述。

Rotate

Ref:AVLTree::LeftRotate & AVLTree::RightRotate in AVLTree.h

AVL树平衡调整(ADJUST_FROM(x))

当Insert或者Delete一个元素后,平衡有可能被破坏,假设x是最底层被破坏性质的节点,而且是right-heavy时(left-heavy是对称的):

  1. Balance
    • 如果x的右孩子y是平衡的或者right-heavy的(Figure 5)
      • 我们对x做LEFT-ROTATE操作可使其恢复平衡
      • 然后重新计算x和y的高度
    • 如果x的右孩子z是left-heavy的(Figure 6)
      • 我们先对z做RIGHT_ROTATE,然后对x做LEFT-ROTATE,可使其恢复平衡
      • 然后重新计算x,y,z的高度
  2. Loop
    • 重新计算x的高度,并使得x=x->p继续循环,直到根节点
    • 若图中发现某节点不平衡的用1. Balance

AVL树

Ref:AVLTree::AdjustFrom in AVLTree.h

AVL树 Insert

因为插入的节点肯定是叶子节点,所以直接对插入节点调用ADJUST_FROM(x)即可。

Ref:AVLTree::Insert in AVLTree.h

AVL树 Delete

删除操作略为复杂,可能会影响子节点的平衡。

  • 当被删除节点只有一个孩子,或者没有孩子时,被删除节点的孩子不会被应用
    • 这时对删除节点原来的父节点调用ADJUST_FROM(x)即可
  • 当被删除节点有两个孩子时
    • 如果被删除节点右子树的最小节点min的父节点为被删除节点,这时对min调用ADJUST_FROM(x)
    • 其他情况,对min原来的父节点调用ADJUST_FROM(x)

Ref:AVLTree::Delete in AVLTree.h

红黑树

红黑树也是一种平衡二叉树。它在每个节点上增加一个存储位color来表示节点的颜色,节点的颜色为RED或者BLACK。通过对任何一条从根节点到叶子节点的颜色进行约束,红黑树确保没有一条路径会比其他路径长出2倍,所以是近似于平衡的。
红黑树还定义一种NIL节点,NIL节点是红黑树的叶子结点(外部节点),真实的数据组成树的内部节点(如下图a所示)。
红黑树的具体性质如下:

  1. 每个节点不是红色的就是黑色的
  2. 根节点是黑色的
  3. 每个叶结点(NIL)是黑色的
  4. 如果一个结点是红色的,那么它的两个孩子都是黑色的
  5. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,黑色结点的数量相同

为了方便处理,使用一个哨兵来替代NIL结点。对于一颗红黑树T,哨兵T.nil是一个普通结点。它的color为BLACK,用哨兵T.nil替换图a中的所有NIL,这样就形如下图b所示,T.nil的p, left, right, key属性我们并不关心,所以方便起见程序中可以对其值随意设定。

红黑树

为了方便起见,我们在图中省去T.nil,这样就变成了图c的样子。

NULL -> T.nil

红黑树与普通二叉搜索树相比,用T.nil替代了空指针。
所以所有操作中用到空指针的地方,需要换成T.nil。后面不做赘述。

红黑树 Insert

红黑树的插入操作,除了一般的操作外,还需要把插入结点的颜色设为RED。因为插入了一个红色的结点,所以有可能触犯红黑树的一些性质(1,4),所以我们要调用RB-INSERT-FIXUP将其修正。

红黑树Insert

红黑树Insert

RB-INSERT-FIXUP会循环的查看z的父结点是否违反性质4,如果违反,则将其修正。如果修正后仍然有可能违反性质4,则继续循环查看其爷结点。(本文只讨论插入结点的父结点为爷结点的左孩子的情况,右孩子的情况与其对称,不再赘述。)

修正过程有三种情况:

  • Case 1. z的叔结点为红色
  • Case 2. z的叔结点为黑色,且z是父亲的右孩子
  • Case 3. z的叔结点为黑色,且z是父亲的左孩子
    红黑树Insert

上图插入4之后的处理过程。下面我们详细讨论下三种情况的处理方式:

  • Case 1
    这种情况只需改变着色,将父节点和叔结点染成黑色,爷结点染成红色。
    因为爷结点以前是黑色,却染成了红色,如果爷结点的父结点为红色,这时仍会触犯性质4,所以我们将爷结点赋给z,下次循环时再修正它。
    如果z的爷结点为root结点,我们修正后root结点变成了红色,并且让z指向root结点然后跳出了循环。在循环外边我们会将z重新染成黑色,所以保持了性质1。
    红黑树Insert

  • Case 2/3
    当z是父结点的左孩子时,符合Case 3,这时我们只需把爷结点做右旋,然后交换之前父结点和爷结点的颜色。这时局部的根节点颜色没有改变,仍然是黑色,所以整个树都满足了红黑树的特性,无需继续循环。
    红黑树Insert
    当z是父结点的右孩子时,符合Case 2,这时只需要对父结点做左旋,使其变为Case 3的情况。

由上可知,一旦循环进入Case 2或者Case 3,就会结束循环。所以一次插入操作最多只会有两次旋转操作。尽量少的循环操作是非常有益的,因为改变结点颜色并不会影响查找功能,当多线程处理时我们只需给旋转操作加锁。

红黑树 Delete

删除操作比插入操作略微复杂,当删除一个黑色结点,或者删除过程中移动了一个黑色结点时,都有可能破坏红黑树的性质(2,4,5)。

当被删除的结点只有一个孩子,或者没有孩子,这时我们只需关心被删除结点的颜色,如果被删除的结点为红色,则无需修正,如果被删除结点为黑色,则需要从被删除结点的孩子开始修正。

当被删除的结点有两个孩子时,我们会找右子树的最小结点来替代被删除结点的位置和颜色,所以这是我们需要关注这个最小结点的颜色,如果最小结点的颜色为红色,则无需修整,如果这个最小结点为黑色,则需要从最小结点的右孩子开始修正。

红黑树Delete

红黑树Delete

从x开始修正时,有五种情况:

  • Case 0. x为红色
    • 这时23行会将其设为黑色,可直接满足所有红黑树性质
  • Case 1. x的兄弟结点w为红色
  • Case 2. x的兄弟节点w为黑色,且w的两个孩子都是黑色
  • Case 3. x的兄弟节点w为黑色,且w的右孩子为黑色,左孩子有红色
  • Case 4. x的兄弟节点w为黑色,且w的右孩子为红色

红黑树Insert

  • Case 1
    这种情况我们将其转换为Case2/3/4

    1. 设置兄弟节点w为黑色,父结点为红色
    2. 左旋父结点
    3. 重新设置w为x的兄弟结点
  • Case 2
    这种情况将兄弟结点w设置为红色,并且x=x.p,继续循环。
    我们会发现,如果是从Case 1转换到Case 2,那么x.p一定是红色,所以会直接退出循环。

  • Case 3
    这种情况我们将其转换为Case4

    1. 设置兄弟节点w为红色,w的左孩子为黑色
    2. 右旋w结点
    3. 重新设置w为x的兄弟结点
  • Case 4
    这种情况可以做一些修改,然后结束修正

    1. 设置w的颜色为x父结点的颜色
    2. x父结点的颜色设置为黑色
    3. w右孩子的颜色设置为黑色
    4. 左旋x的父结点
    5. 使x指向root结点,从而结束修正

由上可知,一旦循环进入Case 1, Case 3,Case 4,就会结束循环。所以一次插入操作最多只会有三次旋转操作(Case 1 -> Case 3 -> Case 4)。

性质维护:

插入过程中维护性质的分析:
Case1 转换后,局部的黑高与之前相同
但是由于将局部根节点C从黑色变成红色,所以需要继续考虑C是否违反性质4
Case2/3 转换后,局部的黑高与之前相同
并且局部根节点B的颜色没有改变,所以直接Done
删除过程中维护性质的分析:

需要修正时,被修正结点x以前的父节点肯定为黑色,x替代了x以前父节点的位置
所以这时这个子树(x为根节点)的黑高减少了1

Case1 转换后,ABCDE的黑高都没有变
所以这时仍然是x为根节点的子树黑高少1

根本情况没有变化,变化的只是x的兄弟节点的颜色
Case2 转换后,以x的父节点c为根节点的子树已经满足红黑树的性质
但是c为根节点的子树的黑高比之前少1

所以令c为新的x,继续调整

Case3 转换后,ABCDE的黑高都没有变
所以这时仍然是x为根节点的子树黑高少1

根本情况没有变化,变化的只是w的右孩子的颜色

Case4 转换后,以c为根节点子树满足红黑树性质
并且根节点的颜色没有变化,子树的黑高也没有变化

所以Done

涉及数据结构

点击查看实现

Github Source: https://github.com/maomao9003/Introduction-to-Algorithms/blob/master/Docs/Chapter/13.红黑树.md

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值