Tree--RedBlackTree詳解(2 - 3 - 4Tree)(紅黑樹)
前言
最近看到好多紅黑樹的東西,英文好的童鞋可以直接點擊http://www.cs.princeton.edu/~rs/talks/LLRB/RedBlack.pdf這裏查看我之前學習的材料,對理解下面講的東西肯定也有點幫助(但是不完全一樣),英文一般的同學就直接看我的文采飛揚把哈哈。還有大家可以去coursera上學習一些國外比較好的資料。感覺比國內一些學習網站做的好很多。
前面一篇隨筆寫的binarysearchtree(http://www.cnblogs.com/robsann/p/7567596.html)說了有一個缺點就是不平衡,意思就是插入已經排好序的對象的時候會變成一條鏈表,鏈表當然比二叉樹要慢很多啦,隨機插入的話二叉樹的各種方法需要的時間和LgN的成正比。所以紅黑樹其實是在解決二叉樹不平衡的問題的。
度娘(這裏稍微看一下)
紅黑樹是每個節點都帶有顏色屬性的二叉查找樹,顏色或紅色或黑色。在二叉查找樹強制一般要求以外,對於任何有效的紅黑樹我們增加了如下的額外要求: 性質1. 節點是紅色或黑色。 性質2. 根節點是黑色。 性質3 每個葉節點(NIL節點,空節點)是黑色的。 性質4 每個紅色節點的兩個子節點都是黑色。(從每個葉子到根的所有路徑上不能有兩個連續的紅色節點) 性質5. 從任一節點到其每個葉子的所有路徑都包含相同數目的黑色節點。 這些約束強制了紅黑樹的關鍵性質: 從根到葉子的最長的可能路徑不多於最短的可能路徑的兩倍長。結果是這個樹大致上是平衡的。因為操作比如插入、刪除和查找某個值的最壞情況時間都要求與樹的高度成比例,這個在高度上的理論上限允許紅黑樹在最壞情況下都是高效的,而不同於普通的二叉查找樹。 正文
2 - 3 - 4 Tree
2 - 3 - 4 Tree 我覺得算是一種模型,一種樹模型保證了樹是平衡的,所謂平衡就是樹不會一個枝頭長得很高,另外一個枝頭長得很矮,那保證平衡有什麽用?平衡的情況下,所有操作需要的時間都是和LgN成正比的,你說膩害不膩害。
2 - 3 - 4 樹,允許一個節點是 2-nodes 或者是 3 nodes 或者是 4 nodes, 具體的意思就是說
這是一個2-nodes, 有2個觸手,能夠指向不同的2個子元素,左邊的子元素小於A,右邊的子元素大於A
3-nodes, 3 分叉,最左邊的子元素小於C,中間的子元素between c和e, 最右邊的子元素大於E
4-nodes, 3分叉。同上
看看2-3-4tree是如何保證平衡的
假設有 10 7 6 3 8 11 15 要插入 2 -3 4 樹中
這裏插入還有另外一種選擇叫Bottom-up solution,之下而上的一種解法(可以忽略)。就是先找到這個節點會被插入的位置,如果插入後變成了5-nodes,就把其中一個節點往父節點拋出,讓父節點和其中的一個子節點結合。
刪除同理,為了要保證樹的平衡,當刪除的時候,如果被刪除的節點只有一個元素的話,必須要把父親元素拉下來形成一個3-nodes然後刪除後變成2-nodes(後面還會說)
左傾紅黑樹(LeftLeaningRedBlackTree)
紅黑樹和2-3-4樹的關系,紅黑樹是2-3-4樹的一種實現方式。2-3-4樹只是一個模型。
先介紹一下節點對象Node,看代碼,之後會用到這個對象
private class Node { private K k; // 這個節點的key private V v; //節點的value private Node left, right; //左右節點 private int size; //節點為根的樹的大小 private boolean color; //節點的顏色,分黑和紅 Node(K k, V v, int size, boolean color) { this.k = k; this.v = v; this.size = size; this.color = color; } @Override public String toString() { if (color) return "red " + v; else return "black " + v; } }Node(節點對象)
左傾紅黑樹和紅黑樹的區別如下
這是一般的紅黑樹,3-node 的時候紅節點可以左傾或者右傾
左邊是2-3-4樹的模型,右邊是紅黑樹的實現
這是左傾紅黑樹, 3-nodes的時候紅色的節點必須在左邊。
用常量來表示紅黑
紅黑樹的修正
首先左傾紅黑樹的性質必須要得到保證(就是上圖中說的Require that 3 -node be left leaning),但是很多時候可能因為插入或者刪除的操作破壞了這個性質,所以我們必須要修正。
這裏介紹3個修正的方法
第一個方法的使用場景是這樣子的,(為了不破壞平衡,每次插入的都是紅節點),下圖插入的節點在右邊,破壞了左傾的性質,所以必須rotateLeft。rotateLeft是指把紅色節點左移
方法如下
//右樹是紅link的時候,turn this red link to left private Node rotateLeft(Node h) { assert(isRed(h.right)); Node x = h.right; //change the pointers h.right = x.left; x.left = h; x.color = h.color; //change the colors h.color = RED; x.size = h.size; //change the sizes h.size = size(h.left) + size(h.right) + 1; return x; }rotateLeft(仔細看一邊)
另外一種情況是最新插入的節點在最左邊,把中間節點rotateRight,重新平衡
方法如下
//左樹是紅link的時候,turn this red link to left private Node rotateRight(Node h) { assert(isRed(h.left)); Node x = h.left; h.left = x.right; x.right = h; x.color = h.color; h.color = RED; x.size = h.size; //size is the same h.size = size(h.left) + size(h.right) + 1; return x; }rotateRight
這張圖是破壞了性質之後,修正的辦法
還有一個方法是flipColors(),代碼如下,就是可能插入的時候需要滿足當前節點不是4-nodes,可能就會使用這個方法
有了上面這些輔助的方法後就可以開始下面的學習了
左傾紅黑樹的put
在2-3-4tree中的介紹中也知道了,put的時候,當前節點如果是4-nodes的話就沒有位置留給需要插入的對象了。
所以我們在put的時候,一定要保證當前的節點(currentNode)以後用cn來表示。cn必須不是4-nodes, 如果是4-nodes的話就用flipColor把4-node變成3個2-node
假設我們要插入10 7 6 3 這4個對象, 大片動態圖,燃燒的經費。
附上代碼
public void put(K k, V v) { root = put(root, k, v); root.color = BLACK; } private Node put(Node cn, K k, V v) { if (cn == null) return new Node(k, v, 1, RED); if(isRed(cn.left) && isRed(cn.right)) split4Node(cn);//是4節點的話 就split int cmp = k.compareTo(cn.k); if (cmp > 0) cn.right = put(cn.right, k, v); // k > node.k go right else if (cmp < 0) cn.left = put(cn.left, k, v); else cn.v = v; //hit //following code is to fix the tree on the way up if (isRed(cn.right) && !isRed(cn.left)) cn = rotateLeft(cn); // right leaning 3nodes的時候 需要變成 left leaning if (isRed(cn.left) && isRed(cn.left.left)) cn = rotateRight(cn); //變成了一個4節點 cn.size = size(cn.left) + size(cn.right) + 1; return cn; }put
左傾紅黑樹的get
樹的get方法其實很簡單,就是判斷key是不是相等,如果相等就return 這個值。
public V get(K k) { return get(root, k); } //cn means currentNode private V get(Node cn, K k) { if (cn == null) return null; // not find the key int cmp = k.compareTo(cn.k); if(cmp > 0) return get(cn.right, k); else if (cmp < 0) return get(cn.left, k); else return cn.v; // hit }get
左傾紅黑樹的刪除
刪除可以說是最難的了把,基本的思想就是,cn節點(當前節點)不會是2-node,(如果root節點是2-nodes,我們需要把root節點變成紅節點)
保證其中一個子節點不是2節點(這個保證需要看刪除的節點位於當前節點的哪裏,比如刪除的節點比cn節點小,所以接下來我們會往left走,所以要保證left節點不是2-node)。因為2節點如果刪除了的話就不會平衡。所以必須要把紅節點從root一步一步carry下去。
先實現一個deleteMin方法,我們要把紅節點帶向左邊。想一想,紅節點帶向左邊後,如果左邊有節點刪除了可能沒辦法保持平衡,紅節點可以變成黑節點,代替剛才被刪除的節點。通過這樣子可以保證左邊的樹是一定會平衡的。
deleteMin的幾種情況
1. , 現在可以直接刪除掉。也不會影響平衡。刪除了後3節點變成了2節點。
2., 需要繼續向左走,但是左子節點是2-node,必須要想辦法變成不是2-node。所以需要把父親節點和兄弟節點和cn節點。整合在一起變成4-node
3. , 需要繼續向左走,但是左子節點是2-node,必須要想辦法變成不是2-node。發現兄弟節點是不是2-nodes。所以把兄弟節點借一個node過來
2和3這2種情況總結在一起就是moveRedLeft的代碼
public void deleteMin() { //保證了root節點不是2nodes if (!isRed(root.left) && !isRed(root.right)) root.color = RED; root = deleteMin(root); root.color = BLACK; } public Node deleteMin(Node cn) { if (cn.left == null) return null; if (!isRed(cn.left) && !isRed(cn.left.left)) //判斷左邊子節點是不是2node,是的話就需要把Red帶下去 cn = moveRedLeft(cn); cn.left = deleteMin(cn.left); return fixup(cn); } private Node fixup(Node h) { if (isRed(h.right) && !isRed(h.left)) h = rotateLeft(h); //右傾 if (isRed(h.left) && isRed(h.left.left)) h = rotateRight(h); h.size = size(h.left) + size(h.right) + 1; //right the size return h; }deleteMin
隨意的delete方法。按照下面的圖說一下,基本的思想。
首先刪除D。從H開始,H不是2-node(右邊有一個紅節點),D小於H,左往H的左邊找
找到了D。D不是2-node且是紅節點。找到了D,處理辦法是找到右樹中最小的值,發現是E, 把最小的值賦值給當前的node
所以我們要往右邊走。但是發現右邊節點F是2-node。所以我們要把紅色的連接往右邊帶。
flipColor(D), B D 和 F就變成了一個4-node(可以自己做一下圖看看是不是這樣子的)。 這時候紅鏈接也往右邊帶了。
現在到了F 節點。發現F 的左節點是 2-node。把紅色的鏈接往左邊帶。
flipColor(F),這個時候F 和 E 和 G ,變成了一個4-node。 順理成章刪除左邊的節點。沒有影響平衡。
接著原路返回,修復節點。
public void delete(K k) { if (k == null) throw new IllegalArgumentException("argument to delete() is null"); if (!contains(k)) return; if (!isRed(root.left) && !isRed(root.right)) root.color = RED; root = delete(root, k); if (root != null) root.color = BLACK; } public boolean contains(K k) { return get(k) != null; } private Node delete(Node cn, K k) { if (cn == null) return null; int cmp = k.compareTo(cn.k); if (cmp < 0) { // k < node.k go left if (!isRed(cn.left) && !isRed(cn.left.left)) //保證了下一個左元素不是2nodes cn = moveRedLeft(cn); cn.left = delete(cn.left, k); } else if (cmp > 0) { // k > node.k go right if (isRed(cn.left) && !isRed(cn.right)) //如果是3節點的話需要 rotate 把red轉到右邊 cn = rotateRight(cn); if (!isRed(cn.right) && !isRed(cn.right.left)) //保證下一個右節點不是2nodes cn = moveRedRight(cn); cn.right = delete(cn.right, k); } else { //hit if (isRed(cn.left) && !isRed(cn.right)) cn = rotateRight(cn); if (k.compareTo(cn.k) == 0 && (cn.right == null)) //find null just return null return null; if (!isRed(cn.right) && !isRed(cn.right.left)) //保證下一個右節點不是2nodes cn = moveRedRight(cn); if (k.compareTo(cn.k) == 0) { Node x = min(cn.right); cn.k = x.k; cn.v = x.v; cn.right = deleteMin(cn.right); } else cn.right = delete(cn.right, k); } return fixup(cn); }delete
總結
具體的實現可以參考一下https://github.com/Cheemion/algorithms/blob/master/src/com/algorithms/tree/LeftLeaningRedBlackTree.java
可能有地方說的不清楚哈。見諒,可以留言我有不清楚的地方
Tree--RedBlackTree詳解(2 - 3 - 4Tree)(紅黑樹)