Every node is either red or black.
The root is black.
Every leaf (NIL) is black.
If a node is red, then both its children are black.
For each node, all paths from the node to descendant leaves contain the same number of black nodes.
上面是算法导论对红黑树性质的描述,这两天趁着不那么忙又比较无聊研究了一下,顺便用Java实现并简单进行了测试。算法不再赘述,只说说自己理解。
红黑树的核心在于进行普通BST的插入删除操作后对树结构的调整以维持红黑树的特性。
在插入操作中,设定新插入的节点为红色,这是为了不改变该节点所在路径的Black Height。但是新节点可能会与它的父节点同为红色,这会与性质4冲突(原树为空导致性质2不满足的情况比较简单,略过不提)。插入后的调整操作就是要在保持其他性质的前提下消除与性质4的冲突。由于新插入的节点x与x.p都是红色,则x.p.p一定为黑色,此时根据x.p.p的另一个孩子(x的uncle)的颜色分了两种情况:第一种即为case 1;第二种有根据x是x.p的左节点还是右节点分为case 2和case 3。针对三种cases分别进行染色与旋转即可,比较简单。
在删除操作中,如果删掉的节点是红色,ok,完全没有影响。但如果删掉的节点是黑色,则必然会导致与性质5的冲突。算法导论给的解决方法是通过一个指针x指向被删节点的非nil子节点(如果被删节点的两个子结点都是nil,则x=nil),并且x指针对Black Height的贡献为1,即x指向的节点的所有父节点在x所在路径上的Black Height为黑色节点个数加一。这样做的目的是保持所有节点在所有路径的Black Height符合红黑树的性质,解决问题的方向是使每条路径的真实值等于对应的Black Height,解决方法只需要通过染色与旋转操作使得x指向一个红色节点(将该红色节点染黑同时删除x指针可以使所有Black Height为真实值)或指向root(没有父节点,也就不会对Black Height产生影响)即可。具体来说,算法根据x的亲兄弟的颜色及亲兄弟的孩子颜色分成了4种情况分别进行操作。
不得不说,使用nil节点代替null的设计非常巧妙, 避免了不少边界情况讨论。但在删除操作的细节上需要注意一点,算法要求左旋与右旋一定不能改变nil的父指针。Coding的时候因为没有注意这一点导致debug了近一天。
还要说的一点体会,是插入与删除的设计思路。通过恰当的分类将所有情况不相交的完全覆盖并各自击破,同时又注意到不同case之间的逻辑关系(比如解决case1后直接进入case2),并根据逻辑关系组织模块顺序,使得程序流程极为清晰又容易理解。
下面贴代码:
这是红黑树的实现(算法导论中的实现方法):
/** * Red-Black Tree * * @author Xiehao * @date 2010-12-17 * @param <T> */ package redblacktree; class TreeNode<T>{ public boolean black; public TreeNode<T> p;//parent reference public TreeNode<T> lchild;//left child reference public TreeNode<T> rchild;//right child reference public T key; public TreeNode(){ black = false; p = this; lchild = this; rchild = this; } public TreeNode(T key) throws Exception{ this(); if(! (key instanceof Comparable)){ throw new Exception("key cann't be compared!"); } this.key = key; } /** * change the color of this node * @return the color after changing */ public char switchColor(){ black = !black; return black?'b':'r'; } } public class RBTree<T> { public final TreeNode<T> nil = new TreeNode<T>(); private TreeNode<T> root; private boolean deletedIsBlack = false; public RBTree(){ nil.black = true;//property 3(Every leaf (NIL) is black) is true root = nil; } public RBTree(T k) throws Exception{ this(); root = new TreeNode<T>(k); root.black = true; root.p = nil; root.lchild = nil; root.rchild = nil; } private TreeNode<T> createTreeNode(T k){ try{ TreeNode<T> res = new TreeNode<T>(k); res.p = nil; res.lchild = nil; res.rchild = nil; return res; }catch(Exception e){ e.printStackTrace(); return nil; } } public TreeNode<T> getRoot(){ return root; } /** * Search the target key in this tree. * @param tgtkey * @return the node */ public boolean search(T tgtkey){ TreeNode<T> ret = searchGetNode(tgtkey); return ret != nil; } public TreeNode<T> searchGetNode(T tgtkey){ TreeNode<T> now = root; while( now!=nil ){ if( tgtkey.equals(now.key) ){ break;//find the answer } else{ @SuppressWarnings("unchecked") boolean toLeft = ((Comparable<T>)tgtkey).compareTo(now.key) <= 0; if(toLeft){ now = now.lchild; } else{ now = now.rchild; } } } return now; } /** * get the predecessor of predecessor of node now */ public TreeNode<T> predecessor(TreeNode<T> now){ TreeNode<T> pre = now; if(pre.lchild != nil){ pre = pre.lchild; while(pre.rchild != nil){ pre = pre.rchild; } } else{ while(pre.p!=nil && pre.p.rchild != pre){ pre = pre.p; } pre = pre.p; } return pre; } /** * get the successor of node now */ public TreeNode<T> successor(TreeNode<T> now){ TreeNode<T> suc = now; if(suc.rchild != nil){ suc = suc.rchild; while(suc.lchild != nil){ suc = suc.lchild; } } else{ while(suc.p!=nil && suc.p.lchild!=suc){ suc = suc.p; } suc = suc.p; } return suc; } /** * get the smallest element of this tree * @return */ public TreeNode<T> first(){ TreeNode<T> now = root; while(now.lchild != nil){ now = now.lchild; } return now; } /** * Walk this tree by inorder sequence */ public void inorderTreeWalk(){ TreeNode<T> now = first(); int k = 0; do{ System.out.printf("%3d : %s\n",++k,now.key); System.out.printf(" %b,%s %b,%s\n",now.lchild.black,now.lchild.key,now.rchild.black,now.rchild.key); now = successor(now); }while(now != nil); } /** * Insert a node with given key into this tree.<br> * In this method, this tree is regarded as a simple binary search tree. * @param key */ @SuppressWarnings("unchecked") private TreeNode<T> insertAsBST(T key){ TreeNode<T> node = createTreeNode(key); if(root == nil){ root = node; return root; } TreeNode<T> now = root; while(now!=nil){ if(key.equals(now.key)) break; boolean toLeft = ((Comparable<T>)key).compareTo(now.key) <= 0; if(toLeft){ if(now.lchild != nil){ now = now.lchild; } else{ now.lchild = node; node.p = now; return node; } } else{ if(now.rchild != nil){ now = now.rchild; } else{ now.rchild = node; node.p = now; return node; } } } return nil; } /** * After inserting a new node in the normal way as a BST,<br> * do use this methoed to fix the Red-Black Tree. * @param x the new node that was newly inserted into this tree <br> * by insertAsBST. */ private void insertFixRBT(TreeNode<T> x){ while(! x.p.black){//if x's parent is black, tree is RBT already if(x.p.p.lchild == x.p){ //x's parent is the left child of x's grandparent TreeNode<T> y = x.p.p.rchild;//y is the uncle of x if(! y.black){ //case 1 : x's uncle y is red y.switchColor();// solve case 1 x.p.switchColor();// solve case 1 x.p.p.switchColor();// solve case 1 x = x.p.p;// solve case 1 } else{ if(x.p.rchild == x){ //case 2 : x's uncle y is black and x is a right child x = x.p;// solve case 2 leftRotate(x);// solve case 2 //step into case 3 } //case 3 : x's uncle y is black and x is a left child x.p.switchColor();// solve case 3 x.p.p.switchColor();// solve case 3 rightRotate(x.p.p);// solve case 3 } } else{ //x's parent is the right child of x's grandparent TreeNode<T> y = x.p.p.lchild; if(! y.black){// solve case 1 y.switchColor(); x.p.switchColor(); x.p.p.switchColor(); x = x.p.p; } else{ if(x.p.lchild == x){// solve case 2 x = x.p; rightRotate(x); } //solve case 3 x.p.switchColor(); x.p.p.switchColor(); leftRotate(x.p.p); } } } if( ! root.black) root.switchColor();//make the root is black } public void insert(T key){ TreeNode<T> x = insertAsBST(key); insertFixRBT(x); } /** * delete a node with given key from this tree.<br> * In this method, this tree is regarded as a simple binary search tree. * @param key */ private TreeNode<T> deleteAsBST(TreeNode<T> x){ TreeNode<T> ret; if(x.lchild!=nil && x.rchild!=nil){ TreeNode<T> y = successor(x); x.key = y.key; y.rchild.p = y.p; if(y.p.lchild == y) y.p.lchild = y.rchild; else y.p.rchild = y.rchild; ret = y.rchild; x=y;//the actually deleted node is y } else if(x.lchild!=nil){ x.lchild.p = x.p; if(x.p.lchild == x) x.p.lchild = x.lchild; else x.p.rchild = x.lchild; if(root == x) root = x.lchild; ret = x.lchild; } else if(x.rchild!=nil){ x.rchild.p = x.p; if(x.p.lchild == x) x.p.lchild = x.rchild; else x.p.rchild = x.rchild; if(root == x) root = x.rchild; ret = x.rchild; } else{ nil.p = x.p; if(x.p.lchild == x) x.p.lchild = nil; else x.p.rchild = nil; if(root == x) root = nil; ret = nil; } deletedIsBlack = x.black; return ret; } /** * After deleting a node in the normal way as a BST,<br> * do use this methoed to fix the Red-Black Tree. * @param x the node that was newly deleted from this tree <br> * by deleteAsBST. */ private void deleteFixRBT(TreeNode<T> x){ TreeNode<T> w; while(root != x && x.black){ if(x.p.lchild == x){ w = x.p.rchild; if(! w.black){ //case 1: w is red w.switchColor();//case 1 x.p.switchColor();//case 1 leftRotate(x.p);//case 1 w = x.p.rchild;//case 1 //after case 1 x's brother (new w) becomes red //step into case (2) or ((3->)4) } if(w.lchild.black && w.rchild.black){ //case 2: w is black and both of w's children are black w.switchColor();//case 2 x = x.p;//case 2 //this loop teminates because new x may be rchild of parent } else{ if(w.rchild.black){//in this situation w.lchild must be red //case 3: w is black, w's left child is red and w's right child is black w.switchColor();//case 3 w.lchild.switchColor();//case 3 rightRotate(w);//case 3 w = x.p.rchild;//case 3 //after this case, w.rchild is changed to red //step into case 4 } //case 4: w is black and w's right child is red w.black = w.p.black;//case 4 w.p.black = true;//case 4 w.rchild.switchColor();//case 4 leftRotate(w.p);//case 4 x = root;//finish, jump out of the loop } } else {//the same method as above w = x.p.lchild; if(! w.black){ //case 1: w is red w.switchColor();//case 1 x.p.switchColor();//case 1 rightRotate(x.p);//case 1 w = x.p.lchild;//case 1 //after case 1 x's brother (new w) becomes red //step into case (2) or ((3->)4) } if(w.rchild.black && w.lchild.black){ //case 2: w is black and both of w's children are black w.switchColor();//case 2 x = x.p;//case 2 //this loop teminates } else{ if(w.lchild.black){//in this situation w.rchild must be red //case 3: w is black, w's right child is red and w's left child is black w.switchColor();//case 3 w.rchild.switchColor();//case 3 leftRotate(w);//case 3 w = x.p.lchild;//case 3 //after this case, w.lchild is changed to red //step into case 4 } //case 4: w is black and w's left child is red w.black = w.p.black;//case 4 w.p.black = true;//case 4 w.lchild.switchColor();//case 4 rightRotate(w.p);//case 4 x = root;//finish, jump out of the loop } } } x.black = true; } public void delete(T key){ TreeNode<T> x = searchGetNode(key); if(x == nil) return; TreeNode<T> y = deleteAsBST(x); if(deletedIsBlack){ deleteFixRBT(y); } } /** * Left-Rotate on the given node * @param x the given node */ private boolean leftRotate(TreeNode<T> x){ TreeNode<T> y = x.rchild; if(y == nil) return false; if(root == x){ root = y; } else{ if(x.p.lchild == x) x.p.lchild = y; else x.p.rchild = y; } y.p = x.p; x.rchild = y.lchild; //if nil.p is changed, deleteFixRBT will fail if(y.lchild != nil) y.lchild.p = x; y.lchild = x; x.p = y; return true; } /** * Right-Rotate on the given node * @param x the given node */ private boolean rightRotate(TreeNode<T> x){ TreeNode<T> y = x.lchild; if(y == nil) return false; if(root == x){ root = y; } else{ if(x.p.lchild == x) x.p.lchild = y; else x.p.rchild = y; } y.p = x.p; x.lchild = y.rchild; //if nil.p is changed, deleteFixRBT will fail if(y.rchild != nil) y.rchild.p = x; y.rchild = x; x.p = y; return true; } }
测试代码:
/** * Test the correctness of my Red-Black Tree * @author Xiehao */ package redblacktree; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import java.util.TreeSet; public class Test<T> { public int dfsCheckRBT(RBTree<T> tree,TreeNode<T> now){ //property 1(Every node is either red or black.) must be true if(now.p==null||now.lchild==null||now.rchild==null) return -1; if(now == tree.nil) return 1; if(now != tree.getRoot()){//check the link between the node and his parent if(now.p!=tree.nil && now.p.lchild!=now && now.p.rchild!=now){ System.out.println("Link error between "+now.key+" and its parent."); return -1; } else if(now.p!=tree.nil && now.p.lchild==now && now.p.rchild==now){ System.out.println("Link error between "+now.key+" and its parent."); return -1; } } if((!now.black) && (!now.p.black)){ //property 4(If a node is red, then both its children are black.) System.out.println("A node and his parent are both RED"); tree.inorderTreeWalk(); return -2; } //property 5 is checked below //(For each node, all paths from the node to descendant leaves contain the same number of black nodes) int l=-1,r=-1; l = dfsCheckRBT(tree,now.lchild); r = dfsCheckRBT(tree,now.rchild); if(l<0 || r<0) return -2; if(l != r){ System.out.println("Two sub trees have different black height in : "+now.key); System.out.println(" They are : "+l+" and "+r); System.out.println(" Now the root is : "+tree.getRoot().key); tree.inorderTreeWalk(); return -2; } if(now.black) return l+1; else return l; } public boolean testTree(RBTree<T> tree){ //property 2(The root is black.) is checked below if(!tree.getRoot().black || tree.getRoot().p!=tree.nil || !tree.nil.black) return false; if( dfsCheckRBT(tree,tree.getRoot()) > 0) return true; return false; } public final static int MAX = 1000000; public static void main(String[] args){ int[] array = new int[MAX]; for(int i=0;i<MAX;++i){ array[i] = (int)(Math.random()*MAX*100); } System.out.println("array creating finish."); Test<Integer> test = new Test<Integer>(); try { //time cost of my Red-Black Tree long begin,create,search,delete; begin = System.currentTimeMillis(); RBTree<Integer> tree = new RBTree<Integer>(); for(int i=0;i<MAX;++i) tree.insert(array[i]); create = System.currentTimeMillis(); System.out.println("test tree : "+test.testTree(tree)); create = System.currentTimeMillis(); int count = 0; for(int i=0;i<MAX;++i) if(tree.search(array[i])) ++ count; System.out.println("search ok : "+(count == MAX)); search = System.currentTimeMillis(); for(int i=0;i<MAX;++i){ tree.delete(tree.getRoot().key); } delete = System.currentTimeMillis(); System.out.println("My Red-Black Tree test result :"); System.out.printf("create: %.3fs, search:%.3fs, delete:%.3fs\n", 1.0*(create-begin)/1000,1.0*(search-create)/1000,1.0*(delete-search)/1000); //time cost of my HashSet begin = System.currentTimeMillis(); Set<Integer> hs = new HashSet<Integer>(); for(int i=0;i<MAX;++i) hs.add(array[i]); create = System.currentTimeMillis(); for(int i=0;i<MAX;++i) hs.contains(array[i]); search = System.currentTimeMillis(); Iterator<Integer> it = hs.iterator(); while(it.hasNext()){ it.next(); it.remove(); } delete = System.currentTimeMillis(); System.out.println("HashSet test result :"); System.out.printf("create:%.3fs, search:%.3fs, delete:%.3fs\n", 1.0*(create-begin)/1000,1.0*(search-create)/1000,1.0*(delete-search)/1000); //time cost of my TreeSet begin = System.currentTimeMillis(); Set<Integer> ts = new TreeSet<Integer>(); for(int i=0;i<MAX;++i) ts.add(array[i]); create = System.currentTimeMillis(); for(int i=0;i<MAX;++i) ts.contains(array[i]); search = System.currentTimeMillis(); it = ts.iterator(); while(it.hasNext()){ it.next(); it.remove(); } delete = System.currentTimeMillis(); System.out.println("TreeSet test result :"); System.out.printf("create:%.3fs, search:%.3fs, delete:%.3fs\n", 1.0*(create-begin)/1000,1.0*(search-create)/1000,1.0*(delete-search)/1000); } catch (Exception e) { e.printStackTrace(); } } }
在测试中首先通过深度优先搜索验证了整个树的指针结构与红黑树性质,之后通过对1,000,000个100,000,000以内的随机数进行插入查找和删除操作统计时间消耗并与相同数据下的HashSet和TreeSet进行了横向比较。结果如下:
array creating finish.
test tree : true
search ok : true
My Red-Black Tree test result :
create: 2.226s, search:1.606s, delete:0.159s
HashSet test result :
create:0.680s, search:0.760s, delete:0.126s
TreeSet test result :
create:1.320s, search:1.739s, delete:0.098s
多次测试后发现,自己实现的RBTree的搜索效率与TreeSet(同样通过红黑树实现)几乎相同,比hash慢。插入操作要慢于TreeSet,我估计原因在于每次插入的节点都是new出来的,需要分配内存;如果能够将节点统一分配并缓存的话应该可以更快。
由于水平有限,程序中必然存在bug,希望大家多多指正~