萌新從TreeMap學習紅黑樹

萌新學習數據結構挺久的了,經常使用數據結構均可以手撕,而平衡樹只是瞭解原理,撕不出來,看各類博客文章也看得暈頭轉向的。
以前看《算法》紅皮書學習了左偏紅黑樹,此次從JDK的TreeMap來分析下常規紅黑樹。 閱讀須要有二叉查找樹的知識背景java

1.紅黑樹的基本性質

出自《算法導論》node

  1. 每一個節點是紅色或者黑色的
  2. 根節點黑
  3. 葉節點Nil黑
  4. 父子不能都是紅色的
  5. 從一個節點到其子孫節點的全部路徑上,黑色節點的數目是相同的

2.從TreeMap中提取紅黑樹相關的代碼

由於TreeMap中有不少集合相關的操做,原代碼長度上千行,看得眼花了。因此這裏把其中和紅黑樹相關的部分提取出來分析。算法

基本類及其屬性

節點類

這裏爲了方便,把其中的泛型部分簡化成int編程

private static class Node implements Comparable<Node> {
        int key;
        int val;
        boolean color = BLACK;
        Node left, right, parent;

        Node(int key, int val, Node parent) {
            this.key = key;
            this.val = val;
            this.parent = parent;
        }

        @Override
        public int compareTo(Node o) {
            return this.key - o.key;
        }
    }
複製代碼

紅黑樹類

//紅黑樹
public class RedBlackTree{
    //常量定義
    private static final boolean RED = false;
    private static final boolean BLACK = true;
    
    private Node root;
    private int size;
    // 以前的節點類
    private static class Node implements Comparable<Node> {...}
}
複製代碼

工具方法

顏色相關操做

private static boolean colorOf(Node p) {
        return (p == null ? BLACK : p.color);
    }

    private static void setColor(Node p, boolean c) {
        if (p != null)
            p.color = c;
    }
複製代碼

節點關係相關操做

private static Node parentOf(Node p) {
        return (p == null ? null : p.parent);
    }

    private static Node leftOf(Node p) {
        return (p == null) ? null : p.left;
    }

    private static Node rightOf(Node p) {
        return (p == null) ? null : p.right;
    }
    
    private Node successor(Node tmp) {
    //後繼節點的查找
        if (tmp == null) {
            return null;
        }
        if (tmp.right != null) {
            Node p = tmp.right;
            while (p.left != null) {
                p = p.left;
            }
            return p;
        } else {
            Node p = tmp.parent;
            Node ch = tmp;
            while (p != null && ch == p.right) {
                ch = p;
                p = p.parent;
            }
            return p;
        }
    }

複製代碼

旋轉相關操做

所謂左旋,就是對原節點N,讓N的右子節點R代替N的位置,
N成爲R的左子節點,至於他們的其餘子節點,看着辦就好。
從觀感上有N下位成左子節點,N的右子節點上位之感,故謂之左旋。
右旋反之。
很是簡單的操做,無須多言。數據結構

private void rotateLeft(Node tmp) {
       if (tmp == null) {
           return;
       }

       Node r = tmp.right;
       if (r == null) {
           return;
       }

       tmp.right = r.left;
       if (r.left != null) {
           r.left.parent = tmp;
       }

       r.parent = tmp.parent;
       if (tmp.parent == null) {
           root = r;
       } else if (tmp.parent.left == tmp) {
           tmp.parent.left = r;
       } else {
           tmp.parent.right = r;
       }

       r.left = tmp;
       tmp.parent = r;
   }

   private void rotateRight(Node tmp) {
       if (tmp == null) {
           return;
       }
       Node l = tmp.left;
       if (l == null) {
           return;
       }
       tmp.left = l.right;
       if (l.right != null) {
           l.parent = tmp;
       }

       l.parent = tmp.parent;

       if (tmp.parent == null) {
           root = l;
       } else if (tmp == tmp.parent.left) {
           tmp.parent.left = l;
       } else {
           tmp.parent.right = l;
       }

       l.right = tmp;
       tmp.parent = l;
   }
複製代碼

插入

總體方法

  • 空樹直接插入到root
  • 非空樹插入後,看是否創建了新的節點,有新的節點就須要修復RBT的性質
  • 這裏有兩個方法須要進一步分析,insert 和 fixAfterInsertion
public void put(int key, int val) {
        if (this.root == null) {
            //對於空樹,咱們直接插入就行了,知足RBT的各類性質
            this.root = new Node(key, val, null);
            size = 1;
        } else {
            Node newNode = insert(root, key, val);
            //newNode爲空,就是key存在,這時候沒有插入新節點
            //非空的話,插入新節點,須要考慮修復被破壞的RBT性質
            if (newNode != null) {
                  fixAfterInsertion(newNode);
            }
        }
    }
複製代碼

insert 方法分析

其實與常規二叉樹的操做同樣的ide

private Node insert(Node root, int key, int val) {
        Node tmp = root, parent = tmp;
        while (tmp != null) {
            parent = tmp;
            int cmp = tmp.key - key;
            if (cmp < 0) {
                tmp = tmp.left;
            } else if (cmp > 0) {
                tmp = tmp.right;
            } else {
                // key已經存在,直接修改val後返回就行了
                tmp.val = val;
                return null;
            }
        }
        
        //tmp爲null, parent是上一輪的tmp
        //只須要把節點插入到這個parent下面
        int cmp = parent.key - key;
        Node newNode = new Node(key, val, parent);
        if (cmp > 0) {
            parent.right = newNode;
        } else {
            parent.left = newNode;
        }
        size++;
        return newNode;
    }
複製代碼

fixAfterInsertion分析,重頭戲

方法名分析:插入以後的修復。修復必然是由於某些性質被破壞,這裏須要經過一些操做來還RBT的性質。工具

幾個關鍵點學習

  1. 首先新插入的節點設爲紅色,這樣能夠保證性質5,咱們只須要維護性質4(父子節點不能都是紅節點)就行了
  2. 循環條件爲當前節點的父節點爲紅色,由於只有這種狀況下咱們才須要維護被破壞的性質4,不然的話符合RBT的性質,能夠返回了
  3. 循環中的處理分紅兩種,按當前節點Tmp的父節點Parent是Tmp的祖父節點的左子節點仍是右子節點來分,其內部的處理方式是鏡像的,咱們只須要分析其中一種就好(第一個if)
  4. 先獲取當前節點的叔節點,其父親的兄弟。 兄弟節點可能的幾種狀態:
    • 紅色
    • 黑色
    • 不存在
  5. 當叔節點爲RED時,即代碼中第二個if處,父節點也爲RED,祖節點必爲BLACK。
    咱們把叔節點和父節點都變爲BLACK,祖節點變爲RED。
    這樣操做後:
    一方面,對於父節點和叔節點及他們的子節點來講,從根節點到當前節點路徑上的黑色節點數目沒有變化。不會破壞RBT的性質。
    另外一方面,父節點變成黑色,當前節點和父節點再也不都是紅色了,性質四被修復。
    這個修復只是針對當前節點來講的,由於祖節點變成了紅色,祖節點的性質4可能被破壞,因此須要把tmp指向祖節點,繼續下一輪循環。

變成

這個操做,能夠參考算法紅皮書中的flip方法this

  1. 當叔節點爲黑色或不存在時,第二個else那裏
    6.1 判斷當前節點是左子節點,仍是右子節點,若是是右子節點,那麼使用旋轉操做把他變成左子節點,方便後面的統一處理

6.2 正式處理:

很好,經過旋轉,成功把一個紅色節點甩鍋給叔節點了,如今性質4又獲得了知足。
注意在旋轉前,祖節點到tmp的路徑上只有一個黑色節點,祖節點到右邊的孫子的路徑上有兩個黑色節點,而旋轉後黑色節點的數目保持不變,從而維護了RBT的性質

private void fixAfterInsertion(Node insertNode) {
        setColor(insertNode, RED);
        Node tmp = insertNode;
        while (tmp != null && tmp != root && tmp.parent.color == RED) {
             //第一個if
            if (parentOf(tmp) == leftOf(parentOf(parentOf(tmp)))) {
                Node uncle = rightOf(parentOf(parentOf(tmp)));//tmp的叔節點
                if (colorOf(uncle) == RED) {
                    //第二個if
                    //可參考紅皮書的翻轉操做
                    setColor(parentOf(tmp), BLACK);
                    setColor(uncle, BLACK);
                    setColor(parentOf(uncle), RED);
                    tmp = parentOf(uncle);
                } else {//第二個else,叔節點爲黑色或不存在
                    if (tmp == rightOf(parentOf(tmp))) {//第三個if
                        //若tmp爲右節點,那麼經過旋轉操做,使tmp指向左子節點,方便下面的統一操做
                        tmp = parentOf(tmp);
                        rotateLeft(tmp);
                    }
                    setColor(parentOf(tmp), BLACK);
                    setColor(parentOf(parentOf(tmp)), RED);
                    rotateRight(parentOf(parentOf(tmp)));
                }
            }
            //else中的內容爲第一個if的鏡像
            else {
                Node uncle = leftOf(parentOf(parentOf(tmp)));//tmp的左叔節點
                if (colorOf(uncle) == RED) {
                    setColor(parentOf(tmp), BLACK);
                    setColor(uncle, BLACK);
                    setColor(parentOf(uncle), RED);
                    tmp = parentOf(uncle);
                } else {
                    if (tmp == leftOf(parentOf(tmp))) {
                        tmp = parentOf(tmp);
                        rotateRight(tmp);
                    }
                    setColor(parentOf(tmp), BLACK);
                    setColor(parentOf(parentOf(tmp)), RED);
                    rotateLeft(parentOf(parentOf(tmp)));
                }
            }
        }
        setColor(root, BLACK);
    }
複製代碼

從編程的角度理解,這裏循環的做用,是把「新加了一個紅色節點」這一事件逐層向上傳遞
在傳遞過程當中,可能某一層能夠處理這一事件,那麼他就處理,而後終止這一事件的傳遞。若是處理不了這一事件,他就經過一系列轉換,把這個事件轉換成上層要處理的問題。
這樣遞歸到root就行了。spa

查找

其實與常規二叉樹查找同樣

public Node get(int key) {
        Node tmp = root;
        while (tmp != null) {
            int cmp = tmp.key - key;
            if (cmp > 0) {
                tmp = tmp.right;
            } else if (cmp < 0) {
                tmp = tmp.left;
            } else {
                return tmp;
            }
        }
        return null;
    }
複製代碼

刪除

基本方法

public void delete(int key) {
        Node node = get(key);
        if (node != null) {
            size--;
            deleteNode(node);
        }
    }
複製代碼

deleteNode

關鍵點分析:

  1. 刪除紅色節點不會破壞RBT的性質,可是刪除黑色節點會破壞性質5,因此刪除黑色節點後須要調用fixAfterDeletion方法來修復性質5,具體後面分析

  2. 先是各類非空判斷。

  3. 語句1:對於要刪除有兩個子節點的節點Tmp,咱們先找的其後繼節點s,而後把s提到Tmp的位置,轉爲刪除s就行了。
    由於Tmp有兩個子節點,可知s一定存在於Tmp的右子樹中,而且s沒有子節點或者只有一個子節點。
    這樣咱們就把全部的刪除操做都概括到刪除葉子節點或者只有一個子節點兩種狀況下了。

  4. 對於只有一個子節點的狀況,咱們在語句2中處理。 主要步驟就是找到這個子節點,而後子節點登基大寶,原節點被忘卻。
    可是原節點若爲黑色,那麼這條路徑下全部節點的路徑都少了一個黑色節點,不符合性質5了,因此要進行修復。

  5. 對於刪除葉子節點的狀況,咱們在語句3中處理。 該節點爲紅色,咱們就直接斷開各類鏈接就行了; 若該節點爲黑色,咱們還須要先進行fixAfterDeletion。

private void deleteNode(Node node) {
        if (node == null) {
            return;
        }
        if (node.parent == null) {
            root = null;
            return;
        }
        
        //語句1
        if (node.left != null && node.right != null) {
            Node s = successor(node);
            node.key = s.key;
            node.val = s.val;
            node = s;
        }
        
        //語句2
        if (node.left != null || node.right != null) {
            //找到這個惟一的子節點
            Node replacement = node.left == null ? node.right : node.left;
            
            //把這個子節點頂上去,原節點的各類鏈接都被這個子節點所佔據
            replacement.parent = node.parent;
            if (node.parent == null) {
                root = replacement;
            } else if (node == node.parent.left) {
                node.parent.left = replacement;
            } else {
                node.parent.right = replacement;
            }
            node.left = null;
            node.right = null;
            node.parent = null; 
            
            //老節點若爲黑色,須要修復
            if (node.color == BLACK) {
                fixAfterDeletion(replacement);
            }

        } else {//語句3
              if (node.parent == null) {
                root = null;
            } else {
                if (node.color == BLACK) {
                    fixAfterDeletion(node);
                 }
                //斷開鏈接
                if (node.parent != null) {
                    if (node == node.parent.left) {
                        node.parent.left = null;
                    } else if (node == node.parent.right) {
                        node.parent.right = null;
                    }
                    node.parent = null;
                }
            }
        }
    }
複製代碼

fixAfterDeletion

若是刪除體現了「新王上位老王敗潰」的無情,那麼修復則體現了「兄弟就是拿來坑的」的暖暖親情。
刪除以後,若不知足RBT的性質,只會是不知足性質5。即和其兄弟比起來,在路徑上少了一個黑色節點。
因此這個修復的關鍵是想辦法從兄弟那裏找一個紅色節點變成黑色,經過parent傳遞過來,從而達到平衡。實在搞不到了,那麼就把兄弟也減小一個黑色節點(黑變紅),而後把問題交給parent去處理,充分體現了高超的甩鍋水平。
關鍵點分析

  1. 循環的條件就是當前節點是黑色,由於只有當前節點是黑色的時候,纔會對路徑上缺乏黑色節點這一問題一籌莫展;若當前節點是紅色,直接把他變成黑色,問題就解決了。這也是循環後要進行的一個處理。
  2. 和插入後的修復相似,這裏也是鏡像條件判斷,分紅兩種狀況來分析,即當前節點是父節點的左子節點仍是右子節點,咱們只須要分析當前節點是左子節點這一種狀況就行了。
  3. 咱們首先獲取兄弟節點bro,一定存在,否則不知足性質5。對於bro,咱們分紅bro顏色爲紅色和黑色來處理。
  4. 首先若是bro的顏色是紅色,咱們經過一系列操做把他變成黑色的,見代碼1。
    即在不改變現有的路徑黑色節點數的前提下,經過旋轉和換色操做,把兄弟節點變成黑的,從而方便以後的統一處理。
  5. 沒法從兄弟處找到紅色節點:對於代碼2,此處bro的顏色一定爲黑色,若bro的兩個子節點顏色也都是黑色,咱們就沒辦法騙過來一個紅色節點,因此這時候只能把問題交給上一級去解決。

先把兄弟變成紅色,讓bro路徑下的黑色節點數目和tmp路徑下的黑色節點數目同樣。如今問題變成了parent路徑下的黑色節點數目比parent的兄弟路徑下的黑色節點數目少一個。
6. 從bro處借來一個節點:從時bro一定爲黑色,且其子節點中至少存在一個紅色,咱們把這個紅色變黑,bro就多了一個黑色,再把多的這個經過parent傳遞過來,來修復tmp路徑下的缺憾。 代碼3,4
第一步: 爲了統一處理,咱們只處理bro的右子節點是紅色的狀況,但若是他的右子節點是黑色怎麼辦?轉化!代碼3

注意這裏的parent顏色不必定爲黑,黑紅均可能
第二步:借節點,代碼4
把兄弟提上去,兄弟的右子節點變黑(原先爲紅,第一步中保證爲紅) 把原先的parent變黑拽下來,給tmp的黑節點充數

private void fixAfterDeletion(Node tmp) {
        while (tmp != root && colorOf(tmp) == BLACK) {
            if (tmp == leftOf(parentOf(tmp))) {
                //獲取兄弟節點
                Node bro = rightOf(parentOf(tmp));
                
                //代碼1,把兄弟變成黑色
                if (colorOf(bro) == RED) {
                    //parent必定爲黑
                    setColor(bro, BLACK);
                    setColor(parentOf(tmp), RED);
                    rotateLeft(parentOf(tmp));
                    bro = rightOf(parentOf(tmp));
                }

                //代碼2,bro的顏色一定爲黑色
                if (colorOf(leftOf(bro)) == BLACK && colorOf(rightOf(bro)) == BLACK) {
                    setColor(bro, RED); 
                    tmp = parentOf(tmp);//向上一層傳遞事件
                } 
                else {
                //代碼3,bro爲黑色,而且bro至少有一個紅子節點
                    if (colorOf(rightOf(bro)) == BLACK) {
                        setColor(leftOf(bro), BLACK);//這種狀況想bro的左子節點一定爲RED
                        setColor(bro, RED);
                        rotateRight(bro);
                        bro = rightOf(parentOf(tmp));
                        //成功把bro的right轉成了紅色
                    }
                    
                    //代碼4這一步的做用是從bro那裏借來一個黑色節點
                    setColor(bro, colorOf(parentOf(tmp)));
                    setColor(parentOf(tmp), BLACK);
                    setColor(rightOf(bro), BLACK);
                    rotateLeft(parentOf(tmp));
                    
                    tmp = root;//退出
                }
            } else {//鏡像操做,沒啥好說的
                Node bro = leftOf(parentOf(tmp));

                if (colorOf(bro) == RED) {
                    setColor(bro, BLACK);
                    setColor(parentOf(tmp), RED);
                    rotateRight(parentOf(tmp));
                    bro = leftOf(parentOf(tmp));
                }
                if (colorOf(rightOf(bro)) == BLACK && colorOf(leftOf(bro)) == BLACK) {
                    setColor(bro, RED);
                    tmp = parentOf(tmp);
                } else {
                    if (colorOf(leftOf(bro)) == BLACK) {
                        setColor(rightOf(bro), BLACK);
                        setColor(bro, RED);
                        rotateLeft(bro);
                        bro = leftOf(parentOf(tmp));
                    }
                    setColor(bro, colorOf(parentOf(tmp)));
                    setColor(parentOf(tmp), BLACK);
                    setColor(leftOf(bro), BLACK);
                    rotateRight(parentOf(tmp));
                    tmp = root;
                }
            }
        }
        setColor(tmp, BLACK);
    }
複製代碼

理解這一步的關鍵就是:經過循環,把少了一個黑色節點這一事件逐層向上傳遞,直到被某一層處理。具體處理方法呢?就是在bro的子節點裏找到一個RED。
疑問爲何不在bro爲RED的時候直接把bro變成BLACK,經過parent傳遞過來呢?由於bro的子節點的黑色路徑數目會變,以下圖

3.總結

問題的等效轉換
初看可能會以爲,各類狀況極其複雜,然而他們經過等效變換(換色+旋轉,保持各個節點路徑的黑色節點數目不變),將各類複雜狀況概括爲兩三種簡單狀況,對這兩三種簡單狀況,又分爲在本層解決,或者在本層局部解決把問題推到上一層這兩種處理方式,從而保持或者修復RBT的各類性質。 插入總結

  • 父紅,叔節點爲紅色。執行flip操做,把問題推到上一層
  • 父紅,叔節點黑色,經過換色和旋轉,把一個紅色節點轉移到叔節點那棵樹上

刪除總結

  • 先把兄弟節點變成黑色的
  • 兄弟節點的左右子節點都是黑色:處理不了,把兄弟也變紅,保持tmp和兄弟的平衡,至於少了一個黑色的事情,交給parent去處理
  • 兄弟節點的左右子節點存在紅色,把紅色統一轉移到右邊,而後parent降低到tmp這裏並變黑,補足黑節點,兄弟上升爲新的parent,兄弟的右子節點變黑
相關文章
相關標籤/搜索