JAVA中的數據結構 - 真正的去理解紅黑樹


一, 紅黑樹所處數據結構的位置

在JDK源碼中, 有treeMap和JDK8的HashMap都用到了紅黑樹去存儲

紅黑樹可以看成B樹的一種: 

從二叉樹看,紅黑樹是一顆相對平衡的二叉樹

二叉樹-->搜索二叉樹-->平衡搜索二叉樹--> 紅黑樹

 

從N階樹看,紅黑樹就是一顆 2-3-4樹

N階樹-->B(B-)樹

 

故我提取出了紅黑樹部分的源碼,去說明紅黑樹的理解

看之前,理解紅黑樹的幾個特性,后面的操作都是為了讓樹符合紅黑樹的這幾個特性,從而滿足對查找效率的O(logn)

 

二,紅黑樹特性,以及保持的手段

1,根和葉子節點都是黑色的

2,不能有有連續兩個紅色的節點

3, 從任一節點到它所能到達得葉子節點的所有簡單路徑都包含相同數目的黑色節點

 

這幾個特效,個人理解就是規定了紅黑樹是一顆2-3-4的B樹了,從而滿足了O(logn)查找效率

 

保持特性的手段,通過下面這些手段,讓紅黑樹滿足紅黑樹的特性,如果要嘗試理解,可以從2-3-4樹的向上增長,后面有詳細介紹

 

當然,這些改變也都是在O(logn)內完成的,主要改變方式有

1, 改變顏色

2, 左旋

3, 右旋

 

 

三,從JDK源碼來理解

主要看我的注釋,邏輯的理解

先看TreeMap

//對treeMap的紅黑樹理解注解. 2017.02.16 by 何錦彬  JDK,1.7.51<br>  <br>/** From CLR */
    private void fixAfterInsertion(Entry<K, V> x) {
 
         
         
        //新加入紅黑樹的默認節點就是紅色
        x.color = RED;
        /**
         * 1. 如為根節點直接跳出
         */
        while (x != null && x != root && x.parent.color == RED) {
            
            if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
                 
                //如果X的父節點(P)是其父節點的父節點(G)的左節點 
                //即 下面這種情況
                /**
                 *                         G
                 *               P(RED)              U
                 */              
                //獲取其叔(U)節點
                Entry<K, V> y = rightOf(parentOf(parentOf(x)));
                if (colorOf(y) == RED) {
                    // 這種情況,對應下面 圖:情況一
                    /**
                     *                         G
                     *               P(RED)              U(RED)
                     *          X
                     */
                    //如果叔節點是紅色的(父節點有判斷是紅色). 即是雙紅色,比較好辦,通過改變顏色就行. 把P和U都設置成黑色然后,X加到P節點。 G節點當作新加入節點繼續迭代
                    setColor(parentOf(x), BLACK);
                    setColor(y, BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    x = parentOf(parentOf(x));
                } else {
                    //處理紅父,黑叔的情況
                    if (x == rightOf(parentOf(x))) {
                        // 這種情況,對應下面 圖:情況二
                        /**
                         *                           G
                         *            P(RED)                        U(BLACK)
                         *                     X
                         */
                        //如果X是右邊節點
                        x = parentOf(x);
                        // 進行左旋
                        rotateLeft(x);
                    }
                    //左旋后,是這種情況了,對應下面 圖:情況三
                    /**
                     *                           G
                     *            P(RED)                        U(BLACK)
                     *      X
                     */
                     
                    // 到這,X只能是左節點了,而且P是紅色,U是黑色的情況
                    //把P改成黑色,G改成紅色,以G為節點進行右旋
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    rotateRight(parentOf(parentOf(x)));
                }
            } else {
                //父節點在右邊的
                /**
                 *                         G
                 *                 U               P(RED)
                 */              
                //獲取U
                Entry<K, V> y = leftOf(parentOf(parentOf(x)));
                 
                if (colorOf(y) == RED) {
                    //紅父紅叔的情況
                    /**
                     *                          G
                     *             U(RED)               P(RED)
                     */    
                    setColor(parentOf(x), BLACK);
                    setColor(y, BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    //把G當作新插入的節點繼續進行迭代
                    x = parentOf(parentOf(x));
                } else {
                    //紅父黑叔,並且是右父的情況
                    /**
                     *                          G
                     *             U(RED)               P(RED)
                     */    
                    if (x == leftOf(parentOf(x))) {
                    //如果插入的X是左節點
                     /**
                        *                            G
                        *            U(BLACK)                        P(RED)
                        *                                       X              
                        */
                        x = parentOf(x);
                        //以P為節點進行右旋
                        rotateRight(x);
                    }
                    //右旋后
                    /**
                     *                            G
                     *            U(BLACK)                        P(RED)
                     *                                                        X
                     */
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    //以G為節點進行左旋
                    rotateLeft(parentOf(parentOf(x)));
                }
            }
        }
        //紅黑樹的根節點始終是黑色
        root.color = BLACK;
    }

  

 

再看看HashMap的實現

在HashMap中,在JDK8后開始用紅黑樹代替鏈表,查找由O(n) 變成了 O(Logn)

源碼分析如下:

for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //JDK8 的hashmap,鏈表到了8就需要變成顆紅黑樹了
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }

 

 

紅黑樹的維護代碼部分如下:

//hashmap的紅黑樹平衡
         static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
                                                            TreeNode<K,V> x) {
                    x.red = true;
                    //死循環加變量定義,總感覺JAVA很少這樣寫代碼 哈
                    for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
                        //xp X父節點, XPP X的祖父節點,  XPPL 祖父左節點  XXPR 祖父右節點  
                        if ((xp = x.parent) == null) {
                            x.red = false;
                            return x;
                        }
                        // 如果父節點是黑色, 或者XP父節點是空,直接返回
                        else if (!xp.red || (xpp = xp.parent) == null)
                            return root;
                        
                        // 下面的代碼就和上面的很treeMap像了,
                       
                        if (xp == (xppl = xpp.left)) {
                             // 第一種情況, 賦值xppl
                            //父節點是左節點的情況,下面這種
                            /**
                             *                         G
                             *               P(RED)              U
                             */              
                            if ((xppr = xpp.right) != null && xppr.red) {
                                //如果紅叔的情況
                                 // 這種情況,對應下面 圖:情況一
                                /**
                                 *                         G
                                 *               P(RED)              U(RED)
                                 *          X
                                 */
                                 //改變其顏色,
                                xppr.red = false;
                                xp.red = false;
                                xpp.red = true;
                                x = xpp;
                            }
                            else {
                                 // 黑叔的情況
                                //  這種情況
                              /**
                                 *                         G
                                 *               P(RED)              U(BLACK)
                                 */
                                if (x == xp.right) {
                                    //如果插入節點在右邊 這種
                                     // 這種情況,對應下面 圖:情況二
                                    /**
                                     *                           G
                                     *            P(RED)                        U(BLACK)
                                     *                     X
                                     */
                                    //需要進行左旋
                                    root = rotateLeft(root, x = xp);
                                    xpp = (xp = x.parent) == null ? null : xp.parent;
                                }
                                //左旋后情況都是這種了,對應下面 圖:情況三
                                /**
                                 *                           G
                                 *            P(RED)                        U(BLACK)
                                 *      X
                                 */
                                // 到這,X只能是左節點了,而且P是紅色,U是黑色的情況
                                if (xp != null) {
                                //把P改成黑色,G改成紅色, 以G為節點進行右旋
                                    xp.red = false;
                                    if (xpp != null) {
                                        xpp.red = true;
                                        root = rotateRight(root, xpp);
                                    }
                                }
                            }
                        }
                        else {
                             //父節點在右邊的
                            /**
                             *                         G
                             *                 U               P(RED)
                             */              
                            //獲取U
                            if (xppl != null && xppl.red) {
                                //紅父紅叔的情況
                                 /**
                                 *                           G
                                 *                 U(RED)               P(RED)
                                 */              
                                xppl.red = false;
                                xp.red = false;
                                xpp.red = true;
                                x = xpp;
                            }
                            else {
                             
                                if (x == xp.left) {
                                    //如果插入的X是右節點
                                  /**
                                     *                            G
                                     *            U(BLACK)                        P(RED)
                                     *                                       X              
                                     */
                                    root = rotateRight(root, x = xp);
                                    xpp = (xp = x.parent) == null ? null : xp.parent;
                                }
                                //右旋后
                                /**
                                 *                            G
                                 *            U(BLACK)                        P(RED)
                                 *                                                        X
                                 */
                                if (xp != null) {
                                   //把P改成黑色,G改成紅色,
                                    xp.red = false;
                                    if (xpp != null) {
                                        xpp.red = true;
                                        //以G節點左旋
                                        root = rotateLeft(root, xpp);
                                    }
                                }
                            }
                        }
                    }

 

 

情況圖如下

 

情形3示意圖

                        情況1

 

情形4示意圖

                      情況2

 

情形5示意圖

                         情況3

 

JDK源碼處理紅黑樹的流程圖

 

可見,其實處理邏輯實現都一樣的

 

 

 

三,個人對紅黑樹理解的方法

1, 如何理解紅黑樹的O(lgN)的特性?

從2-3-4樹去理解

紅黑樹,其實是一顆 2-3-4的B樹,B樹都是向上增長的,如果不理解向上增長可以先看看2-3樹,這樣理解就能知道為什么能O(logn)的查找了

 

2, 如何理解紅黑樹的紅黑節點意義?

可以把紅色節點看成是連接父節點的組成的一個大節點(2個或3個或4個節點組成的一個key),如下:

image_thumb54

                                         (此圖轉自網上)

 

紅色的就是和父節點組成了大節點,

比如 

節點7和6,6是紅色節點組成,故和它父節點7組成了一個大節點,即 2-3-4樹的 6, 7節點

又如

節點 9和10和11,9和10為紅色節點,故和10組成了一個2-3-4的3階節點, 9,10,11(注意順序有的關性)

 

3 , B樹是如何保持O(lgn)的復雜度的呢?

 B+樹都是從底布開始往上生長,自動平衡,如 2-3-4樹,當節點達到了3個時晉升到上個節點,所以不會產生單獨生長一邊的情況,形成平衡。

 

留個問題

4, 數據庫里的索引為什么不用紅黑樹而是用B+樹(Mysql)呢? 

后續解答

 

 

歡迎關注我的公眾號,重現線上各種BUG, 一起來構建我們的知識體系

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM