红黑树细节颇多,这里我可能没法子全部展开。
红黑数和普通二叉查找树的重大性能改进在于,它可以保持任何空节点至根节点的黑链数量一致。换句话说,在进行查找树种的任何元素时,总能在o(lgN)时间复杂度内完成,且与元素插入或删除操作的顺序无关。二叉查找树的主要限制问题,在于树结构会随着插入次序改变而改变,如果树结构偏向一边而不平衡,查找效率将急剧下降。
红黑数在当前分布式大规模集群的环境中依然有其用武之地,从3-节点结构向多节点结构转变,便能进一步极大地压缩树的深度(改变的是对数的底数),而代价是对树的一系列操作将会变得更为复杂。
最好的理解红黑数的方法,是类比2-3树的结构,并由此套用到红黑树当中。直接阅读红黑树的源代码过于抽象而很容易迷失其中。
在红黑树插入过程中,大体的插入逻辑如图:
插入过程中,最重要的思想是从叶子节点插入后,不断向上迭代,直至遇见一个2-节点或者根节点。这一部分的主要代码如下:
//put
public void put(Key key, Value val){
root = put(root, key, val);
root.color = BLACK; //root node is always black
}
private Node put(Node h, Key key, Value val){
//create new node
if (h == null)
return new Node(key, val, 1, RED);
//find the place to insert
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;
//modify the color
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);
//update the N of each node
h.N = 1 + size(h.left) + size(h.right);
return h;
}
此外,红黑树中比插入逻辑更困难的是删除逻辑。
书本中所介绍的删除逻辑分成了三个步骤:1.首先实现删除全树最小节点 2.接着实现删除全树最大节点 3.在前两步的基础上,实现删除任意节点的函数。
其中,第三步的主要思想如下:
1.如果需要删除的节点比当前节点小,调用moveRedLeft()函数后,保证需要删除的节点不是一个2-节点,并不断向其靠近,直至当前节点即为需要删除的节点
2.当需要删除的节点大于当前节点,同理调用deleteMax()当中的moveRedRight(),并不断向其靠近,直至当前节点即为需要删除的节点
3.当当前节点为所需删除的节点时,若节点右节点为空,则直接删除(之前的算法能够保证该节点的左节点同样为空,将该节点直接删除不会引起其他节点的丢失);若该节点右节点非空,则将该节点与其右子树中的最小元素的值赋值给该节点,并对该节点的右子树调用deleteMin(h.right),将其最小元素删除,这样变实现了制定元素的删除任务。
//********************
//deleteMin()
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.
//flip the colors so that we could build a temp 4-node
h.color = BLACK;
h.left.color = RED;
h.right.color = RED; //step2 step5
if (isRed(h.right.left)){
h.right = rotateRight(h.right); //step3
h = rotateLeft(h); //step4
}
//this statement not included in the book
//it could reduce the modify times in balance()
h.left.color = BLACK;
return h;
}
public void deleteMin(){
bala_cnt = 0;
if (!isRed(root.left) && !isRed(root.right))
root.color = RED; //step1 (use the data in graph3.3.24, the right one,p282
root = deleteMin(root);
if (!isEmpty()) root.color = BLACK;
//print out the modify times in balance()
System.out.println("balance modify cnt: "+bala_cnt);
}
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);
}
private Node balance(Node h){
if (isRed(h.right)) {h = rotateLeft(h); bala_cnt++;} //deleteMin: step6 step7
if (isRed(h.right) && !isRed(h.left)) {h = rotateLeft(h); bala_cnt++;}
if (isRed(h.left) && isRed(h.left.left)) {h = rotateRight(h); bala_cnt++; } //deleteMin: step8
if (isRed(h.left) && isRed(h.right)) {flipColors(h); bala_cnt++;}//deleteMin: step9
h.N = size(h.left) + size(h.right) + 1;
return h;
}
//********************
//deleteMax()
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.
h.color = BLACK;
h.left.color = RED;
h.right.color = RED;
if (!isRed(h.left.left))
h = rotateRight(h);
//this statement not included in the book
//it could reduce the modify times in balance()
h.right.color = BLACK;
return h;
}
public void deleteMax(){
if (!isRed(root.left) && !isRed(root.right))
root.color = RED;
root = deleteMax(root);
if (!isEmpty()) root.color = BLACK;
}
private Node deleteMax(Node h){
//not like in deleteMin(), following statement will make sure the root will not be removed at first
//and lead to losing its child(ren) when the tree only has two nodes
if (isRed(h.left))
h = rotateRight(h);//step2 step3
if (h.right == null)
return null;
if (!isRed(h.right) && !isRed(h.right.left))
h = moveRedRight(h);
h.right = deleteMax(h.right); //step1
return balance(h);
}
//********************
//delete()
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 (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); //step1
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){ //step2
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);
}
不得不说,红黑树是本书所有例子当中逻辑最为复杂的数据结构了,尤其是删除算法,非常讲究技巧。
值得一提的是,我在书本的基础上,对moveRedRight()和moveRedLeft()做了细微的改动,使得在删除任务完成,由底向上拆解之前所生成的临时4-节点过程中(调用balance()),需要调整节点数量有所下降。全局变量bala_cnt便是用来计算balance()中的调整次数而建立的,在改进后其累加次数减少,验证了改进效果。