住上鋪的師兄面試去了TX,就因爲他和麪試官光紅黑樹就聊了半個小時,建議收藏 紅黑樹 寫在最後

紅黑樹

紅黑樹,Red-Black Tree 「RBT」是一個自平衡(不是絕對的平衡)的二叉查找樹(BST),樹上的每個節點都遵循下面的規則:

  1. 每個節點要麼是黑色,要麼是紅色。
  2. 根節點是黑色。
  3. 每個葉子節點(NIL)是黑色。
  4. 每個紅色結點的兩個子結點一定都是黑色。
  5. 任意一結點到每個葉子結點的路徑都包含數量相同的黑結點。

紅黑樹能自平衡,它靠的是什麼?三種操作:左旋、右旋和變色

操作 描述
左旋 以某個結點作爲支點(旋轉結點),其右子結點變爲旋轉結點的父結點,
右子結點的左子結點變爲旋轉結點的右子結點,左子結點保持不變。
右旋 以某個結點作爲支點(旋轉結點),其左子結點變爲旋轉結點的父結點,
左子結點的右子結點變爲旋轉結點的左子結點,右子結點保持不變。
變色 結點的顏色由紅變黑或由黑變紅。

1 旋轉操作

1.1 概念講解

左旋:以某個節點作爲旋轉點,其右子節點變爲旋轉節點的父節點,右子節點的左子節點變爲旋轉節點的右子節點,左子節點保持不變。

右旋:以某!個節點作爲旋轉點,其左子節點變爲旋轉節點的父節點,左子節點的右子節點變爲旋轉節點的左子節點,右子節點保持不變。


1.2 代碼實現

先進行類結構定義

package com.bobo.util.treemap;

public class BRTree {

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

    private RBNode root;

    public RBNode getRoot() {
        return root;
    }

    public void setRoot(RBNode root) {
        this.root = root;
    }

    /**
     * 表示 節點
     * @param <K>
     * @param <V>
     */
    static class RBNode<K extends Comparable<K>,V>{
        // 節點是雙向的
        private RBNode parent;
        private RBNode left;
        private RBNode right;
        private boolean color;
        private K key;
        private V value;

        public RBNode() {
        }

        public RBNode(RBNode parent, RBNode left, RBNode right, boolean color, K key, V value) {
            this.parent = parent;
            this.left = left;
            this.right = right;
            this.color = color;
            this.key = key;
            this.value = value;
        }

        public RBNode getParent() {
            return parent;
        }

        public void setParent(RBNode parent) {
            this.parent = parent;
        }

        public RBNode getLeft() {
            return left;
        }

        public void setLeft(RBNode left) {
            this.left = left;
        }

        public RBNode getRight() {
            return right;
        }

        public void setRight(RBNode right) {
            this.right = right;
        }

        public boolean isColor() {
            return color;
        }

        public void setColor(boolean color) {
            this.color = color;
        }

        public K getKey() {
            return key;
        }

        public void setKey(K key) {
            this.key = key;
        }

        public V getValue() {
            return value;
        }

        public void setValue(V value) {
            this.value = value;
        }
    }
} 

左旋代碼實現

 /**
     * 圍繞p左旋
     *     p               pr(r)
     *    / |             / \
     *   pl  pr(r) =>    p   rr
     *      / \         / \
     *     rl  rr     pl  rl
     *
     *  左旋的時候
     *  p-pl 和 pr-rr的關係不變
     *  pr-rl 要變爲 p-rl
     *      也就是 rl要變爲 p的右子節點
     *      同時 p要成爲 rl 的父節點
     *  還有就是要判斷  p 是否有父節點
     *  如果沒有
     *     r 變爲 root 節點
     *  如果有
     *     r.parent = p.parent
     *     還要設置 r爲 p.parent 的子節點(可能左也可能右)
     *     如果 p.parent.left == p
     *        p.parent.left = r;
     *     否則
     *        p.parent.right = r;
     *    最後
     *       p.parent = r;
     *       r.left = p;
     * @param p
     */
    private void leftRotate(RBNode p){
        if(p != null){
            RBNode r = p.right;
            // 1.設置 pr-rl 要變爲 p-rl
            // 把rl設置到p的右子節點
            p.right = r.left;
            if(r.left != null){
                // 設置rl的父節點爲p
                r.left.parent = p;
            }
            // 2.判斷p的父節點情況
            r.parent = p.parent; // 不管 p是否有父節點,都把這個父節點設置爲 r的父節點
            if(p.parent == null){
                root = r; // p沒有父節點 則r爲root節點
            }else if(p.parent.left == p){
                p.parent.left = r; // 如果p爲 p.parent的左子節點 則 r 也爲 p.parent的左子節點
            }else{
                p.parent.right = r; // 反之設置 r 爲 p.parent的右子節點
            }
            // 最後 設置 p 爲 r 的左子節點
            r.left = p;
            p.parent = r;
        }
    } 

右旋實現:

 /**
     * 圍繞p右旋
     * @param p
     */
    public void rightRotate(RBNode p){
        if(p != null){
            RBNode r = p.left;
            p.left = r.right;
            if(r.right != null){
                r.right.parent = p;
            }
            r.parent = p.parent;
            if(p.parent == null){
                root = r;
            }else if(p.parent.left == p){
                p.parent.left = r;
            }else{
                p.parent.right = r;
            }
            r.right = p;
            p.parent = r;
        }
    } 

2 新增節點

2-3-4樹中結點添加需要遵守以下規則:

  • 插入都是向最下面一層插入
  • 升元:將插入結點由 2-結點升級成 3-結點,或由 3-結點升級成 4-結點;
  • 向 4-結點插入元素後,需要將中間元素提到父結點升元,原結點變成兩個 2-結點,再把元素插入2-結點中,如果父結點也是 4-結點,則遞歸向上層升元,至到根結點後將樹高加1;

而將這些規則對應到紅黑樹裏,就是:

  • 新插入的結點顏色爲 紅色 ,這樣纔可能不會對紅黑樹的高度產生影響。
  • 2-結點對應紅黑樹中的單個黑色結點,插入時直接成功(對應 2-結點升元)。
  • 3-結點對應紅黑樹中的 黑+紅 子樹,插入後將其修復成 紅+黑+紅 子樹(對應 3-結點升元);
  • 4-結點對應紅黑樹中的 紅+黑+紅 子樹,插入後將其修復成 紅色祖父+黑色父叔+紅色孩子 子樹,然後再把祖父結點當成新插入的紅色結點遞歸向上層修復,直至修復成功或遇到 root 結點;

公式:紅黑樹+新增一個節點(紅色)**=**對等的2-3-4樹+新增一個節點

2.1 新增節點示例

我們通過新增2-3-4樹的過程來映射對應的紅黑樹的節點新增

2-3-4樹的新增(全部在葉子節點完成)

1.新增一個節點,2 節點

2.新增一個節點,與2節點合併,直接合並


3.新增一個節點,與3節點合併,直接合並

插入的值的位置會有3種情況

對應的紅黑樹爲:

4.新增一個節點,與4節點合併,此時需要分裂

插入值的位置可能是

對應的紅黑樹的結構爲

2.2 新增代碼實現

紅黑樹的新增規則我們理清楚了,接下來就可以通過Java代碼來具體的實現了。

先實現插入節點,這就是一個普通的二叉樹的插入

 /**
     * 新增節點
     * @param key
     * @param value
     */
    public void put(K key , V value){
        RBNode t = this.root;
        if(t == null){
            // 說明之前沒有元素,現在插入的元素是第一個
            root = new RBNode<>(key , value == null ? key : value,null);
            return ;
        }
        int cmp ;
        // 尋找插入位置
        // 定義一個雙親指針
        RBNode parent;
        if(key == null){
            throw new NullPointerException();
        }
        // 沿着跟節點找插入位置
        do{
            parent = t;
            cmp = key.compareTo((K)t.key);
            if(cmp < 0){
                // 左側找
                t = t.left;
            }else if(cmp > 0){
                // 右側找
                t = t.right;
            }else{
                // 插入節點的值==比較的節點。值替換
                t.setValue(value==null?key:value);
                return;
            }
        }while (t != null);
        // 找到了插入的位置  parent指向 t 的父節點  t爲null
        // 創建要插入的節點
        RBNode<K, Object> e = new RBNode<>(key, value == null ? key : value, parent);
        // 然後判斷要插入的位置 是 parent的 左側還是右側
        if(cmp < 0){
            parent.left = e;
        }else{
            parent.right = e;
        }
        // 調整  變色 旋轉
        fixAfterPut(e);
    } 

然後再根據紅黑樹的特點來實現調整(旋轉,變色)

private boolean colorOf(RBNode node){
        return node == null ? BLACK:node.color;
    }

    private RBNode parentOf(RBNode node){
        return node != null ? node.parent:null;
    }

    private RBNode leftOf(RBNode node){
        return node != null ? node.left:null;
    }

    private RBNode rightOf(RBNode node){
        return node != null ? node.right:null;
    }

    private void setColor(RBNode node ,boolean color){
        if(node != null){
            node.setColor(color);
        }
    }

    /**
     * 插入節點後的調整處理
     * 1. 2-3-4樹 新增元素 2節點添加一個元素將變爲3節點 直接合並,節點中有兩個元素
     *      紅黑樹:新增一個紅色節點,這個紅色節點會添加在黑色節點下(2節點) --- 這種情況不需要調整
     * 2. 2-3-4樹 新增元素 3節點添加一個元素變爲4節點合併 節點中有3個元素
     *      這裏有6中情況,( 根左左 根左右  根右右 根右左)這四種要調整  (左中右的兩種)不需要調整
     *      紅黑樹:新增紅色節點 會添加到 上黑下紅的節點中 = 排序後中間節點是黑色,兩邊節點是紅色
     *
     *  3. 2-3-4樹:新增一個元素 4節點添加一個元素需要裂變:中間元素升級爲父節點,新增元素與剩下的其中一個合併
     *      紅黑樹:新增節點是紅色+爺爺節點是黑色,父親節點和叔叔節點爲紅色 調整爲
     *              爺爺節點變紅色,父親和叔叔節點變爲黑色,如果爺爺節點爲root節點則調整爲黑色
     * @param x
     */
    private void fixAfterPut(RBNode<K, Object> x) {
        x.color = RED;
        // 本質上就是父節點是黑色的就不需要調整,對應的 2 3的情況
        while(x != null && x != root && x.parent.color == RED){
            // 1. x 的父節點是爺爺的 左孩子
            if(parentOf(x) == parentOf(parentOf(x)).left){
                // 獲取當前節點的叔叔節點
                RBNode y = rightOf(parentOf(parentOf(x)));
                // 情況3
                if(colorOf(y) == RED){
                    // 說明是 上3的情況  變色處理
                    // 父親節點和叔叔節點設置爲黑色
                    setColor(parentOf(x),BLACK);
                    setColor(y,BLACK);
                    // 爺爺節點設置爲 紅色
                    setColor(parentOf(parentOf(x)),RED);
                    // 遞歸處理
                    x = parentOf(parentOf(x));
                }else{
                    // 情況 2
                    if(x == parentOf(x).right){
                        // 如果x是父節點的右節點那麼我們需要先根據 父節點 左旋
                        x = parentOf(x);
                        leftRotate(x);
                    }
                    // 叔叔節點爲空  對應於 上面的情況2
                    // 將父節點變爲黑色
                    setColor(parentOf(x),BLACK);
                    // 將爺爺節點變爲紅色
                    setColor(parentOf(parentOf(x)),RED);
                    // 右旋轉  根據爺爺節點右旋轉
                    rightRotate(parentOf(parentOf(x)));

                }
            }else{
                // x 的父節點是爺爺是右孩子
                // 獲取父親的叔叔節點
                RBNode y = leftOf(parentOf(parentOf(x)));
                if(colorOf(y) == RED){
                    // 情況3
                    setColor(parentOf(x),BLACK);
                    setColor(y,BLACK);
                    setColor(parentOf(parentOf(x)),RED);
                    x = parentOf(parentOf(x));
                }else{
                    // 情況2
                    if( x == parentOf(x).left){
                        x = parentOf(x);
                        rightRotate(x);
                    }
                    setColor(parentOf(x),BLACK);
                    setColor(parentOf(parentOf(x)),RED);
                    leftRotate(parentOf(parentOf(x)));
                }
            }
        }
        root.color = BLACK;
    } 

2.3 插入節點

不通過2-3-4樹來實現添加節點的分析,看大家是否能理解哦


插入的場景

插入場景1:紅黑樹爲空樹

最簡單的一種情景,直接把插入結點作爲根結點就行,但注意,根據紅黑樹性質2:根節點是黑色。還需要把插入結點設爲黑色。
處理:把插入結點作爲根結點,並把結點設置爲黑色。

插入場景2:插入結點的父結點爲黑結點
由於插入的結點是紅色的,且父節點爲黑色節點,並不會影響紅黑樹的平衡,直接插入即可,無需做自平衡。
處理:直接插入。

插入場景3:插入結點的父結點爲紅結點
再次回想下紅黑樹的性質2:根結點是黑色。如果插入的父結點爲紅結點,那麼該父結點不可能爲根結點,所以插入結點總是存在祖父結點。這點很重要,因爲後續的旋轉操作肯定需要祖父結點的參與。

插入場景3.1:叔叔結點存在並且爲紅結點
從紅黑樹性質4可以,祖父結點肯定爲黑結點,因爲不可以同時存在兩個相連的紅結點。那麼此時該插入子樹的紅黑層數的情況是:黑紅紅。顯然最簡單的處理方式是把其改爲:紅黑紅。

實際案例:

祖父節點爲根節點:紅黑黑



祖父節點不爲根節點:

插入場景3.2**:叔叔結點不存在或爲黑結點,並且插入結點的父親結點是祖父結點的左子結點
單純從插入前來看,也即不算情景3.1自底向上處理時的情況,叔叔結點非紅即爲葉子結點(Nil)。因爲如果叔叔結點爲黑結點,而父結點爲紅結點,那麼叔叔結點所在的子樹的黑色結點就比父結點所在子樹的多了,這不滿足紅黑樹的性質5。後續情景同樣如此,不再多做說明了。

前文說了,需要旋轉操作時,肯定一邊子樹的結點多了或少了,需要租或借給另一邊。插入顯然是多的情況,那麼把多的結點租給另一邊子樹就可以了。

插入場景3.2.1:插入結點是其父結點的左子結點


插入場景3.2.2:叔叔結點不存在或爲黑結點,並且插入結點的父親結點是祖父結點的右子結點

寫在最後

傻姑粉絲福利:更多一線大廠面試題,高併發等主流技術資料盡在下方
github直達地址一線大廠面試題,高併發等主流技術資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章