上一篇中的2-3树的插入算法并不难理解,接下来我们看一种名为红黑二叉查找树的数据结构。
替换3-结点
红黑二叉查找树背后的基本思想是用标准的二叉查找树(完全由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树中的普通链接,根据定义这些链接必然是完美平衡的。无论我们选择用何种方式去定义他们,红黑树都既是二叉查找树,也是2-3树。因此,如果我们能够在保持–对应关系的基础上实现2-3树的插入算法,那么我们就能够将两个算法的优点结合起来:二叉查找树中简洁高效的查找方法和2-3树中高效的平衡插入算法。
颜色表示
方便起见,因为每个结点都只会有一条指向自己的链接(从它的父结点指向它),我们将链接的颜色保存在表示结点的Node数据类型的布尔变量color中。如果指向它的链接是红色的,那么该变量为true,黑色的为false。我们约定空链接为黑色。为了代码的清晰我们定义了两个常亮RED和BLACK来设置和测试这个变量。我们使用提到一个结点的颜色时,我们指的是指向该结点的链接的颜色,反之亦然。
private static final boolean RED = true;
private static final boolean BLACK = false;
public class Node {
Key key; //键
Sampler.Value val; //相关联的值
Node left,right; //左右子树
int N; //这棵子树中的结点数量
boolean color; //由其父结点指向它的链接的颜色
Node(Key key, Sampler.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;
}
旋转
在我们实现的某些操作中可能会出现红色右链接或者两条连续的红链接,但在操作完成前这些情况都会被小心地旋转并修复。旋转操作会改变红链接的指向。首先,假设我们有一条红色的右链接需要被转化为左链接。这个操作叫做左旋转,它对应的方法接受一条指向红黑树中的某个结点作为参数。假设被指向的结点的右链接是红色的,这个方法会对树进行必要的调整并返回一个指向包含同一组键的子树且其左链接为红色的根结点的链接。尾盘,恶魔只需要将两个键中的较小者作为根结点变为较大者作为根结点。实现将一个红色左链接转换为一个红色右链接的代码完全相同,只需要将left换成right即可。
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;
}
在旋转后重置父结点的链接
无论左旋转还是右旋转,旋转操作都会返回一条链接。我们总是用rotateRight()或者rotateLeft()的返回值重置父结点(或者根结点)中相应的链接。返回的链接可能是左链接也可能是右链接,但是我们总会将它赋予父结点中的链接。这个链接可能是红色的也可能是黑色的做反转和有反转都是通过将x.color设为h.color保留它原来的颜色。这可能会产生两条连续的红链接,但我们的算法会继续用旋转操作修正这种情况。
再插入新的键时我们可以使用旋转操作帮助我们保证2-3树和红黑树之间的一一对应关系,因为旋转操作可以保证红黑树的两个重要性质:有序性和完美平衡性。也就是说,我们在红黑树中进行旋转时无需为树的有序性或者完美平衡性担心。
向2-结点中插入新键
一颗只含有一个键的红黑树只含有一个2-结点。插入另一个键之后,我们马上就需要将他们旋转。如果新键小于老键,我们只需要新增一个红色的结点即可,新的红黑树和单个3-结点完全等价。如果新键大于老键,那么新增的红色结点将会产生一条红色的右链接。我们需要使用root = rotateLeft(root);来将其旋转为红色左链接并修正根结点的链接,插入操作才算完成。两种情况的结果均为一颗和单个3-结点等价的红黑树,其中含有两个键,一条红链接,树的黑链高度为1。
向树的底部的2-结点插入新键
永和二叉查找树相同的方式向一颗红黑树中插入一个新键会在树的底部新增一个结点(为了保证有序性),但总是用红链接将新结点和他的父结点相连。如果它的父结点是一个2-结点,那么刚才讨论的两种处理方法仍然适用。如果指向新结点的是父结点的左链接,那么父结点就直接称为了一个3-结点;如果指向新结点的是父结点的右链接,这就是一个错误的3-结点,但一次左旋转就能修正它。
向一颗双键树(即一个3-结点)中插入新键
这种情况又可以分为三种自情况:新键小于树中的两个键;在两者之间;或是大于树中的两个键。每种情况都会产生一个同事链接到两条红链接的结点,而我们的目标就是修正这一点。
- 三者最简单的情况是新键大于原树中的两个键,因此他被连接到3-结点的右链接。此时树是平衡的,根结点为中间大小的键,它有两条红链接分别和较小和较大的结点相连。如果我们将两条链接的颜色都由红变为黑,那么我们就能得到一颗由三个节点组成,高度为2的平衡树。它正好能够对应一颗2-3树。其他两种情况最终也会转化为这种情况。
- 如果新键小于原树中的两个键,他会被连接到最左边的空链接,这样就产生了两条连续的红链接。此时我们只需要将上层的红链接右旋转即可得到第一种情况
- 如果新键介于原树中的两个键之间,这又会产生两条连续的红链接,一条红色左链接接一条红色右链接,此时我们只需要将下层的红链接左旋转即可得到第二种情况。
总的来说,我们通过0次,1次和2次旋转以及颜色的变化得到了期望的结果。