红黑树和2-3树
参考资料:Algorithms, 4th
背景:红黑树来源于2-3树。
解决的问题:2-3树通过代码难实现。2-3树作为平衡二叉树在树高方面有优势,但是在插入操作时维护树结构的操作过于繁琐,导致时间复杂度没有明显优势。
方法:为节点间的links添加颜色,用node和link对2-node和3-node进行编码。在代码实现中用links所指向的节点的颜色代表links的颜色。
既然红黑树只是对2-3树的模拟,那么先要懂得2-3树。
2-3树定义和插入
节点类型和树的定义
Definition. A 2-3 search tree is a tree that is either empty or
- A 2-node, with one key (and associated value) and two links, a left link to a 2-3 search tree with smaller keys, and a right link to a 2-3 search tree with larger keys
- A 3-node, with two keys (and associated values) and three links, a left link to a 2-3 search tree with smaller keys, a middle link to a 2-3 search tree with keys between the node’s keys, and a right link to a 2-3 search tree with larger keys
As usual, we refer to a link to an empty tree as a null link.
节点类型
- 2-node。有1个key和2条link,2条link分别指向2棵2-3搜索树,左子树的key更小,右子树的key更大。
- 3-node。有2个key和3条link,3条link分别指向3棵2-3搜索树,左子树的key比2个key都小,中子树的key在2个key之间,右子树的key比2个key都大。
树定义
2-3搜索树是空树,2-node,或者3-node。按照惯例,将空树视作null link。
插入
将key插入2-node
2-node变成3-node,2个key的顺序视具体情况而定。
将key插入3-node
-
插入独立的3-node:
将4-node拆分成3个2-node。
-
插入父节点为2-node的3-node
将4-node的中间key上移到父节点。
-
插入父节点为3-node的3-node
将4-node的中间key上移到父节点,父节点成为4-node,此时父节点符合情况2或情况3。
根节点分裂
若根节点成为4-node,则将4-node拆分成3个2-node。
规律总结
- (对于理解红黑树很重要)直接插入只可能发生在2-3树的叶节点。
- 树高增加的原因只有一个,就是根节点分裂。
时间复杂度
Proposition F. Search and insert operations in a 2-3 tree with N keys are guaranteed to visit at most log N nodes.
Proof: The height of an N-node 2-3 tree is between ⎣log3 N⎦ = ⎣(log N)/(log 3)⎦ (if the tree is all 3-nodes) and ⎣log2 N⎦ = ⎣(log N)/(log 2)⎦(if the tree is all 2-nodes).
红黑树定义和插入
在红黑树中表示3-node
2种link
- red link,链接2个2-node表示3-node。
- black link,普通的link,将2-3树结合起来。
3-node在红黑树中的表示
具体的说,通过左倾斜的red link链接2个2-node,组成一个3-node。
private class Node
{
private static final boolean RED = true;
private static final boolean BLACK = false;
Key key; // key
Value val; // associated data
Node left, right; // subtrees
int N; // # nodes in this subtree
boolean color; // color of link from parent to this node
Node(Key key, Value val, int N, boolean color)
{
this.key = key;
this.val = val;
this.N = N;
this.color = color;
}
private boolean isRed(Node x)
{
if (x == null) return false;
return x.color == RED;
}
}
红黑树的等价定义
An equivalent definition. Another way to proceed is to define red-black BSTs as BSTs having red and black links and satisfying the following three restrictions:
■ Red links lean left.
■ No node has two red links connected to it.
■ The tree has perfect black balance : every path from the root to a null link has the
same number of black links
翻译如下:
红黑树的red link和black link满足以下3种限制:
- red link只能左倾
- 没有节点能够被2条red link同时连接
- 树满足黑平衡:每一条从根节点到null link的路径都有同样数量的black link
推论:不可能存在一个节点被2条或以上red link同时连接。
颜色的表示
For convenience, since each node is pointed to by precisely one link (from its parent), we encode the color of links in nodes, by adding a boolean instance variable color to our Node data type, which is true if the link from the parent is red and false if it is black. By convention, null links are black.
翻译如下:
因为每个node都被唯一的link所指向,因此可以将link的颜色编码在它所指向的node中。
维护树平衡的操作
- 旋转(被旋转的边是red link,旋转后不改变黑高度)
- 翻转颜色(前提条件是2个孩子节点颜色相同且和父节点不一样,此操作改变黑高度)
旋转
分为左旋和右旋2种。(2个节点,3棵子树)
返回父节点的link旋转后应该指向的节点。
Node rotateLeft(Node h)
{
Node x = h.right;
h.right = x.left;
x.left = h;
x.color = h.color;
h.color = RED;
x.N = h.N;
h.N = 1 + size(h.left) + size(h.right);
return x;
}
Node rotateRight(Node h)
{
Node x = h.left;
h.left = x.right;
x.right = h;
x.color = h.color;
h.color = RED;
x.N = h.N;
h.N = 1 + size(h.left) + size(h.right);
return x;
}
旋转的意义:维护了BST顺序性和黑平衡性。接下来,在插入的过程中,可以通过旋转纠正插入key后导致的连续red link和右倾斜 red link。
翻转颜色
这里插播一个概念:翻转颜色。当某个节点的左右孩子节点同时为红色时,违反红黑树的定义,翻转孩子节点及当前节点的颜色。
void flipColors(Node h)
{
boolean newColor = h.color == RED ? BLACK : RED;
h.color = newColor;
h.left.color = !newColor;
h.right.color = !newColor;
}
插入
由2-3树可知,插入node只可能发生在叶子节点。
插入2-node
既是叶子节点,也是根节点(刚开始的时候会出现)
- 插入到右子树,出现右倾斜red link,左旋。
- 插入到左子树。
结果:2-node变成了3-node。
是叶子节点,不是根节点(一般情况)
同理。
插入3-node
- 插入值大于2个key。2条link翻转颜色。
- 插入值小于2个key。右旋第二个key,2条link翻转颜色。
- 插入值在2个key之间。左旋第一个key,右旋第二个key,2条link翻转颜色。
- 第2,3两点中提到的第几个key,说的是大小顺序。旋转的目的总是令4-node的中间的key上升到父节点。
将red link传递到树的上层(也是为了维持黑高度)
每次插入3-node必定翻转颜色,将red link传递给父节点,相当于为父节点增加了一个新的子节点。
这一点非常重要,从调整红黑树的角度,它将2-3树中的直接插入和上移统一到了增加一个新的子节点这个概念里。
保持根节点为black
在插入node之后维护红黑树的过程中,根节点的颜色可能被翻转为红色。这意味着根节点是某个3-node的一部分,但事实并非如此,因此在最后应该将根节点的颜色调整为黑色,这意味着黑高度增加。
维护红黑树的总结
We can accomplish the insertion by performing the following operations, one after the other, on each node as we pass up the tree from the point of insertion:
■ If the right child is red and the left child is black, rotate left.
■ If both the left child and its left child are red, rotate right.
■ If both children are red, flip colors.
- 右孩子红,左孩子黑,左旋。
- 左孩子和左孙子都红,右旋。
- 左右孩子都红,翻转颜色。
public class RedBlackBST<Key extends Comparable<Key>, Value>
{
private Node root;
private class Node // BST node with color bit (see page 433)
private boolean isRed(Node h) // See page 433.
private Node rotateLeft(Node h) // See page 434.
private Node rotateRight(Node h) // See page 434.
private void flipColors(Node h) // See page 436.
private int size() // See page 398.
public void put(Key key, Value val)
{
// Search for key. Update value if found; grow table if new.
root = put(root, key, val);
root.color = BLACK;
}
private Node put(Node h, Key key, Value val)
{
if (h == null) // Do standard insert, with red link to parent.
return new Node(key, val, 1, RED);
int cmp = key.compareTo(h.key);
if (cmp < 0) h.left = put(h.left, key, val);
else if (cmp > 0) h.right = put(h.right, key, val);
else h.val = val;
if (isRed(h.right) && !isRed(h.left)) h = rotateLeft(h);
if (isRed(h.left) && isRed(h.left.left)) h = rotateRight(h);
if (isRed(h.left) && isRed(h.right)) flipColors(h);
h.N = size(h.left) + size(h.right) + 1;
return h;
}
}
由于在算法第四版中,红黑树是由于键值对搜索而出现的,所以既包含了key,也包含了value,因此专门改造出一个只含key的版本。
class RedBlackBST {
private static final boolean RED = true;
private static final boolean BLACK = false;
private Node root;
public void put(int key) {
root = put(root, key);
root.color = BLACK;
}
//插入指定根节点的树并返回新的根节点
public Node put(Node h, int key) {
if(h == null) {
return new Node(key, RED, 1);
}
if(key == h.key) {
return h;
}
if(key < h.key) {
h.left = put(h.left, key);
} else if(key > h.key) {
h.right = put(h.right, key);
}
if(isRed(h.right) && !isRed(h.left)) {
h = rotateLeft(h);
}
if(isRed(h.left) && isRed(h.left.left)) {
h = rotateRight(h);
}
if(isRed(h.left) && isRed(h.right)) {
flipColors(h);
}
h.N = size(h.left) + size(h.right) + 1;
return h;
}
private rotateLeft(Node h) {
Node x = h.right;
h.right = x.left;
x.left = h;
//注意颜色
x.color = h.color;
h.color = RED;
x.N = h.N;
h.N = size(h.left) + size(h.right) + 1;
return x;
}
private rotateRight(Node h) {
Node x = h.left;
h.left = x.right;
x.right = h;
x.color = h.color;
h.color = RED;
x.N = h.N;
h.N = size(h.left) + size(h.right) + 1;
return x;
}
private void flipColors(Node h) {
h.color = RED;
h.left.color = BLACK;
h.right.color = BLACK;
}
private static class Node {
int key;
boolean color;
int N;
Node left;
Node right;
public Node(int key, boolean color, int N) {
this.key = key;
this.color = color;
}
private boolean isRed(Node x) {
if(node == null) {
return false;
}
return x.color == RED;
}
private int size(Node x)
{
if (x == null) {
return 0;
} else {
return x.N;
}
}
}
}
2-3树删除
删除最值(以最小值为例)
观察:
- 最小值肯定是最低层的最左节点。
- 若被删除节点是3-node,直接删除即可。
- 若被删除节点是2-node,删除后破坏平衡。
重要结论: 为了被删除的节点不是2-node,所以我们必须保证当前遍历节点不是2-node(可以是3-node或临时的4-node)。
-
根节点调整规则:
-
若根节点和2个孩子都是2-node,将3个2-node合并成1个4-node。
2. 否则,在有需要的情况下,向右孩子借一个最小key,从而保证左孩子不是2-node。 -
顺着左链接向下查找最小值过程中,调整节点的规则:
-
若左孩子为不是2-node,不变。
-
若左孩子是2-node,右孩子不是2-node,左孩子借右孩子最小key(向父节点借最小key组成3-node,右孩子最小key往父节点补位)。
-
若左孩子是2-node,右孩子是2-node,左孩子借右孩子的唯一key和父节点最小key(父节点的最小key和右孩子的唯一key和左孩子节点组成4-node)。
-
-
直到底部,最小值所在节点一定是3-node或4-node,直接删除最小值即可。
-
向上调整,拆分无用的4-node。
删除指定值
- 向下查找指定值。
- 若指定值在底部节点,直接删除。
- 若指定值不在底部节点,将指定值和直接后继交换,当前节点不是2-node,问题转换为删除根节点不是2-node的红黑树的最小值。
- 最后向上调整,拆分无用的4-node,修复右倾,翻转颜色。
红黑树删除
说明:4-node在红黑树中的表示,3个节点组成的红黑树,子节点与父节点以red links相连。
注意:和2-3树的最大不同,红黑树中向下查找时,先将根节点染红,表明根节点是虚拟3-node的最小key。
删除最小值
- 一开始将根节点染红。
- 当前节点是3-node的最小key所在节点,
- 若左孩子的左孩子是红节点(左孩子不是2-node),继续检查左孩子。
- 若左孩子的左孩子是黑节点(左孩子是2-node),右孩子的左孩子是黑节点(右孩子是2-node),翻转当前节点和兄弟节点的颜色(为了不破坏黑高度),三者组成4-nodes(左孩子借右孩子唯一key和父节点最小key,共同组成4-node),继续检查左孩子。
- 若左孩子的左孩子是黑节点(左孩子是2-node),右孩子的左孩子是红节点(右孩子是3-node),翻转当前节点和兄弟节点的颜色(左孩子和父节点用red link相连,相当于2-3树将父节点最小key和左孩子组成3-node),右孩子右旋(右孩子3-node的最小key到父节点补位的第一次上升),自己左旋(右孩子3-node的最小key到父节点补位成功,继续检查左孩子。
- 若左孩子是null,说明当前节点要被删除,返回null。
- 回溯过程中,向上调整,拆分无用的4-node,修复右倾,翻转颜色。
private Node moveRedLeft(Node h) {
// Assuming that h is red and both h.left and h.left.left
// are black, make h.left or one of its children red.
flipColors(h);
if (isRed(h.right.left)) {
h.right = rotateRight(h.right);
h = rotateLeft(h);
}
return h;
}
public void deleteMin() {
if(root == null) {
return;
}
if (!isRed(root.left) && !isRed(root.right)) {
root.color = RED;
}
root = deleteMin(root);
if (!isEmpty()) {
root.color = BLACK;
}
}
private Node deleteMin(Node h) {
if (h.left == null) {
return null;
}
if (!isRed(h.left) && !isRed(h.left.left)) {
h = moveRedLeft(h);
}
h.left = deleteMin(h.left);
return balance(h);
}
删除最大值
- 一开始将根节点染红。
- 是删除最小值的镜像操作,但需要注意略有不同。
- 注意:若当前节点的左孩子是红节点(即当前节点是3-node),镜像操作:若右孩子的左孩子是黑节点(右孩子是2-node),由于红黑树和2-3树的差异:红黑树中3-node的中孩子和右孩子不在同一高度,所以无法直接复现2-3树中右孩子向左孩子借key,为了让他们在同一高度,我们可以右旋当前节点,虽然导致了red link右倾,但是可以在回溯中修复。
- 若当前节点的左孩子是红节点(即当前节点是3-node),右旋(当前节点将变为原来的左孩子);然后若右孩子不是红节点和右孩子的左孩子不是红节点(即右旋了或右孩子不是2-node),继续检查右孩子(这种情况的右旋是因为未知左孩子是不是3-node)。
- 若当前节点的左孩子是红节点(即当前节点是3-node),右旋(当前节点将变为原来的左孩子);然后若右孩子不是红节点和右孩子的左孩子不是红节点(即没有右旋和右孩子是2-node),翻转颜色(为了右旋而不破坏黑高度);若左孩子的左孩子是红节点(左孩子是3-node),右旋(当前节点变化原来的右孩子:相当于右孩子补位父节点,父节点和左孩子组成3-node),继续检查右孩子(可以留意到新形成的3-node的red link右倾,后面可以省一次右旋)。
- 若当前节点的左孩子是红节点(即当前节点是3-node),右旋(当前节点将变为原来的左孩子);然后若右孩子不是红节点和右孩子的左孩子不是红节点(即没有右旋和右孩子是2-node),翻转颜色(为了右旋而不破坏黑高度);若左孩子的左孩子不是红节点(左孩子是2-node,这种情况下,前面的翻转颜色相当于将当前节点和左右孩子合成4-node),继续检查右孩子(红黑树中4-node的最右边2个孩子在同一高度)。
- 若左孩子的左孩子是红节点(左孩子是3-node),右旋(当前节点变化原来的右孩子:相当于右孩子补位父节点,父节点和左孩子组成3-node),继续检查右孩子(可以留意到新形成的3-node的red link右倾,后面可以省一次右旋)。
- 若右孩子是null,说明当前节点被删除,返回null。
- 回溯过程中,拆分无用的4-node,修复右倾,翻转颜色。
private Node moveRedRight(Node h) {
// Assuming that h is red and both h.right and h.right.left
// are black, make h.right or one of its children red.
flipColors(h)
if (!isRed(h.left.left)) {
h = rotateRight(h);
}
return h;
}
public void deleteMax() {
if(root == null) {
return;
}
if (!isRed(root.left) && !isRed(root.right)) {
root.color = RED;
}
root = deleteMax(root);
if (!isEmpty()) {
root.color = BLACK;
}
}
private Node deleteMax(Node h)
{
if (isRed(h.left)) {
h = rotateRight(h);
}
if (h.right == null) {
return null;
}
if (!isRed(h.right) && !isRed(h.right.left)) {
h = moveRedRight(h);
}
h.right = deleteMax(h.right);
return balance(h);
}
删除指定值
- 染红根节点。
- 若指定值小于当前值,使用删除最小值的调整方法使下一节点不是2-node。
- 若指定值大于当前值,使用删除最大值的调整方法使下一节点不是2-node。
- 若指定值是指定值,用当前值的直接前继(中序遍历的前继)替换当前值,然后删除当前节点的右子树的最小值。
public void delete(Key key) {
if (!isRed(root.left) && !isRed(root.right)) {
root.color = RED;
}
root = delete(root, key);
if (!isEmpty()) {
root.color = BLACK;
}
}
private Node delete(Node h, Key key) {
if(h == null) {
return null;
}
if (key.compareTo(h.key) < 0) {
if (!isRed(h.left) && !isRed(h.left.left)) {
h = moveRedLeft(h);
}
h.left = delete(h.left, key);
} else {
if (isRed(h.left)) {
h = rotateRight(h);
}
if (key.compareTo(h.key) == 0 && (h.right == null)) {
return null;
}
if (!isRed(h.right) && !isRed(h.right.left)) {
h = moveRedRight(h);
}
if (key.compareTo(h.key) == 0) {
h.val = get(h.right, min(h.right).key);
h.key = min(h.right).key;
h.right = deleteMin(h.right);
} else {
h.right = delete(h.right, key);
}
}
return balance(h);
}