上一篇寫了關於紅黑樹基本性質的東西,這篇來說一說如何創建一棵紅黑樹吧。
如果對紅黑樹的基本性質還有疑問,請先查看一下我的前一篇:http://www.cnblogs.com/unpolishedgem/archive/2012/05/16/2504311.html。
如果圖片打不開的話,就去看我的csdn博客:http://blog.csdn.net/arge129。
紅黑樹是一種二叉查找樹,那么我們可以使用插入的方法來創建一棵紅黑樹,為此,我們先來介紹關於紅黑樹的一些基本操作。
1. 旋轉
旋轉是一種能保持二叉查找樹性質的查找樹局部操作,包括左旋和右旋兩種操作。
如下圖所示,在x結點上做左旋時,我們假設它的右孩子不是nil[T];x可以是樹內任意右孩子不是nil[T]的結點。算法導論里面講到“左旋以x到y之間的鏈為支軸進行。”我沒太理解這句話,但是我是這么想象的,如下圖中的曲線箭頭所示,左旋就是x下移,y上移,箭頭所示方向為左,右旋就是x上移,y下移,箭頭所示方向為右。
值得注意的是,在旋轉過程中,只會有指針結構的變化,不會有顏色的變化,因此在上面的圖中,我沒有畫出結點的顏色。
旋轉的偽代碼,我就不寫了,在算法導論里面都有,下面我把我寫的旋轉代碼給貼過來吧,當然還是Java版的。
1 /** 2 * 左旋 3 * @author Alfred 4 * @param x 輸入結點 5 */ 6 private void leftRotated(RBTreeNode x){ 7 RBTreeNode y = x.getRight(); 8 //x的右孩子y不能是NIL_T,如果是的話,直接返回。 9 if(y == NIL_T){ 10 return; 11 } 12 //將y的左子樹變為x的右子樹 13 //設置x的右子樹 14 x.setRight(y.getLeft()); 15 //設置y的右子樹的父結點為x 16 if(y.getLeft() != NIL_T){ 17 y.getLeft().setParent(x); 18 } 19 //將x的父結點設置為y的父結點 20 y.setParent(x.getParent()); 21 //如果x是根結點,則更換根結點 22 if(x.getParent() == NIL_T){ 23 rootNode = y; 24 }else if(x == x.getParent().getLeft()){ 25 //如果x是其父結點的左孩子,則將y設為其父結點的左孩子 26 x.getParent().setLeft(y); 27 }else{ 28 //如果x是其父結點的右孩子,則將y設為其父結點的右孩子 29 x.getParent().setRight(y); 30 } 31 //y的左孩子為x 32 y.setLeft(x); 33 //x的父結點為y 34 x.setParent(y); 35 } 36 /** 37 * 右旋 38 * @author Alfred 39 * @param y 輸入結點 40 */ 41 private void rightRotated(RBTreeNode y){ 42 RBTreeNode x = y.getLeft(); 43 //y的左孩子x不能是NIL_T,如果是的話,直接返回。 44 if(x == NIL_T){ 45 return; 46 } 47 //將x的右子樹變為y的左子樹 48 //設置y的左子樹 49 y.setLeft(x.getRight()); 50 //設置x的右子樹的父結點為y 51 if(x.getRight() != NIL_T){ 52 x.getRight().setParent(y); 53 } 54 //將y的父結點設置為x的父結點 55 x.setParent(y.getParent()); 56 //如果y是根結點,則更換根結點 57 if(y.getParent() == NIL_T){ 58 rootNode = x; 59 }else if(y == y.getParent().getLeft()){ 60 //如果y是其父結點的左孩子,則將x設為其父結點的左孩子 61 y.getParent().setLeft(x); 62 }else{ 63 //如果y是其父結點的右孩子,則將x設為其父結點的右孩子 64 y.getParent().setRight(x); 65 } 66 //x的右孩子為y 67 x.setRight(y); 68 //y的父結點為x 69 y.setParent(x); 70 }
2. 插入
既然紅黑樹是一棵二叉查找樹,那么我們就可以像二叉查找樹那樣為紅黑樹插入一個元素。我們將二叉查找樹的插入算法做一個略微的修改,我們將結點z插入到樹中,就像樹T是一棵普通的二叉查找樹一樣,然后將z着為紅色,為保持紅黑樹的性質,我們需要對樹中的結點進行重新着色並旋轉。如果對二叉查找樹的插入操作不熟悉,請閱讀我之前寫過的博客:http://www.cnblogs.com/unpolishedgem/archive/2012/05/10/2494403.html。
我們來分析一下,在插入過程中可能違反的性質有哪幾個。為此,我把紅黑樹的性質再抄寫一次。
一棵二叉查找樹如果滿足下面的紅黑性質,則為一棵紅黑樹:
1) 每個結點是或是紅的,或是黑的。
2) 根結點是黑的。
3) 每個葉結點(nil[T])是黑的。
4) 如果一個結點是紅的,那么它的兩個兒子是黑的。
5) 對每個結點,從該結點到其子孫結點的所有路徑上包含相同數目的黑結點。
首先,我們插入的結點是紅色的,因此不會違反性質1)和性質5),性質3)自然成立。唯一可能被破壞的是2)和4)。而且,2)和4)至多有一個性質被破壞。性質2)被破壞時的修復很簡單,只需要將根結點重新着色為黑色即可。而性質4)被破壞的修復則要復雜一些,具體分為三種請況。
情況1):z的叔叔y是紅色的。
如下圖所示,如果z的叔叔y是紅色的,將z的父結點和y着色為黑色,然后將z的祖父結點着色為紅色,最后將z的祖父結點作為新的z結點進行迭代檢查,因為z的祖父結點原來是紅色的,被着色為黑色的時候,有可能會引起紅黑樹性質的破壞。
情況2):z的叔叔y是黑色的,而且z是右孩子。
情況3):z的叔叔y是黑色的,而且z是左孩子。
如下圖所示,如果是情況2),我們可以立即使用一個左旋變成情況3)。情況3)中,首先交換了B和C的顏色,然后通過一個右旋來使整個樹達到了滿足性質4)。
從這三種情況來看,可以發現一個非常有趣的事情,那就是該過程所做的旋轉從不超過兩次,因為只有情況1)會繼續將z上移進行紅黑性質檢查,而一旦進入了情況2)或者情況3),就不會再進行檢查了。
同樣,偽代碼就不寫了,算法導論上都有,在此只寫Java實現代碼。
1 /** 2 * 插入操作 3 * @author Alfred 4 * @param k 5 */ 6 public void treeInsert(int k){ 7 RBTreeNode z = new RBTreeNode(k, NodeColor.RED); 8 RBTreeNode y = NIL_T; 9 RBTreeNode x = rootNode; 10 //與二叉查找樹的插入過程類似 11 while(x != NIL_T){ 12 y = x; 13 if(z.getKey() < x.getKey()){ 14 x = x.getLeft(); 15 }else{ 16 x = x.getRight(); 17 } 18 } 19 z.setParent(y); 20 if(y == NIL_T){ 21 rootNode = z; 22 }else if(z.getKey() < y.getKey()){ 23 y.setLeft(z); 24 }else{ 25 y.setRight(z); 26 } 27 z.setLeft(NIL_T); 28 z.setRight(NIL_T); 29 //進行修復 30 rbInsertFixUp(z); 31 } 32 /** 33 * 修復插入操作引起的不滿足的紅黑性質 34 * @author Alfred 35 * @param z 要修復的結點 36 */ 37 private void rbInsertFixUp(RBTreeNode z){ 38 RBTreeNode y = null; 39 while(z.getParent().getColor() == NodeColor.RED){ 40 //如果z的父結點是z的祖父結點的左孩子 41 if(z.getParent() == z.getParent().getParent().getLeft()){ 42 y = z.getParent().getParent().getRight(); 43 //情況1),z的叔叔y的顏色是紅色的。 44 if(y.getColor() == NodeColor.RED){ 45 z.getParent().setColor(NodeColor.BLACK); 46 y.setColor(NodeColor.BLACK); 47 z.getParent().getParent().setColor(NodeColor.RED); 48 z = z.getParent().getParent(); 49 }else if(z == z.getParent().getRight()){ 50 //情況2),z的叔叔y的顏色是黑色的,且z是其父結點的右孩子 51 z = z.getParent(); 52 leftRotated(z); 53 //情況2)經過左旋之后變為情況3),z的叔叔y的顏色是黑色的,且z是其父結點的左孩子 54 z.getParent().setColor(NodeColor.BLACK); 55 z.getParent().getParent().setColor(NodeColor.RED); 56 rightRotated(z.getParent().getParent()); 57 } 58 }else{ 59 //與上面情況類似。 60 y = z.getParent().getParent().getLeft(); 61 if(y.getColor() == NodeColor.RED){ 62 z.getParent().setColor(NodeColor.BLACK); 63 y.setColor(NodeColor.BLACK); 64 z.getParent().getParent().setColor(NodeColor.RED); 65 z = z.getParent().getParent(); 66 }else if(z == z.getParent().getLeft()){ 67 z = z.getParent(); 68 rightRotated(z); 69 z.getParent().setColor(NodeColor.BLACK); 70 z.getParent().getParent().setColor(NodeColor.RED); 71 leftRotated(z.getParent().getParent()); 72 } 73 74 } 75 } 76 //修復性質2) 77 rootNode.setColor(NodeColor.BLACK); 78 }
ps:寫博客很累,轉載的朋友請注明出處,謝謝。