1. 程式人生 > >【演算法】紅黑樹插入資料的情況與實現(三)

【演算法】紅黑樹插入資料的情況與實現(三)

大家如果有玩魔方,我相信是可以理解我說的東西的,轉魔方就是先把第一面轉出來,然後把第一面作為底面,然後根據遇見的情況來轉魔方(是有公式的)

該系列到現在暫只有3篇文章:

 

【演算法】紅黑樹(二叉樹)概念與查詢(一):https://blog.csdn.net/lsr40/article/details/85230703

【演算法】紅黑樹插入資料(變色,左旋、右旋)(二):https://blog.csdn.net/lsr40/article/details/85245027

【演算法】紅黑樹插入資料的情況與實現(三):https://blog.csdn.net/lsr40/article/details/85266069

 

紅黑樹也是一樣,我們也會遇到不同的情況,也會遇到不同情況中的不同階段,我們對於每一種情況的處理(變色和旋轉),都需要讓區域性滿足規則5,然後再迴圈的向上判斷剛剛的變換是否破壞了上層結構的平衡(一般就是破壞了上層的規則4)

1、會遇到的情況:

這裡我總結為4種大情況在第4種情況中,有階段1階段2

情況1:如果是根節點,直接插入就完事了(插入還是固定為紅色,然後在程式碼的最後把根目錄設定為黑色)

情況2:插入節點的父親,為黑色,也一樣,插入就完事了,不用做任何的改動

情況3:插入節點的父親為紅色,叔叔節點(插入節點的爺爺的另一個子節點)的顏色也是紅色

情況4:插入節點的父親為紅色,叔叔節點節點為黑色(這種情況最麻煩,因為需要再做一次判斷)

我先來描述下:這裡可能會出現4種情況(別怕,四種情況只有2種處理方式),看圖:

(爺爺節點我用G表示,爸爸:F,叔叔:U,插入節點:M)

請注意!!!你發現如下的4張圖U這個節點都為NIL(也就是一個不存在的空節點),不存在的空節點預設也為黑色,我強行畫了出來只是為了便於理解,大家可以去:https://sandbox.runjs.cn/show/2nngvn8w 網站上自己插入試試,遇到的情況4,叔叔節點都是空的,另外我發現很多講紅黑樹的文章,也把叔叔節點畫成一個黑色的節點,可能會讓看官產生疑惑(因為如果U是一個存在的節點,那本身就不符合規則5,然後你會發現一頓旋轉操作結束,也不能符合規則5,接著就一臉懵逼),所以我這裡特地標明!

圖從左往右,從上到下看

圖1:父節點是爺爺節點的左節點,插入節點是父節點的右節點

圖2:父節點是爺爺節點的左節點,插入節點是父節點的左節點

圖3:父節點是爺爺節點的右節點,插入節點是父節點的左節點

圖2:父節點是爺爺節點的右節點,插入節點是父節點的右節點

是不是被繞暈了,其實就是爺爺和爸爸和插入節點要是否三點一線,如果不是三點一線,就符合情況4的第1階段

如果是三點一線,就符合情況4的第2階段!為什麼是階段1和階段2呢?因為階段1的處理方式,就是經過旋轉變成階段2,然後再做階段2應該做的旋轉!

2、對於不同情況的整理:

情況1:這棵樹沒有任何節點,插入的點為根節點,一開始插入紅色,然後直接轉為黑色

情況2:父節點是黑色,直接插入,不做任何的換色和旋轉

情況3:父節點是紅色,且叔叔節點也是紅色,直接把叔叔和爸爸變成黑色,然後把爺爺變成和自己一樣的紅色,繼續迭代(因為這樣可能會出現爺爺和太爺爺的都是紅色的情況,那麼就要繼續判斷是哪種情況)

情況4:父節點是紅色,且叔叔節點也是紅色,要先判斷在哪個階段

-1.如果符合階段1,就是圖1和圖3

圖1就對F節點做左旋,圖3就對F節點做右旋,把圖形的樣子旋轉成階段2

-2.如果符合階段2,就是圖2和圖4

圖2就對G節點做右旋,然後將G變紅色,F變黑色,如下圖

 

圖4就對G節點做左旋,然後G變紅色,F變黑色,如下圖

3、程式碼實現

我們一起來思考下應該怎麼實現?

1、外層要有迴圈

所以我們發現,一套變換下來,實現了局部滿足紅黑樹的所有規則的,但是情況3將爺爺節點變成了紅色,那就有可能爺爺和太爺爺變成了兩個紅色,相互衝突(違反規則4),所以程式碼應該外面有一層迴圈。

2、判斷父節點是爺爺節點的哪邊的子節點

因為情況4中,需要判斷新插入的節點和父親節點和爺爺節點是否三點一線,如果三點一線就直接進行階段2的變換,否則要先進行階段1,再進行階段2。

3、其他設定

插入的節點必須為紅色,程式碼的最後根節點必須設定為黑色,最好封裝好左旋和右旋的方法,獲取父節點,獲取左右子節點的方法等待呼叫

如果上面三點大家能夠想明白的話,我們再一起來看看TreeMap的put和fixAfterInsertion的原始碼!

/**
  * 我相信put的程式碼,我就不需要過多的解釋了
  * put就是判斷是否有比較器,然後從根節點一層層往下遍歷
  * 最後插入到最底下的節點中,關鍵是插入完之後
  * 呼叫了fixAfterInsertion方法(用來使插入節點後的樹重新滿足紅黑樹的性質)
  */
public V put(K key, V value) {
    Entry<K,V> t = root;
    if (t == null) {
        compare(key, key); // type (and possibly null) check

        root = new Entry<>(key, value, null);
        size = 1;
        modCount++;
        return null;
    }
    int cmp;
    Entry<K,V> parent;
    // split comparator and comparable paths
    Comparator<? super K> cpr = comparator;
    if (cpr != null) {
        do {
            parent = t;
            cmp = cpr.compare(key, t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    }
    else {
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
            Comparable<? super K> k = (Comparable<? super K>) key;
        do {
            parent = t;
            cmp = k.compareTo(t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    }
    Entry<K,V> e = new Entry<>(key, value, parent);
    if (cmp < 0)
        parent.left = e;
    else
        parent.right = e;
    fixAfterInsertion(e);
    size++;
    modCount++;
    return null;
}
  
    private void fixAfterInsertion(Entry<K,V> x) {
        //將插入的新節點,顏色設定為紅色
        x.color = RED;
        /**
         * 當插入的節點x不為空,不是根節點,並且父節點是紅色的
         * 因為如果父節點是黑色的,直接插入紅色的新節點是不會影響紅黑樹的性質的,不用做任何操作
         * 所以到這裡已經把情況1和情況2都過濾掉了,下面就是對情況3和情況4的處理
         * 並不是處理一次就能得到最後的結果,大家還需要注意x的重新賦值,進入下一次迴圈
         */
        while (x != null && x != root && x.parent.color == RED) {
            //如果x的祖父節點的左節點是x的父節點,這裡的判斷就是為了情況4的三點一線
            if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
                //那設定x的祖父節點的右節點為y(就是x的叔叔節點為y)
                Entry<K,V> y = rightOf(parentOf(parentOf(x)));
                //如果叔叔y顏色是紅色,那就是情況3
                if (colorOf(y) == RED) {
                    //那麼就設定x的父節點為黑色
                    setColor(parentOf(x), BLACK);
                    //叔叔節點y也設定為黑色
                    setColor(y, BLACK);
                    //設定祖父節點為紅色
                    setColor(parentOf(parentOf(x)), RED);
                    //把x設定為祖父節點,繼續往上游迴圈
                    x = parentOf(parentOf(x));
                    //否則叔叔y的顏色是黑色,情況4	
                } else {
                    //如果x是父節點的右節點,三點不一線,先做情況4的階段1
                    if (x == rightOf(parentOf(x))) {
                        //把x設定為父節點
                        x = parentOf(x);
                        //基於剛剛新設定的x節點進行左旋
                        rotateLeft(x);
                    }
                    //情況4的階段2
                    //設定x節點為黑色
                    setColor(parentOf(x), BLACK);
                    //祖父節點為紅色
                    setColor(parentOf(parentOf(x)), RED);
                    //基於祖父節點進行右旋
                    rotateRight(parentOf(parentOf(x)));
                }
                //否則就是情況4的另外兩種,父節點是爺爺節點的右子節點
            } else {
                Entry<K,V> y = leftOf(parentOf(parentOf(x)));
                //如果叔叔y是紅色,那就是情況3
                if (colorOf(y) == RED) {
                    setColor(parentOf(x), BLACK);
                    setColor(y, BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    //把x設定為祖父節點,繼續往上游迴圈
                    x = parentOf(parentOf(x));
                } else {
                    //否則叔叔y是黑色,就是情況4,先判斷是不是情況4的階段1,
                    if (x == leftOf(parentOf(x))) {
                        //階段1會把插入的節點轉到上面去,所以我需要重新設定插入的節點為原來的父節點
                        x = parentOf(x);
                        rotateRight(x);
                    }
                    //執行情況4的階段2
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    rotateLeft(parentOf(parentOf(x)));
                }
            }
        }
        root.color = BLACK;
    }

好了,到這裡我就已經把插入的情況和程式碼處理就講完了!

我的思路是偏向於:https://en.wikipedia.org/wiki/Red%E2%80%93black_tree#Insertion這篇英文wiki的,我覺得它整理的很清晰,也很有助於我的學習,如果英文好的同學也可以直接去學習~

如果本文中有什麼筆誤,還希望大家批評指出,避免誤導新人~如果有任何疑問,歡迎留言評論!