有别于上一篇文章介绍的红黑树,在《算法<第四版>》一书中用另一套规则实现了红黑树,主要手段是递归,实现思路来自于2-3树,这本书中有详细的解读,在这里我谈谈自己对它的理解。
首先,在之前文章中介绍的红黑树,我们把节点看成红,黑两色,而这里红节点指的是它指向父亲的链接是红色的,有什么不同?当我们介绍左旋,右旋你就会看到。
来看看这套定义红黑树的规则:
- 红链接均为左链接
- 没有任何一个节点同时与两个红链接相连
- 该树是完美的黑平衡,即任何空节点到根节点的路径上的黑链接数目相同
与之前红黑树的不同:之前的红节点在左在右都行。
先来看看关于Node的定义:
private class Node {
private int size;
private Key key;
private Value value;
private Node left;
private Node right;
private boolean color;
public Node(Key key, Value value, boolean color, int size) {
this.color = color;
this.key = key;
this.value = value;
this.size = size;
}
}
只有左右两个指针没了之前的parent指针,加了个size用于记录节点个数,规则是x.size = size(x.left) + size(x.right) + 1;
再来介绍三板斧:rotateLeft, rotateRight, flipColors 这三板斧在变换过程中不会破坏黑平衡
rotateLeft
看到区别没有,原来的黑节点变成了红,红链接被转过来了;还有一个要注意的地方就是它的返回,接下来的操作我们会经常看到这样 x = rotateLeft(x); 这个x实际上在变换后变成了他原来的右子节点,是因为没有指向父节点的指针所以用这种方法来替代。 最重要的是这里的旋转不会破坏树的黑平衡
rotateRight
flipColors
// flip the colors of a node and its two children
private void flipColors(Node h) {
// h must have opposite color of its two children
// assert (h != null) && (h.left != null) && (h.right != null);
// assert (!isRed(h) && isRed(h.left) && isRed(h.right))
// || (isRed(h) && !isRed(h.left) && !isRed(h.right));
h.color = !h.color;
h.left.color = !h.left.color;
h.right.color = !h.right.color;
}
flipColor函数不是能够随便调用的,一定要满足以下条件: x及其左右子树都不为null && 左右子树颜色相同,x颜色与他们相反。原因是保证黑平衡不被破坏。
put
插入操作就用到了上面这三板斧,我们将在插入操作中见识到这套规则的魅力
public void put(Key key, Value value) {
if (key == null) throw new IllegalArgumentException("first argument to put() is null");
if (value == null) {
delete(key);
}
root = put(root, key, value);
root.color = BLACK; //根节点为黑
}
private Node put(Node x, Key key, Value value) {
if (x == null) return new Node(key, value, RED, 1);
int cmp = key.compareTo(x.key);
if (cmp < 0) x.left = put(x.left, key, value);
else if (cmp > 0) x.right = put(x.right, key, value);
else x.value = value;
if (isRed(x.right) && !isRed(x.left)) x = rotateLeft(x);
if (isRed(x.left) && isRed(x.left.left)) x = rotateRight(x);
if (isRed(x.left) && isRed(x.right)) flipColors(x);
x.size = size(x.left) + size(x.right) + 1;
return x;
}
就是二叉查找树的插入操作+重平衡递归式的二产查找树实现
递归添加新节点,完成后在回溯过程中对树进行检查修复。修复三种情况:
1. x左黑右红,左旋;
2. x左红,左左红,右旋;
3. x左右都红,flipColor
这样就能够在递归回溯过程中保证一层层往上直到根节点都符合规则,最后要保证根节点为黑。
deleteMin
public void deleteMin() {
if (isEmpty()) throw new NoSuchElementException("the tree is empty");
//为嘛要有这步操作?因为若root的左儿子与左孙子节点皆为黑,则不符合moveRedLeft调用规则。从代码意图上来说:
通过自上而下的将红色传给最小节点,那么在一开始若root.left为黑的情况下,便将root改为红,
这种改动防止了上述的错误情况,最后仍会回复root为黑
if (!isRed(root.left) && !isRed(root.right))
root.color = RED;
root = deleteMin(root);
if (!isEmpty()) root.color = BLACK; //根节点为黑
}
private Node deleteMin(Node x) {
//为什么返回null?它要是有右子树呢?它不可能有右子树,因为我们定义的红黑树,最小节点一定没有子节点
而moveRedLeft()中如果冲突修复操作也不会添加给x右子树
if (x.left == null)
return null;
//为什么判断条件是左儿左孙都为黑才调用moveRedLeft?因为该方法本意就是自上而下传递红色,从而将最
小节点变为红色,这样删除就不会破坏红黑平衡,所以当左儿子或左孙子有一个为红时,就可以跳到那里从那开
始往下传递。moveRedLeft利用三板斧来传递当前节点的红色给左子节点,过程中红黑平衡并未破坏,只是会出
现右链接为红的情况,所以需要在回溯过程中自下而上修复这一情况,即balance()
就管自己的这一亩三分地,然后从上往下递归传递红链接,若存在左儿或左孙为红,那就跳到那去传递它的红链接
if (!isRed(x.left) && !isRed(x.left.left))
x = moveRedLeft(x);
x.left = deleteMin(x.left);
//在我们从上往下改变红黑树的过程中,会出现右链接为红还有两左红链接连一起的情况,
这就需要在递归从下往上过程中进行修复,因为三板斧所以不用担心会破坏平衡
return balance(x);
}
//凡是调用该方法的节点一定是红,且其左右子节点皆为黑。
// 该方法目的是将当前节点的红色传递给左子节点。
private Node moveRedLeft(Node x) {
// assert (h != null);
// assert isRed(h) && !isRed(h.left) && !isRed(h.left.left);
flipColors(x);
// flipColors后x为黑,左右子节点为红,若x的右节点的左节点也为红,
// 则需进行平衡操作
if(isRed(x.right.left)) {
x.right = rotateRight(x.right);
x = rotateLeft(x);
flipColors(x);
}
return x; //注意这里返回值,若进行过平衡操作则不再是原先的节点。
}
// 重新平衡以x为根的子树,该方法在递归回溯过程中不断自下而上修复整个树
private Node balance(Node x) {
if (isRed(x.right) &&!isRed(x.left)) x = rotateLeft(x);
if (isRed(x.left) && isRed(x.left.left)) x = rotateRight(x);
if (isRed(x.left) && isRed(x.right)) flipColors(x);
x.size = size(x.left) + size(x.right) + 1;
return x;
}
//空节点为黑
private boolean isRed(Node x) {
if (x == null) return false;
return x.color == RED;
}
如果以之前的红黑树来思考:如果x为黑,它可能有一个右红子节点,他的父亲(p)的情况,他父亲的兄弟(sib)的情况,还有sib两子节点情况,以及sib左子节点的两个子节点情况。我们需要把每种情况都考虑到,很复杂。不过一切的根源就在于x为黑,那x要是红直接删除就可以了,有没有一种办法保证x一定为红?这就是上面代码做的事,让被删除节点为红。
现在来想想怎么把它变红?
我们之前说过flipColor有它的使用前提来保证不会破坏平衡,旋转操作也不会破坏平衡,那moveRedLeft是干嘛用的?人如其名,就是要把红链接往下传的,但是又不能破坏平衡,于是用三板斧来操作,如上述代码,moveRedLeft用三板斧将当前的红链接转到了他的儿子,这样从上往下到最后最小节点一定为红无论他原来是什么(这里有个前提是空连接视为黑),从上而下改变树,这从上而下的变换结果就会让最小节点是一个红的且没有任何子节点。最后在从下往上回溯中修复树,即balance函数。
deleteMax
思想与deleteMin一致
public void deleteMax() {
if (isEmpty()) throw new NoSuchElementException("tree is null");
if (!isRed(root.left) && !isRed(root.right))
root.color = RED;
root = deleteMax(root);
if (!isEmpty()) root.color = BLACK;
}
private Node deleteMax(Node x) {
//如果x左子节点为红,直接右旋,将红色传到右侧
if (isRed(x.left))
rotateRight(x);
// 最大节点最多有一个左红子节点,这种情况该左子节点会经过上面的右旋操作,使得最后最大节点一定没有
子节点
if (x.right == null)
return null;
// 这种情况下需要传递红色
if (!isRed(x.right) && !isRed(x.right.left))
x = moveRedRight(x);
x.right = deleteMax(x.right);
// 回溯中自下而上修复树
return balance(x);
}
// 该方法的调用需要满足条件:h为红,其右子树即右子树的左子树皆为黑(空节点颜色为黑)
方法目的是在上述情况下,将h的红色传递给右儿子
private Node moveRedRight(Node h) {
// assert (h != null);
// assert isRed(h) && !isRed(h.right) && !isRed(h.right.left);
flipColors(h);
// 若左孙子也为红,则需要解决冲突。处理后h的右子树为黑,右孙子为红
if (isRed(h.left.left)) {
h = rotateRight(h); // 注意h被重新赋值
flipColors(h);
}
return h;
}
主要思想与deleteMin相同,不同在处理手段上。
这里的主要操作就是从根节点开始通过右旋或moveRedRight在从上往下递归过程中不断将红链接传下去(我们定义的红黑树它的最大节点的左右子树情况:最多只可能有一个左红节点),最后最大节点一定是个无子节点的红节点。再通过从下往上的递归过程中balance()红黑树。
delete
之所以上面分析deleteMIn与deleteMax是因为它们会在删除操作中使用到。
public void delete(Key key) {
if (key == null) throw new IllegalArgumentException("argument to delete is null");
if (!contains(key)) return;
// 原因上面分析过
if (!isRed(root.left) && !isRed(root.right))
root.color = RED;
root = delete(root, key);
if (!isEmpty()) root.color = BLACK;
}
private Node delete(Node x, Key key) {
// 在当前节点左侧就将颜色向左传递
if (key.compareTo(x.key) < 0) {
if (!isRed(x.left) && !isRed(x.left.left))
x = moveRedLeft(x);
x.left = delete(x.left, key);
}
// 下面的逻辑是deleteMax()的逻辑加上相等时的操作:即用该节点的右子树的最小节点替换该节点
然后删除该右子树的最小节点
else {
if (isRed(x.left))
x = rotateRight(x);
// 若找到该节点且其右子树为空(表明该节点没有子节点,因为上面的左旋操作),删除
if (key.compareTo(x.key)==0 && x.right == null)
return null;
if (!isRed(x.right) && !isRed(x.right.left))
x = moveRedRight(x);
if (key.compareTo(x.key) == 0) {
Node min = min(x.right);
x.key = min.key;
x.value = min.value;
x.right = deleteMin(x.right);
}
else x.right = delete(x.right, key);
}
return balance(x);
}
public Node min(Node x) {
//assert x!=null
if (x.left == null) return x;
else return min(x.left);
}
这里的处理方式就是deleteMin对左链接和deleteMax对右链接的处理方式的综合
删除操作最终会转化为删除右子树中最小节点操作(例外是右子树为null,那么直接返回null,因为经过从上到下的变换,右子树为null的节点一定是无子节点且颜色为红);我们从根节点开始,到这右子树的最小节点这一路径上不断变换向下传递红节点,期间并没有影响到树的原有黑平衡(除了一开始人为设置根节点为红,前面说过)。最后再从下自上修复。
总结
这种红黑树的特点:rotateLeft,rotateRight,flipColor这三个操作不会破坏树的黑平衡,于是以此为基础我们就可以来借助递归从上至下的传递红链接,即moveRedLeft,moveRedRight。最后在递归从下至上过程中修复树,即balance。它有别于之前那种红黑树,之前的是先找到目标节点然后综合考虑各种情况以达到黑平衡,是自下而上的处理。