1. 程式人生 > >資料結構——平衡樹之紅黑樹

資料結構——平衡樹之紅黑樹

一、2-3樹

在瞭解什麼是紅黑樹之前,首先需要補充一下什麼是2-3樹,因為它有助於我們對紅黑樹的理解,包括對B類樹的理解。

2-3樹可以有兩個孩子或三個孩子,所以也就被稱為2-3樹,且2-3滿足二分搜尋樹的基本性質。如左圖中,a的左孩子值 < aa右孩子的值 > a,在右圖中可以存放兩個元素b、c,該樹有三個孩子,左孩子的值 < bb < 中間孩子的值 < c右孩子的值 > c。通常稱左圖含有兩個孩子的節點為2節點,稱右圖中含有三個孩子的節點為3節點。

當我們需要對2-3進行搜尋的時候,實際上也是和二分搜尋樹思路一致的,當我們搜尋的過程來到一個3節點的話,搜尋數x如果小於3節點的左值,則在3節點的左子樹中繼續尋找,如果搜尋的這個樹大於3節點的右值,到三節點的右子樹中繼續查詢,如果搜尋數x的值介於3節點的左值與右值之間,則在3節點的中間子樹繼續尋找。

對於2-3樹來說,它是一個絕對平衡的樹。即從根節點到任意一個葉子節點,所經過的節點數量一定是相同的。

理解在2-3樹中新增節點時,是如何維護絕對平衡的,對我們理解紅黑樹的機制很有幫助。

2-3樹在新增節點時,如果子樹為空,則進行融合,如果融合之後是四節點,則分裂。

如圖:

     在新增元素6時,元素12不存在左子樹,則元素12與元素6融合形成3節點              

     融合形成4節點則分裂

 

插入後形成3節點,且父親節點為2節點時

 

 

插入後形成3節點,且父親節點為3節點時

 

二、2-3樹與紅黑樹的等價性

黑色為普通節點,紅色為特殊節點,紅黑組合相當於2-3樹中的3節點,我們把3節點中的左節點b作為右節點c的 左子節點 ,因為左節點 < 右節點 ,之所以b為紅色是因為在2-3樹中,元素b元素c是作為3節點相融在一起的,但在紅黑樹中只有2節點,紅色表示【紅色的b】與其父節點【黑色的c】在2-3樹是以3節點相融的,在紅黑樹中,所有的紅色節點都是向左傾斜的

 

轉化為對應的紅黑樹:

 

三、紅黑樹的五大特性

1、每個節點或者是紅色的或者是黑色的。

2、根節點是黑色的。

3、每一個葉子節點(最後的空節點NIL)是黑色的。

4、如果一個節點是紅色的,那麼他的孩子節點一定是黑色的。

5、從任意一個節點到葉子節點,所經過的黑色節點數量相同。

 

性質2證明: 根節點是黑色的

 

性質3是一種性質:每一個葉子節點(最後的空節點NIL)是黑色的

對於一棵空樹本身,其根節點一定是黑色的,在極端的環境下,空樹即是葉節點又是根節點,都是黑色的

 

性質4證明:如果一個節點是紅色的,那麼他的孩子節點一定是黑色的

在紅黑樹中,紅黑節點組合代表的是2-3樹中的3節點,3節點中左側元素b對應紅黑樹中的紅色節點,該紅色節點的孩子為對應2-3樹中的左孩子和中間孩子,若該孩子節點為2節點,如左邊圖所示,則一定為黑色;若孩子節點為3節點,則連線的形狀如右圖所示,先連線黑色節點,黑色節點的左孩子才為空色節點,所以如果一個節點它是紅色的,它的孩子節點一定是黑色的。

拓展:對於一個黑色節點,他的左孩子可能為紅色【對應2-3樹的融合】,也可能為黑色。 總的來說:它的左孩子右可能為紅色,有可能為黑色,但右節點一定是黑色節點

 

性質5【紅黑樹核心特性】證明:從任意一個節點到葉子節點,所經過的黑色節點數量相同

因為紅黑樹和2-3樹是等價的,2-3樹是一棵絕對平衡的樹。對於2-3樹的任意一個節點出發,到葉子節點所經過的節點數一樣多,由於2-3樹是絕對平衡的樹,所有的葉子節點都在同一層中。

在2-3樹轉化為紅黑樹時,對應2節點/3節點分別轉化為黑色節點/紅色和黑色節點2個節點,不管如何轉化一定會存在一個黑色節點,所以從任意一個節點到葉子節點,對於2-3樹來說經過的節點數相同,對紅黑樹來說經過的黑色節點數量相同。

紅黑樹是保持"黑平衡"的二叉樹,嚴格意義上講紅黑樹並不是平衡二叉樹,左右子樹的黑色節點保持著絕對的平衡,對紅黑樹來說,如果存在節點個數n,那麼最大高度為2logn,時間複雜度為O(logn)。

AVL  vs  紅黑樹

查詢效率: AVL樹 > 紅黑樹

增刪改效率:  AVL樹 < 紅黑樹

 

四、向紅黑樹中新增新元素

由於2-3樹和紅黑樹具有等價性,先回憶在2-3樹中新增新節點時,永遠不會新增到一個空節點【要麼融合,要麼產生臨時4節點分裂】,所以新增進2節點時,會形成一個3節點,當新增進3節點時,會暫時形成一個4節點後分裂。

在紅黑樹中新增新元素時,設定為永遠是紅色節點,在遞迴結束後,我們需要手動設定根節點為黑色BLACK(boolean)。

新增元素時,有幾種情況:

新增到對應2-3樹中的2節點下:

1、新增元素新增到左右子樹都為空的黑色節點[2-node]

新插入節點比其根節點小,則作為根節點的左子節點,這種情況比較簡單【相當於2-3樹融合】

新插入節點比根節點大,則作為根節點的右子節點,我們知道紅色節點不可能在右側,所以需要做調整

                            

執行左旋轉

步驟1:

步驟2:

為了表示37、42是一個3節點,則原來的元素37需要變為紅色節點 

其中第三句x.color = node.color,如果原來node顏色為紅色節點,則打破了紅黑樹的基本性質,在這裡左旋轉只是一個子過程,左旋轉後形成子樹新的根節點x將會被返回做後續處理,在左旋轉時並不維持紅黑樹的基本性質,只需要保證37、42兩個元素對應2-3樹中的三節點即可。

左旋轉程式碼:

    //   node                     x
    //  /   \     左旋轉         /  \
    // T1   x   --------->   node   T3
    //     / \              /   \
    //    T2 T3            T1   T2
    private Node leftRotate(Node node){
        Node x = node.right;
        node.right = x.left;
        x.left = node;
        //切換顏色
        x.color = node.color;
        node.color = RED;
        //返回旋轉之後的根節點
        return x;
    }

 

2、新增元素新增到黑色節點[3-node]

向原3節點的紅黑樹中新增一個大於其根節點42的值

步驟1:根據二分搜尋樹的新增規則,66新增到37的右節點上

對應的是一個臨時的4節點【融合】 處理方式為:

步驟2:拆分成3個2節點

對應紅黑樹為3個黑色節點,讓42的左右子節點都改變為黑色

步驟3:

此時其根節點42應該與42的父節點進行融合,所以42需要變為紅色

我們發現42由黑色變為了紅色,而37和66都由紅色變為了黑色,該動作稱為顏色翻轉【flipColors】

    //顏色翻轉
    private void flipColors(Node node){
        node.color = RED;
        node.left.color = BLACK;
        node.right.color = BLACK;
    }

 

向原3節點的紅黑樹中新增一個小於其父節點的值

新增後:

此時產生了一個臨時的4節點,處理方式仍然為分裂,即變為由3個2節點的子樹,如圖:

此時應該執行右旋轉步驟1

旋轉之前:

旋轉之後:

步驟2:此時需要變換顏色,x的顏色需要改變為node的顏色,node的顏色需要設定為紅色

最後執行我們編寫過的顏色翻轉即可,變為:

右旋轉程式碼:

    //     node                   x
    //    /   \     右旋轉       /  \
    //   x    T2   ------->   y   node
    //  / \                       /  \
    // y  T1                     T1  T2
    public Node rightNode(Node node){
        Node x = node.left;
        node.left = x.right;
        x.right = node;
        //切換顏色
        x.color = node.color;
        node.color = RED;
        return x;
    }

 

向原3節點的紅黑樹中新增一個大於根節點且大於其父節點的值

新增後形成的紅黑樹情況:

 

步驟1:對節點37進行左旋轉,如圖

步驟2:對節點42進行右旋轉,如圖:

步驟3:變色操作,對節點40變為42的顏色,將節點42變為紅色

步驟4:執行顏色翻轉

 

新增元素總結

這種過程即為我上述的向紅黑樹3節點中新增元素【新增的元素小於根節點大於新增位置的父節點】的全過程

 

向紅黑樹3節點中新增元素【新增的元素小於根節點於新增位置的父節點】的全過程,即直接跳到了第三步

 

向紅黑樹3節點中新增元素【新增的元素大於根節點】的全過程,即直接跳到了第四步

 左右旋轉條件總體邏輯程式碼編寫:

左旋轉 右節點為紅色且左節點不為紅色

右旋轉 左節點為紅色且左節點的左節點也為紅色

顏色翻轉 

在add方法中:【使用3個if 每次都順序判斷】

    // 向以node為根的二分搜尋樹中插入元素(key, value),遞迴演算法
    // 返回插入新節點後二分搜尋樹的根
    private Node add(Node node, K key, V value){

        if(node == null){
            size ++;
            return new Node(key, value);   //預設插入紅色節點
        }

        if(key.compareTo(node.key) < 0)
            node.left = add(node.left, key, value);
        else if(key.compareTo(node.key) > 0)
            node.right = add(node.right, key, value);
        else // key.compareTo(node.key) == 0
            node.value = value;

        //左旋轉 右節點為紅色且左節點不為紅色
        if(isRed(node.right) && !isRed(node.left))
            node = leftRotate(node);
        //右旋轉 左節點為紅色且左節點的左節點也為紅色
        if(isRed(node.left) && isRed(node.left.left))
            node = rightNode(node);
        //顏色翻轉
        if(isRed(node.left) && isRed(node.right))
            flipColors(node);
        return node;
    }

 

紅黑樹的效能總結:

 

紅黑樹完整程式碼:

import java.util.ArrayList;

public class RBTree<K extends Comparable<K>, V> {

    private static final boolean RED = true;
    private static final boolean BLACK = false;

    private class Node{
        public K key;
        public V value;
        public Node left, right;
        public boolean color;

        public Node(K key, V value){
            this.key = key;
            this.value = value;
            left = null;
            right = null;
            color = RED;
        }
    }

    private Node root;
    private int size;

    public RBTree(){
        root = null;
        size = 0;
    }

    public int getSize(){
        return size;
    }

    public boolean isEmpty(){
        return size == 0;
    }

    // 判斷節點node的顏色
    private boolean isRed(Node node){
        if(node == null)
            return BLACK;
        return node.color;
    }

    //   node                     x
    //  /   \     左旋轉         /  \
    // T1   x   --------->   node   T3
    //     / \              /   \
    //    T2 T3            T1   T2
    private Node leftRotate(Node node){

        Node x = node.right;

        // 左旋轉
        node.right = x.left;
        x.left = node;

        x.color = node.color;
        node.color = RED;

        return x;
    }

    //     node                   x
    //    /   \     右旋轉       /  \
    //   x    T2   ------->   y   node
    //  / \                       /  \
    // y  T1                     T1  T2
    private Node rightRotate(Node node){

        Node x = node.left;

        // 右旋轉
        node.left = x.right;
        x.right = node;

        x.color = node.color;
        node.color = RED;

        return x;
    }

    // 顏色翻轉
    private void flipColors(Node node){

        node.color = RED;
        node.left.color = BLACK;
        node.right.color = BLACK;
    }

    // 向紅黑樹中新增新的元素(key, value)
    public void add(K key, V value){
        root = add(root, key, value);
        root.color = BLACK; // 最終根節點為黑色節點
    }

    // 向以node為根的紅黑樹中插入元素(key, value),遞迴演算法
    // 返回插入新節點後紅黑樹的根
    private Node add(Node node, K key, V value){

        if(node == null){
            size ++;
            return new Node(key, value); // 預設插入紅色節點
        }

        if(key.compareTo(node.key) < 0)
            node.left = add(node.left, key, value);
        else if(key.compareTo(node.key) > 0)
            node.right = add(node.right, key, value);
        else // key.compareTo(node.key) == 0
            node.value = value;

        if (isRed(node.right) && !isRed(node.left))
            node = leftRotate(node);

        if (isRed(node.left) && isRed(node.left.left))
            node = rightRotate(node);

        if (isRed(node.left) && isRed(node.right))
            flipColors(node);

        return node;
    }

    // 返回以node為根節點的二分搜尋樹中,key所在的節點
    private Node getNode(Node node, K key){

        if(node == null)
            return null;

        if(key.equals(node.key))
            return node;
        else if(key.compareTo(node.key) < 0)
            return getNode(node.left, key);
        else // if(key.compareTo(node.key) > 0)
            return getNode(node.right, key);
    }

    public boolean contains(K key){
        return getNode(root, key) != null;
    }

    public V get(K key){

        Node node = getNode(root, key);
        return node == null ? null : node.value;
    }

    public void set(K key, V newValue){
        Node node = getNode(root, key);
        if(node == null)
            throw new IllegalArgumentException(key + " doesn't exist!");

        node.value = newValue;
    }

    // 返回以node為根的二分搜尋樹的最小值所在的節點
    private Node minimum(Node node){
        if(node.left == null)
            return node;
        return minimum(node.left);
    }

    // 刪除掉以node為根的二分搜尋樹中的最小節點
    // 返回刪除節點後新的二分搜尋樹的根
    private Node removeMin(Node node){

        if(node.left == null){
            Node rightNode = node.right;
            node.right = null;
            size --;
            return rightNode;
        }

        node.left = removeMin(node.left);
        return node;
    }

    // 從二分搜尋樹中刪除鍵為key的節點
    public V remove(K key){

        Node node = getNode(root, key);
        if(node != null){
            root = remove(root, key);
            return node.value;
        }
        return null;
    }

    private Node remove(Node node, K key){

        if( node == null )
            return null;

        if( key.compareTo(node.key) < 0 ){
            node.left = remove(node.left , key);
            return node;
        }
        else if(key.compareTo(node.key) > 0 ){
            node.right = remove(node.right, key);
            return node;
        }
        else{   // key.compareTo(node.key) == 0

            // 待刪除節點左子樹為空的情況
            if(node.left == null){
                Node rightNode = node.right;
                node.right = null;
                size --;
                return rightNode;
            }

            // 待刪除節點右子樹為空的情況
            if(node.right == null){
                Node leftNode = node.left;
                node.left = null;
                size --;
                return leftNode;
            }

            // 待刪除節點左右子樹均不為空的情況

            // 找到比待刪除節點大的最小節點, 即待刪除節點右子樹的最小節點
            // 用這個節點頂替待刪除節點的位置
            Node successor = minimum(node.right);
            successor.right = removeMin(node.right);
            successor.left = node.left;

            node.left = node.right = null;

            return successor;
        }
    }

}