红黑树
红黑树(Red Black Tree) 是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构。下面对红黑树,以及二叉查找树等相关概念作一个总结。
介绍
二叉查找树
所谓二叉查找树,可提供对数时间的元素插入和访问,二叉查找树的节点放置规则是:任何节点的键值一定大于其左子树中每一个节点的键值,并小于其右子树的每一个节点的键值。因此,从根节点一直往左走,指定无左路可走,即得最小元素;从根节点一直往右走,直至无右路可走,即得最大元素。
AVL树
AVL树是一棵“加了额外平衡条件”的二叉搜索树。其平衡条件的建立时为了确保整棵树的深度为O(lgN);
AVL树要求任何节点的左右子树高度相差最多为1.
由于在插入和删除节点后,AVL可能会失去平衡,所以,此时需要对AVL树进行必要的旋转,使之保持平衡。
二叉搜索树的旋转
树的旋转分为左旋和右旋
- 左旋:当在某个节点x上做左旋时,假设它的右孩子为y而不是T.nil; x可以为其右孩子不是T.nil节点的树内任意节点。左旋以x到y的链为“支轴”进行。它使y成为该子树新的根节点,x成为y的左孩子,y的左孩子成为x的右孩子。
在LEFT-ROTATE的伪代码中, 假设x.right != T.nil且根节点的父节点为T.nil
LEFT-ROTATE(T, x)
y = x.right; //set y
x.right = y.left; //turn y's left subtree int x's right subtree
if y.left != T.nil
y.left.p = x;
y.p = x.p; //link x's parent to y
if x.p == T.nil
T.root = y;
else if x == x.p.left
x.p.left = y;
else x.p.right = y;
y.left = x; //put x on y's left
x.p = y;
下图给出了一个LEFT-ROTATE操作修改二叉搜索树的例子。RIGHT-ROTETE操作的代码是对称的。LEFT-ROTATE和RIGHT-ROTATE都在O(1)的时间内运行完成。在选择操作中只有指针改变,其他属性都保持不变。
红黑树
定义
红黑树是一棵二叉搜索树,它在每个节点上增加了一个存储位来表示节点的颜色,可以是RED或者BLACK。通过对任何一条从根到叶子的简单路径上各个节点的颜色进行约束,红黑树确保没有一条路径会比其他路径长出2倍,因而是近似于平衡的。
树中每个节点包含5个属性:color,key,left,right和p。如果一个节点没有子节点或父节点,则该节点相应指针属性值为NIL。我们可以把这些NIL视为指向二叉搜索树的叶节点(外部节点)的指针,而把带有关键字的节点视为树的内部节点。
性质
一棵红黑树是满足下面红黑性质的二叉搜索树:
- 每个节点或是红色的,或是黑色的
- 根节点是黑色的
- 每个叶节点(NIL)是黑色的
- 如果一个节点是红色的,则它的两个子节点都是黑色的
- 对于每个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数据的黑色节点
黑高:从某个节点x出发(不含有该节点)到达一个叶节点的任意一条简单路径上的黑色节点个数称为该节点的黑高,记为bh(x)。根据性质5,黑高的概念是明确的,因为从该节点出发的所有下降到其叶节点的简单路径的黑节点个数都相同。于是定义红黑树的黑高为其根节点的黑高。
引理:一棵有n个内部节点的红黑树的高度至多为2lg(n+1).
插入
直接插入
如果要往红黑树中插入一个节点z,首先要找到节点z插入的位置,然后进行插入,最后应该将节点z着为红色。(为了满足性质5,所以新插入点为红色)。最后调整红黑树使之重新满足相关性质
RB-INSERT(T, z)
y = T.nil;
x = T.root;
while x != T.nil
y = x;
if z.key < x.key
x = x.left;
else x = x.right;
z.p = y;
if y == T.nil
T.root = z;
else if z.key < y.key
y.left = z;
else y.right = z;
z.left = T.nil;
z.right = T.nil;
z.color = RED;
RB-INSERT-FIXUP(T, z);
插入修复
红黑树的插入可能出现的情况如下:
特殊情况:
- 如果插入的是根节点,因为原树是空树,此情况只会违背性质2,所以直接将此节点涂黑即可。
- 如果插入的父节点是黑色,由于此不会破坏性质2和性质4,红黑树没有被破坏,此时什么都不用做。
但是当遇到下列三种情况时,需要做出相应调整:
- 如果当前节点的父节点是红色,且祖父节点的另一个节点(叔叔节点)是红色
- 当前节点的父节点是红色,叔叔节点为黑色,此节点是父节点的右子
- 当前节点的父节点是红色,叔叔节点是黑色,此节点是父节点的左子
RB-INSERT-FIXUP(T, z)
while z.p.color == RED
if z.p == z.p.p.left
y = z.p.p.right;
if y.color == RED
z.p.color = BLACK; //case 1
y.color = BLACK; //case 1
z.p.p.color = RED; //case 1
z = z.p.p; //case 1
else if z == z.p.right
z = z.p; //case 2
LEFT-ROTATE(T, z); //case 2
z.p.color = BLACK; //case 3
z.p.p.color = RED; //case 3
RIGHT-ROTATE(T, z.p.p); //case 3
else (same as then clause with "right" and "left" exchanged)
T.root.color = BLACK;
对于上面三种情况的对应调整策略如下:
case 1: 当前节点的父节点是红色,且祖父节点的另一个子节点(叔叔节点)是红色:
- 此时,父节点的父节点一定存在,否则插入前就已不再时红黑树
- 与此同时,又分为父节点是祖父节点的左子还是右子,对于对称性,我们只要解释其中一个就可以了,所以,接下来都只考虑父节点是祖父节点的左子的情况。
- 策略:将当前节点的父节点和叔叔节点涂黑,祖父节点涂红,把当前节点指向祖父节点,从新的当前节点重新开始算法。
case2: 当前节点的父节点为红色,叔叔节点为黑色,当前节点是其父节点的右子
- 策略:当前节点的父节点为新的当前节点,以当前节点为支点左旋
case 3: 当前节点的父节点为红色,叔叔节点为黑色,当前节点为其父节点的左子
- 策略:父节点变为黑色,祖父节点变为红色,以祖父节点为支点右旋(当前节点不变)
插入时间复杂度
由于一棵有n个节点的红黑树的高度为O(lgn),因此直接插入的过程需要花费O(lgn)的时间,在RB-INSERT-FIXUP中,仅当情况1发生,然后指针z沿树上升2层,while循环才会重复执行。所以while循环可能被执行的总次数为O(lgn)。因此,RB-INSERT总共花费O(lgn)时间。此外,该程序所做的旋转从不超过2次,因为只要执行了情况2或情况3,while循环就结束了。
删除
二叉搜索树的删除
二叉搜索树的删除,根据待删除的节点按照儿子的个数可以分为以下三种:
- 没有儿子,即为叶子节点,直接把父节点的对应儿子指针置为NULL,完成
- 只有一个儿子,把父节点的相应儿子指针指向该节点的独子,删除该节点,完成
- 有两个儿子,这是最麻烦的情况。此时我们将该节点的直接后继的内容复制到该节点上,之后以同样的方式的删除它的直接后继,它的后继节点不可能是双子非空,因此此传递过程最多只进行一次。
伪代码
- 辅助函数,将节点v迁移到节点u的位置上
RB-TRANSPLANT(T, u, v)
if u.p == T.nil
T.root = v;
else if u == u.p.left
u.p.left = v;
else u.p.right = v;
v.p = u.p;
- 红黑树中直接删除节点后,需要通过改变颜色和执行旋转来恢复红黑性质
RB-DELETE(T, z)
y = z;
y-original-color = y.color;
if z.left == T.nil
x = z.right;
RB-TRANSPLANT(T, z, z.right);
else if z.right == T.nil
x = z.left;
RB-TRANSPLANT(T, z, z.left);
else
y = TREE-MINIMUM(z.right);
y-original-color = y.color;
x = y.right;
if y.p == z
x.p = y;
else
RB-TRANSPLANT(T, y, y.right);
y.right = z.right;
y.right.p = y
RB-TRANSPLANT(T, z, y);
y.left = z.left;
y.left.p = y;
y.color = z.color;
if y-original-color == BLACK
RB-DELETE-FIXUP(T, x)
以下几点需要注意:
- 始终维持节点y为从树中删除的节点或者移至树内的节点。
- 由于节点y的颜色可能改变,变量y-original-color存储了发生变化前的y颜色
- 如果y是黑色,需要调整。如果y是红色,当y删除或移动时,红黑性质仍然保持,原因如下:
- 树中的黑高没有变化
- 不存在相邻的红节点
- 如果y是红色,就不可能是根节点,所以根节点仍旧是黑色
删除后调整
我们从被删除节点后来顶替它的那个节点开始调整,并认为它有额外的一重黑色。
如果是以下情况,恢复比较简单:
- 当前节点是红+黑。解法:直接把当前节点染成黑色,完成。
- 当前节点是黑+黑,且是根节点。解法:什么都不用做,完成。
如果是以下情况,则需要作出相应调整:
- 当前节点是黑+黑,且兄弟节点为红,此时父节点与兄弟节点的子节点为黑
- 当前节点是黑+黑,且兄弟节点为黑,且兄弟节点的两个子节点全部为黑
- 当前节点是黑+黑,且兄弟节点为黑,兄弟左子为红,右子为黑
- 当前节点是黑+黑,且兄弟节点为黑,兄弟右子为红,左子为任意色
RB-DELETE-FIXUP(T, x)
while x != T.root and x.color == BLACK
if x == x.p.left
w = x.p.right;
if w.color == RED
w.color = BLACK; //case 1
x.p.color = RED; //case 1
LEFT-ROTATE(T, x.p); //case 1
w = x.p.right;
if w.left.color == BLACK and w.right.color == BLACK
w.color = RED; //case 2
x = x.p; //case 2
else
if w.right.color == BLACK
w.left.color = BLACK; //case 3
w.color = RED; //case 3
RIGHT-ROTATE(T, w); //case 3
w = x.p.right; //case 3
else
w.color = x.p.color; //case 4
x.p.color = BLACK; //case 4
w.right.color = BLACK; //case 4
LEFT-ROTATE(T, x.p); //case 4
x = T.root; //case 4
else (same as then clause with "right" and "left" exchanged)
x.color = BLACK;
对于相关情况的调整策略,讨论当前节点是其父节点的左子节点,右子类推。
case 1: 当前节点黑+黑,且兄弟节点为红,此时父节点和兄弟节点的子节点必为黑
- 策略:把父节点染成红色,兄弟节点染成黑色,以父节点左旋,之后重新进入算法。次变换后红黑树性质5不变,而是变为把兄弟节点转化为黑色的情况。
case 2: 当前节点是黑+黑,兄弟节点是黑色,且兄弟节点的两个子节点为黑色
- 策略:把当前节点和兄弟节点抽取一重黑色加到父节点上,把父节点变为新的当前节点,重新进行算法。(即把兄弟节点染红,变换当前节点为父节点)
case 3: 当前节点是黑+黑,兄弟节点是黑色,兄弟左子为红,右子为黑
- 策略:把兄弟节点染红,兄弟左子染黑,之后再以兄弟节点为支点右旋,之后重新进入算法
case 4: 当前节点是黑+黑,兄弟节点是黑,兄弟右子为红,左子为任意色
- 策略:把兄弟节点染成当前父节点的颜色,把当前节点父节点染成黑色,兄弟右子染黑,之后再以当前节点的父节点为支点左旋,此时算法结束。
删除的时间复杂度
因为含有n个节点的红黑树的高度为O(lgn),不调用RB-DELETE-FIXUP时该过程的总时间代价为O(lgn)。在RB-DELETE-FIXUP中,情况1、3和4在个执行常数次数的颜色改变和至多3次旋转后终止。情况2是while循环可以重复执行的唯一情况,然后指针x沿树上升至多O(lgn)次,且不执行任何旋转操作。所以,过程RB-DELETE-FIXUP要花费O(lgn)时间,做至多3次旋转,因此RB-DELETE运行的总时间为O(lgn)。
C++实现红黑树
在造STL轮子的时候,有实现过一个红黑树,并给出了相关测试用例,有兴趣的可以看看。
总结
- 红黑树的难点就在于插入和删除部分,重点在于理解不同的情况
- 像学习红黑树这种硬货的时候,还是适合自己动手画画,写写代码。
- 本文参考部分网上资料和算法导论,感谢大佬们!
参考
- 教你初步了解红黑树
- 《算法导论》