红黑二叉树
红黑二叉树的基本思想是想用标准的二叉查找树(完全由2-结点构成)和一些额外的信息(替换3-结点)来表述2-3树。
树中的链接有两种类型:
-
红链接将两个2-结点连接起来构成一个3-结点
-
黑链接则是2-3树中的普通链接
或者说,我们将3-结点表示为由一条左斜的红色链接相(两个2-结点其中之一是另一个的左子节点)连的两个2-结点。
这种表示的优点:我们无需修改就可以直接使用标准二叉查找树的get()方法。对于任意的2-3树,只要对结点进行转换,我们都可以立即派生出一颗对应的二叉查找树。我们用这种方式表示2-3树的二叉查找树称为红黑二叉树。
一种等价的定义
-
红链接均为左链接;
-
没有任何一个结点同时和两条红链接相连;
-
该树是完美黑色平衡的,即任意空连接到根结点的路径上的黑链接数量相同。
满足这样定义的红黑树和相应的2-3树是一一对应的。
一一对应
我们将一颗红黑树中的红链接画平,那么所有的空链接到根结点的距离都将是相同的。如果我们将由红链接相连的结点合并,得到的就是一颗2-3树。
如果将一颗2-3树中的3-结点画作由红色左链接相连的两个2-结点,那么不会存在能够和两条红链接相连的结点,且树必然是完美黑色平衡的。
红黑树即是二叉树,又是2-3树。
颜色表示
每个结点都有一个指向自己的链接,我们将链接的颜色保存在表示结点的Node数据类型的布尔变量color中。如果指向它的链接为红色,则color为true,反之为false。我们约定空链接为黑色。
package cn.chen.tree;
public class RBT <Key extends Comparable<Key>,Value> {
private static final boolean RED = true;
private static final boolean BLACK = false;
private class Node {
private Key key;
private Value value;
private Node left,right;
private boolean color;
public Node(Key key, Value value, Node left, Node right, boolean color) {
this.key = key;
this.value = value;
this.left = left;
this.right = right;
this.color = color;
}
}
private boolean isRed(Node x){
if(x == null) return false;
return x.color == RED;
}
}
旋转
我们实现的某些操作可能会出现红色右链接或者两条连续的红链接,在操作完成前这些情况必须被小心地旋转并修复。旋转操作会改变红链接的指向。
private 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;
}
private 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;
}
插入
1.向单个2-节点插入新键
一棵只含有一个键的红黑树只含有一个2-结点,处理方式如下:
-
如果新键大于老键,我们只需要新增一个红色结点即可,新的红黑树和单个3-结点完全等价;
-
如果新键小于老键,那么新增的红色结点将会产生一条红色右链接。我们需要使用root = rotateLeft(root);来将其旋转为红色左链接并修正根节点的链接,插入操作才算完成。
两种情况均为一棵和单个3-结点等价的红黑树,其中包含两个键,一条红链接,树的黑链接高度为1。
2.向树底部的2-结点插入新建
用和二叉查找树相同的方式向一棵红黑树中插入一个新键会在树的底部新增一个结点,但总用红链接将新结点和它的父结点相连。如果它的父结点是一个2-结点,那么刚才讨论的两种处理方式仍然适用。
-
如果指向新结点的是父结点的左链接,那么父结点就直接成为一个3-结点;
-
如果指向新结点的是父结点的右链接,我们需要一次左旋转来修正它。
3.向一棵双键树(即一个3-结点)中插入新键
这种情况又可以分为三种子情况:新键小于树中的两个键,在两者之间,或者大于两个键。每种情况都会产生一个同时链接到两条红链接的结点。
-
三者中最简单的情况是新键大于原树中的两个键,它被连接到3-结点的右链接。此时树是平衡的,根节点为中间大小的键,它有两条红链接分别和较小较大的结点相连。如果我们将两条链接的颜色都由红变黑,那么我们就得到了一棵由三个结点组成的,高位2的平衡树。它正好对应一棵2-3树。后两种情况也会转化为第一种情况。
-
如果新键小于原树中的两个键,它被连接到最左边的空链接,这样就产生了两条连续的红链接。此时我们只需要将上层的红链接右旋转即可得到第一种情况(中值键位根节点并和其他两个结点用红链接相连)。
-
如果新键位于两个键之间,这也会产生两条连续的红链接。一条红色左链接,一条红色右链接。此时我们只需要将下层的红链接左旋转即可得到第二种情况。
4.颜色转换
这个方法转换一个结点的两个红色子结点的颜色。除了将子结点的颜色由红变黑外,我们还要将父结点的颜色由黑变红。这项操作最重要的性质在于它和旋转操作一样是局部变换,不会影响整棵树的黑色平衡性。
private void flipColors(Node h){
h.color = RED;
h.left.color = BLACK;
h.right.color = BLACK;
}
5.根节点总是黑色
颜色转换会使根节点变为红色。红色的根节点说明根节点是一个3-结点的一部分,但实际情况不是这样。我们在每次插入后都会将根节点设为黑色。注意,每当根节点由红变黑时树的黑链接高度就会加1。
6.向树底部的3-结点插入新键
和3中讨论的三种情况类似。指向新结点的链接可能是3-结点的右链接(转换颜色),左链接(右旋转然后转换颜色),或是中链接(左旋转下层链接然后右旋转上层链接,最后转换颜色)。在颜色转换时使中结点的链接变红,相当于将它送入了父结点。这意味着在父结点中继续插入一个新键。
7.将红链接在树中向上传递
private void put(Key key,Value value){
root = put(root, key, value);
root.color = BLACK;
}
private Node put(Node h,Key key, Value value){
if(h == null) return new Node(key, value,1, RED);
int cmp = key.compareTo(h.key);
if(cmp > 0) h.left = put(h.left, key, value);
else if(cmp < 0) h.right = put(h.right, key, value);
else h.value = value;
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;
}