1. 程式人生 > >Tree--RedBlackTree詳解(2 - 3 - 4Tree)(紅黑樹)

Tree--RedBlackTree詳解(2 - 3 - 4Tree)(紅黑樹)

解法 實現 重新 選擇 詳解 ole src else 場景

  前言

  最近看到好多紅黑樹的東西,英文好的童鞋可以直接點擊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)(紅黑樹)