红黑树是一种自平衡二叉查找树,具有在最坏情况下查找、插入、删除O(log2n)的复杂度。红黑树中从根节点到任意一叶子节点的最长路径不超过最短路径的两倍,因此是一种近似平衡的二叉树。
节点信息
红黑树的节点具有如下信息:
struct RBNode{ int data; //数据 int color; //颜色,红或者黑 RBNode* parent; //父节点 RBNode* child[2]; int child_dir; //指示当前节点是其父节点的左还是右子节点 RBNode(int d){ data = d; color = RED; parent = NULL; child[0] = child[1] = NULL; } };
红黑树性质
红黑树满足如下性质:
- 每个节点或者是黑色或者是红色
- 树的根节点为黑色
- 树的叶节点(NIL)为黑色(为了表示方便红黑树中所有有效叶子节点<含有数据的叶节点>都添加了额外的叶子节点为NIL, 这里是指NIL为黑色)
- 如果一个节点是红色,则它的两个子节点均为黑色
- 对每个节点,从该节点到其子孙叶节点上的所有路径上包含有相同数目的黑节点
对于满足如上条件的红黑树,则有以下引理:
一棵有n个内节点的红黑树的高度至多为 2*log2(n+1)
因此红黑树的操作复杂度最坏为 O(log2n)
红黑树的旋转
和AVL树、Splay树、Treap一样,红黑树实现其平衡性也是通过节点的旋转(以及对节点重新染色)来实现。红黑树的旋转具有左旋和右旋两种,和伸展树中的旋转相同,具体见树的旋转。
红黑树的插入以及维护
红黑树的插入和二叉查找树的插入相同,都是通过二叉树的性质找到待插入节点应该被插入的位置,然后插入。不过,由于红黑树具有之前提到的五点性质的要求,因此,还需要对红黑树进行额外的维护操作。
插入之后,可能违反性质2、4,因此需要维护:
(1)如果被插入的节点为根节点,则违反性质2,因此直接将 根节点的color改为BLACK即可
(2)如果被插入的节点的父节点为黑色,则不违反任何规则,直接返回
(3)如果被插入节点的父节点为红色,且祖父节点(肯定)为黑色,此时细分三种情况:
* (a) 被插入节点的祖父节点的另一个子节点(即被插入节点的叔叔节点)为红色
* (b) 被插入节点的叔叔节点为黑色,且被插入节点为其父节点的右子节点
* (c) 被插入节点的叔叔节点为黑色,且被插入节点为其父节点的左子节点
对于(3.a),则
被插入节点的祖父节点的另一个子节点(即被插入节点的叔叔节点)为红色
将当前节点的祖父节点改为红色,当前节点的父节点和叔叔节点改为黑色,同时当前节点设为其祖父节点,递归执行InsertFix
之前
之后
对于(3.b),则
被插入节点的叔叔节点为黑色,且被插入节点为其父节点的右子节点
当前节点和其父节点执行左旋,父节点被旋转到下方,当前节点更新为下方的原父节点
之前
之后
对于(3.c),则
被插入节点的叔叔节点为黑色,且被插入节点为其父节点的左子节点
父节点变为黑色,祖父节点变为红色,祖父节点和父节点进行右旋
之前
之后
这样经过以上的旋转染色操作,可以保证红黑树的性质2、4、5成立。
红黑树的删除与维护
红黑树删除入和二叉查找树的删除相同,都是通过二叉树的性质找到待删除节点的位置,如果待删除节点没有子节点,则直接删除,并用空节点顶替该节点;如果待删除节点只有一个非空子节点,则用该非空子节点顶替待删除的节点;如果待删除节点两个节点均非空,则找到该节点的后继节点,交换该节点和其后继节点的数据,再次调用Delete操作。这样,再次调用后,找到原来节点的后继节点,此时该后继节点必定至多只有一个非空子节点,因此在前面所列举的情况下可以解决。
不过,由于红黑树具有之前提到的五点性质的要求,因此,还需要对红黑树进行额外的维护操作,维护操作从顶替被删除的节点的节点处开始。
如果被删除的节点为黑色,则删除之后,可能会违反红黑树的性质5,因此需要维护:设顶替被删除节点的节点为当前节点
(1) 如果当前节点为红色,则直接将该节点变成黑色,返回即可
(2) 如果当前节点是黑色,且为根节点,则直接返回即可
(3) 如果当前节点为黑色,且非根节点,细分为如下四种情况:
(a) 当前节点是黑色,且兄弟节点为红色(此时其父节点和兄弟节点的子节点必定为黑色)
(b) 当前节点为黑色,且兄弟节点为黑色,且兄弟节点的两个子节点均为黑色
(c) 当前节点为黑色,且兄弟节点是黑色,兄弟节点的左子结点为红色,右子节点为黑色
(d) 当前节点为黑色,且兄弟节点为黑色,兄弟节点的右子节点为红色,兄弟节点的左子结点颜色任意
对于情形(3.a)
当前节点是黑色,且兄弟节点为红色(此时其父节点和兄弟节点的子节点必定为黑色), 则
把父节点变成红色,兄弟节点变成黑色,然后对父节点和兄弟节点执行左或右旋操作,之后重新进入算法。
之前
之后
对于情形(3.b)
当前节点是黑色,且兄弟节点为黑色,且兄弟节点的两个子节点全为黑色
把当前节点的兄弟节点变成红色,然后当前节点变为其父节点
之前
之后
由于被删除节点为黑色,此时当前节点的那条路径上的黑色节点的个数就减少1, 不满足性质5,此时将当前节点的兄弟节点染成红色,当前节点提升为其父节点。此时,从当前节点向下的路径上,黑色节点的个数相同,但是和从更高节点到叶节点的路径相比,黑色节点个数仍然少1.
对于情形(3.c)
当前节点为黑色,且兄弟节点是黑色,兄弟节点的左子结点为红色,右子节点为黑色
把兄弟节点变成红色,兄弟节点的左子结点变成黑色,然后对兄弟节点及其左子结点执行右旋操作
之前
之后
对于情形(3.d)
当前节点为黑色,且兄弟节点为黑色,兄弟节点的右子节点为红色,兄弟节点的左子结点颜色任意 把兄弟节点染成当前节点父节点的颜色,把当前节点父节点染成黑色,兄弟节点右子节点染成黑色,之后对当前节点父节点执行左旋操作
之前
之后
实现(伪码)
RB-INSERT-FIXUP(T, z) while z.p.color == RED do if z.p == z.p.p.left then y ← z.p.p.right if y.color == RED then 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 then 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 while x ≠ root[T] and color[x] = BLACK do if x = left[p[x]] then w ← right[p[x]] if color[w] = RED then color[w] ← BLACK ▹ Case 1 color[p[x]] ← RED ▹ Case 1 LEFT-ROTATE(T, p[x]) ▹ Case 1 w ← right[p[x]] ▹ Case 1 if color[left[w]] = BLACK and color[right[w]] = BLACK then color[w] ← RED ▹ Case 2 x ← p[x] ▹ Case 2 else if color[right[w]] = BLACK then color[left[w]] ← BLACK ▹ Case 3 color[w] ← RED ▹ Case 3 RIGHT-ROTATE(T, w) ▹ Case 3 w ← right[p[x]] ▹ Case 3 color[w] ← color[p[x]] ▹ Case 4 color[p[x]] ← BLACK ▹ Case 4 color[right[w]] ← BLACK ▹ Case 4 LEFT-ROTATE(T, p[x]) ▹ Case 4 x ← root[T] ▹ Case 4 else (same as then clause with "right" and "left" exchanged) color[x] ← BLACK
和AVL比较
红黑树并不追求“完全平衡”——它只要求部分地达到平衡要求,降低了对旋转的要求,从而提高了性能。
红黑树能够以O(log2n) 的时间复杂度进行搜索、插入、删除操作。此外,由于它的设计,任何不平衡都会在三次旋转之内解决。当然,还有一些更好的,但实现起来更复杂的数据结构 能够做到一步旋转之内达到平衡,但红黑树能够给我们一个比较“便宜”的解决方案。红黑树的算法时间复杂度和AVL相同,但统计性能比AVL树更高。