【數據結構】紅黑樹(jdk1.8詳解)

我的原則:先會用再說,內部慢慢來。
學以致用,根據場景學源碼


一、架構

在這裏插入圖片描述

二、特性

2.1 二叉查找樹的特點

任意一個節點所包含的鍵值,大於等於左孩子的鍵值,小於等於右孩子的鍵值。

2.2 紅黑樹的特點

  1. 紅黑樹(Red-Black Tree,簡稱R-B Tree),它一種特殊的二叉查找樹。
  2. 節點要麼黑色,要麼紅色
  3. 根節點是黑的
  4. 葉子節點也是黑的 [注意:這裏葉子節點,是指爲空的葉子節點!]
  5. 如果一個節點是紅色的,則它的兒子必須是黑色的。
  6. 從一個節點到該節點的子孫節點的所有路徑上包含相同數目的黑節點。(確保沒有一條路徑會比其他路徑長出倆倍。因而,紅黑樹是相對是接近平衡的二叉樹。)
總結: 非黑即紅,頭尾是黑,紅兒是黑,各路同黑。

=== 點擊查看top目錄 ===

2.3 紅黑樹的基本操作

  1. 添加
  2. 刪除
  3. 旋轉。
  • 爲什麼要旋轉?
  1. 旋轉的目的是讓樹保持紅黑樹的特性。
  2. 添加或刪除紅黑樹中的節點之後,紅黑樹就發生了變化,可能不滿足紅黑樹的5條性質,也就不再是一顆紅黑樹了,而是一顆普通的樹。而通過旋轉,可以使這顆樹重新成爲紅黑樹。
  3. 旋轉包括兩種:左旋 和 右旋。

=== 點擊查看top目錄 ===

2.3.1 【左旋】
2.3.1.1 圖示與步驟

 height=500 width=500 src="https://img-blog.csdn.net/20160418153051177>

在這裏插入圖片描述

  1. 先檢查 P 是不是空,並且 P 的右邊有東西
  2. P 的右指針指向了 Y 的左節點
  3. 原先 Y 的左節點換爸爸
  4. Y 的爸爸換成了 P 的爸爸。由於 P 的爸爸比較頂部,所以假如原先 root (root 指向的節點的爸爸是 null) 指向了 P 話,那麼此時 root 改爲指向了 Y ,並且由於紅黑樹性質“頭尾是黑”,此時 Y 的節點的 red 肯定是 false
  5. 頂部爸爸換兒子(原先兒子是 P, 現在要變成 Y),先判斷原先孩子P到底是左孩子,還是右孩子,然後再替換
  6. Y 的左孩子變成了 P
  7. P 的爸爸變成了 Y

=== 點擊查看top目錄 ===

2.3.1.2 代碼(HashMap1.8)
  • HashMap中紅黑樹的代碼(jdk1.8)
  • java.util.HashMap.TreeNode#rotateLeft
 static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
                                          TreeNode<K,V> p) {
    TreeNode<K,V> y, pp, yl;
   	// 1. 先檢查 P 是不是空,並且 P的右邊有東西
    if (p != null && (y = p.right) != null) {
    	// 2. P 的右指針指向了 Y 的左節點
        if ((yl = p.right = y.left) != null)
        	// 3.原先 Y 的左節點換爸爸 
            yl.parent = p;
        // 4. Y 的爸爸換成了 P 的爸爸。
        if ((pp = y.parent = p.parent) == null)
        	// 假如 “原先 root 指向 P”,進入此代碼
            (root = y).red = false;
        // 5.  頂部爸爸換兒子(原先兒子是 P, 現在要變成 Y)
        // 先判斷原先孩子P到底是左孩子,還是右孩子,然後再替換
        else if (pp.left == p)
        	// 原先是左孩子
            pp.left = y;
        else
 			// 原先是右孩子
            pp.right = y;
        // 6. Y 的左孩子變成了 P
        y.left = p;
        // 7. P  的爸爸變成了 Y
        p.parent = y;
    }
    return root;
}

=== 點擊查看top目錄 ===

2.3.2 【右旋】
2.3.2.1 圖示與步驟

在這裏插入圖片描述
在這裏插入圖片描述

  1. 先檢查 P 是否爲空,並且檢查 P 是否有左孩子
  2. P 的左指針指向了 X 的右孩子
  3. 原先 X 的右節點換爸爸
  4. X 的爸爸換成了 P 的爸爸。由於 P 的爸爸比較頂部,所以假如原先 root (root 指向的節點的爸爸是 null) 指向了 P 話,那麼此時 root 改爲指向了 Y ,並且由於紅黑樹性質“頭尾是黑”,此時 Y 的節點的 red 肯定是 false
  5. 頂部爸爸換兒子(原先兒子是 P, 現在要變成 Y),先判斷原先孩子P到底是左孩子,還是右孩子,然後再替換
  6. X 的右兒子變成 P
  7. P 的爸爸變成了 X

=== 點擊查看top目錄 ===

2.3.2.2 代碼(HashMap1.8)
  • HashMap中紅黑樹的代碼(jdk1.8)
  • java.util.HashMap.TreeNode#rotateRight
  static <K,V> HashMap.TreeNode<K,V> rotateRight(HashMap.TreeNode<K,V> root,
                                                   HashMap.TreeNode<K,V> p) {
        HashMap.TreeNode<K,V> x, pp, lr;
        // 1. 先檢查 P 是否爲空,並且檢查 P 是否有左孩子
        if (p != null && (x = p.left) != null) {
        	// 2. P 的左指針指向了 X 的右孩子
            if ((lr = p.left = x.right) != null)
            	// 3. 原先 X 的右節點換爸爸 
                lr.parent = p;
            // 4. X 的爸爸換成了 P 的爸爸
            if ((pp = x.parent = p.parent) == null)
            	 // 假如 “原先 root 指向 P”,進入此代碼
                (root = x).red = false;
            else if (pp.right == p)
                // 原先是右孩子
                pp.right = x;
            else
                // 原先是左孩子
                pp.left = x;
            // 6. X 的右兒子變成 P
            x.right = p;
            // 7. P 的爸爸變成了 X
            p.parent = x;
        }
        return root;
    }

=== 點擊查看top目錄 ===

2.3.3 【插入】
  • 添加節點的步驟
  1. 將紅黑樹當作一顆二叉查找樹,將節點插入。
  2. 將插入的節點着色爲"紅色"。
  3. 通過一系列的旋轉或着色等操作,使之重新成爲一顆紅黑樹。
  • 第一步: 將紅黑樹當作一顆二叉查找樹,將節點插入。

黑樹本身就是一顆二叉查找樹,將節點插入後,該樹仍然是一顆二叉查找樹。也就意味着,樹的鍵值仍然是有序的。此外,無論是左旋還是右旋,若旋轉之前這棵樹是二叉查找樹,旋轉之後它一定還是二叉查找樹。這也就意味着,任何的旋轉和重新着色操作,都不會改變它仍然是一顆二叉查找樹的事實。

  • 第二步:將插入的節點着色爲"紅色"。( 爲什麼着色成紅色,而不是黑色呢?爲什麼呢?)
  • 先看紅黑樹的特點

這裏將插入的節點着色爲紅色,不會違背"特性(6)" !
少違背一條特性,就意味着我們需要處理的情況越少。接下來,就要努力的讓這棵樹滿足其它性質即可;滿足了的話,它就又是一顆紅黑樹了。

  • 第三步: 通過一系列的旋轉或着色等操作,使之重新成爲一顆紅黑樹。

=== 點擊查看top目錄 ===

2.3.3.2 簡易代碼
  • 看先簡易的實現,理解完再去看HashMap(jdk1.8)中的紅黑樹實現。
/* 
 * 將結點插入到紅黑樹中
 *
 * 參數說明:
 *     node 插入的結點
 */
private void insert(RBTNode<T> node) {
	// 1. 設置節點的顏色爲紅色
    node.color = RED;
    
    int cmp; // 節點的比較結果 0 1 -1
    RBTNode<T> y = null; // 緩衝使用,記錄上一個節點
    RBTNode<T> x = this.mRoot;

    // 2. 將紅黑樹當作一顆二叉查找樹,將節點添加到二叉查找樹中。
    while (x != null) { // 遍歷到最後,沒有左孩子或者右孩子了,退出
        y = x;// 緩衝使用,記錄上一個節點
        cmp = node.key.compareTo(x.key);
        if (cmp < 0)
            x = x.left; // node 比 X 小,那麼往左邊遍歷
        else
            x = x.right;// node 比 X 大,那麼往右邊遍歷
    }
	
	//已經找到並且退出,此時認爸爸,指向了上一個節點 y
    node.parent = y;
    if (y!=null) { // 爲 null 表明是最頂部了
        cmp = node.key.compareTo(y.key); // 看下插入的節點是y的左孩子還是右孩子
        if (cmp < 0)
            y.left = node;
        else
            y.right = node;
    } else {
        this.mRoot = node;
    }

 
    // 3. 將它重新修正爲一顆二叉查找樹
    insertFixUp(node);
}

=== 點擊查看top目錄 ===

2.3.3.2 代碼(HashMap1.8)

在這裏插入圖片描述

  1. HashMap的數據結構 = 數組 + 鏈表 + 紅黑樹,要出現紅黑樹,必須該鏈表中節點數量達到一定限度纔會進行樹化。
  2. 看上圖,能到同一個桶的Node,他們的Hash必然相同,不同的是equals的結果,假如hashcode()與equals()方法相同,那麼在HashMap中會進行一個覆蓋處理。
  3. 紅黑樹是二叉樹,所以可以允許key值重複,這一點在看不同數據結構的時候,需要甄別理解。
  • java.util.HashMap.TreeNode#putTreeVal
    注意,這個方法putTreeVal 上游的調用方是一個TreeNode,也是上面圖中數組桶黑色的那個最開始的節點
    在這裏插入圖片描述

/**
 * 當存在hash碰撞的時候,且元素數量大於8個時候,就會以紅黑樹的方式將這些元素組織起來
 * map 當前節點所在的HashMap對象
 * tab 當前HashMap對象的元素數組
 * h   指定key的hash值
 * k   指定key
 * v   指定key上要寫入的值
 * 返回:指定key所匹配到的節點對象,針對這個對象去修改V(返回空說明創建了一個新節點)
 */
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,int h, K k, V v) {
	// 定義k的Class對象
    Class<?> kc = null; 
     // 標識是否已經遍歷過一次樹,未必是從根節點遍歷的,但是遍歷路徑上一定已經包含了後續需要比對的所有節點。
    boolean searched = false;
    // 父節點不爲空那麼查找根節點,爲空那麼自身就是根節點
    TreeNode<K,V> root = (parent != null) ? root() : this; 
    // 從根節點開始遍歷,沒有終止條件,只能從內部退出
    for (TreeNode<K,V> p = root;;) { 
        int dir; // 對比結果 0 1 -1,用來聲明方向
        int ph; // 節點p的hash值(當前節點hash值)
        K pk; // 節點p的key(當前節點的鍵對象)
        if ((ph = p.hash) > h) // 如果當前節點hash 大於 指定key的hash值
            dir = -1; // 要添加的元素應該放置在當前節點的左側
        else if (ph < h) // 如果當前節點hash 小於 指定key的hash值
            dir = 1; // 要添加的元素應該放置在當前節點的右側
        // 如果當前節點的鍵對象 和 指定key對象相同(hashCode 與 equals方法結果一樣,那麼就得覆蓋處理)
        else if ((pk = p.key) == k || (k != null && k.equals(pk))) /
            return p; // 那麼就返回當前節點對象,在外層方法會對v進行寫入
 
        // 走到這一步說明 當前節點的hash值  和 指定key的hash值  是相等的,但是equals不等
        // 打個比方,16個桶,【1,17,33,39】他們的hash值相同,都位於第一個桶裏面,但是equals不等、
        else if ((kc == null &&
                    (kc = comparableClassFor(k)) == null) ||
                    (dir = compareComparables(kc, k, pk)) == 0) {
 
            // 走到這裏說明:指定key沒有實現comparable接口   或者   實現了comparable接口並且和當前節點的鍵對象比較之後相等(僅限第一次循環)
        
 
            /*
             * searched 標識是否已經對比過當前節點的左右子節點了
             * 如果還沒有遍歷過,那麼就遞歸遍歷對比,看是否能夠得到那個鍵對象equals相等的的節點
             * 如果得到了鍵的equals相等的的節點就返回
             * 如果還是沒有鍵的equals相等的節點,那說明應該創建一個新節點了
             */
            if (!searched) { // 如果還沒有比對過當前節點的所有子節點
                TreeNode<K,V> q, ch; // 定義要返回的節點、和子節點
                searched = true; // 標識已經遍歷過一次了
                /*
                 * 紅黑樹也是二叉樹,所以只要沿着左右兩側遍歷尋找就可以了
                 * 這是個短路運算,如果先從左側就已經找到了,右側就不需要遍歷了
                 * find 方法內部還會有遞歸調用。參見:find方法解析
                 */
                if (((ch = p.left) != null &&
                        (q = ch.find(h, k, kc)) != null) ||
                    ((ch = p.right) != null &&
                        (q = ch.find(h, k, kc)) != null))
                    return q; // 找到了指定key鍵對應的
            }
 
            // 走到這裏就說明,遍歷了所有子節點也沒有找到和當前鍵equals相等的節點
            dir = tieBreakOrder(k, pk); // 再比較一下當前節點鍵和指定key鍵的大小
        }
 
        TreeNode<K,V> xp = p; // 定義xp指向當前節點
        /*
        * 如果dir小於等於0,那麼看當前節點的左節點是否爲空,如果爲空,就可以把要添加的元素作爲當前節點的左節點,如果不爲空,還需要下一輪繼續比較
        * 如果dir大於等於0,那麼看當前節點的右節點是否爲空,如果爲空,就可以把要添加的元素作爲當前節點的右節點,如果不爲空,還需要下一輪繼續比較
        * 如果以上兩條當中有一個子節點不爲空,這個if中還做了一件事,那就是把p已經指向了對應的不爲空的子節點,開始下一輪的比較
        */
        if ((p = (dir <= 0) ? p.left : p.right) == null) {  
            // 如果恰好要添加的方向上的子節點爲空,此時節點p已經指向了這個空的子節點
            Node<K,V> xpn = xp.next; // 獲取當前節點的next節點
            TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn); // 創建一個新的樹節點
            if (dir <= 0)
                xp.left = x;  // 左孩子指向到這個新的樹節點
            else
                xp.right = x; // 右孩子指向到這個新的樹節點
            xp.next = x; // 鏈表中的next節點指向到這個新的樹節點
            x.parent = x.prev = xp; // 這個新的樹節點的父節點、前節點均設置爲 當前的樹節點
            if (xpn != null) // 如果原來的next節點不爲空
                ((TreeNode<K,V>)xpn).prev = x; // 那麼原來的next節點的前節點指向到新的樹節點
            moveRootToFront(tab, balanceInsertion(root, x));// 重新平衡,以及新的根節點置頂
            return null; // 返回空,意味着產生了一個新節點
        }
    }
}

=== 點擊查看top目錄 ===

2.3.4 【刪除】
2.3.4.1 圖示與步驟

在這裏插入圖片描述

  • 宏觀步驟:
  1. 按照二叉查找樹的屬性,刪除節點
  2. 按照紅黑樹的規則,通過左旋或者右旋變成一顆紅黑樹。
  • 刪除節點的情形:
  1. 被刪除節點沒有兒子,即爲葉節點。【那麼,直接將該節點刪除就OK了。】
  2. 被刪除節點只有一個兒子。【那麼,直接刪除該節點,並用該節點的唯一子節點頂替它的位置。】
  3. 被刪除節點有兩個兒子。【那麼,先找出它的後繼節點;然後把“它的後繼節點的內容”複製給“該節點的內容”;之後,刪除“它的後繼節點”。】
  • 情形三中有倆子節點,思考一下後繼節點:二叉樹是用中序排序,也就是“左中右”的節點排列順序,那麼右繼節點也就是“右子樹的最左節點”,也就是說,情形三,最終都是轉化爲情形一與情形二。
  • 那麼思考一下:
  1. 假如刪除了上圖“4”,那麼“4”的右子樹的最左節點是“5”,也就是把“4”和“5”換個位置,然後按照“情形1”處理,把節點“5”刪了。
  2. 假如刪除了上圖“14”,那麼“14”的右子樹的最左節點是“13”,那麼也就是“情形2”處理,把後繼節點“11”包括“11”下面整個樹,往上拉上去。
2.3.4.2 代碼(HashMap1.8)
/**
 *  @param map 用於樹化或者鏈化
 *  @param tab 桶數組,該 node 位於某一個桶內
 */
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
                                  boolean movable) {
	    // ====== section 1:通過prev和next刪除當前節點 ======

	    int n;
	    if (tab == null || (n = tab.length) == 0)
	        return;
	    // 獲取桶 index 
	    int index = (n - 1) & hash; // 用與字符取餘,看我之前的文章
	    // first 指向了桶的第一個元素,可能是鏈表頭,也可能是樹的 root節點 ,此處是樹的根節點
	    TreeNode<K,V> first = (TreeNode<K,V>)tab[index],  rl;
	    TreeNode<K,V> root = first; // root 指向了 first
	    TreeNode<K,V> rl; // 
	    // succ 指向了 Node 節點的後繼節點 next,對於二叉查找樹來說,就是右子樹的最左節點 。
	    TreeNode<K,V> succ = (TreeNode<K,V>)next;
	    // pred 指向了 this節點的前驅節點
	    TreeNode<K,V> pred = prev;
	    // 如果前驅結點是 null,那麼他肯定是root節點
	    if (pred == null)
	    	// 首先 first先指向 succ,也就是this節點的後繼節點next
	    	// 然後 tab[index] 指向了 first指向的位置,也就是指向了 this節點的 next 
	        tab[index] = first = succ; // 此處說明一下,假如 succ爲空,那麼代表這棵樹啥都沒了
	    else
	    	// 不是root節點,那麼前驅結點的next後繼指針指向了this的後繼節點,意思就是斷開了this節點
	        pred.next = succ;
	    // 看上面 succ 原本指向了 this節點的後繼節點 next,如果不爲空,說明 succ 是一個節點
	    if (succ != null)
	    	// this的後繼節點的 prev前驅指針指向了 this節點的前驅節點,照樣是斷開了 this節點
	        succ.prev = pred;
	    // 這棵樹啥都不剩下了。直接 return
	    if (first == null)
	    	// ************ 此處有 return ************
	        return;

	    // ====== section 2:當節點數量小於7時轉換成鏈棧的形式存儲  ====== 

	    // root的爸爸不是null,那證明不是根節點,那麼繼續往上面找上去,直到找到真正的 root
	    if (root.parent != null)
	        root = root.root();
	    // 樹是空
	    if (root == null
	    		// 可以移動 並且
                || (movable
                	// root節點的右子樹是空,
                    && (root.right == null
                    	// 或者root節點的左子樹是空 ,rl指向了左子樹
                        || (rl = root.left) == null
                        // 或者左子樹的左子樹是空
                        || rl.left == null))) {
                tab[index] = first.untreeify(map);  // too small ,此輪條件不判斷樹種節點的總數量,只判斷根的左右子樹是否符合鏈化的條件
                // ************ 此處有 return ************
                return;
            }

	    //  ======  section 3:判斷當前樹節點情況  ====== 
	    // p 指向當前節點, pl 當前Node的左子樹,pr當前Node的右子樹
	    TreeNode<K,V> p = this, pl = left, pr = right, replacement;
	    // 有倆孩子,屬於情形三,那麼就必須找到右子樹的最左節點,也就是轉化爲情形1或者情形2
	    if (pl != null && pr != null) {
	    	// s 指向thisNode 的右子樹
	        TreeNode<K,V> s = pr, sl;
	        // s 是爲了找到最左節點
	        while ((sl = s.left) != null) // find successor
	            s = sl;
	        boolean c = s.red; s.red = p.red; p.red = c; // swap colors todo
	        // sr 指向了最左節點的右子樹,因爲 s已經是最左節點了,所以s肯定沒有左子樹
	        TreeNode<K,V> sr = s.right;
	        // pp 指向了 p的父親(p在上面指向了this節點)
	        TreeNode<K,V> pp = p.parent;
	        // s是當前節點右子樹的最左節點,pr是當前節點的右子樹,s == pr 說明右子樹的最左節點就是當前節點的右子樹
	        // demo:看上圖的紅黑樹,假如要刪除“18”,那麼“18”的s 右子樹的最左節點是“19”,pr是當前節點的右子樹也是“19”,那麼他們倆互相換位置
	        if (s == pr) { // p was s's direct parent
	        	// 那麼互換位置
	            p.parent = s;
	            s.right = p;
	        }
	        // 如果不是上面那個情況,比如此時要刪除的是節點“18”,那麼	
	        else {
	        	// s是當前節點右子樹的最左節點,此時s是節點“15”
	        	// sp是節點“15”的爸爸節點“16”,那麼此時要把 節點“18” 和s 節點“15”換個位置
	            TreeNode<K,V> sp = s.parent;
	            // 當前節點與右子樹的最左節點換位置
	            // 當前節點換爸爸
	            if ((p.parent = sp) != null) {
	            	// 替換元素換兒子
	            	// s 是 sp的左孩子,
	                if (s == sp.left)
	                    sp.left = p;
	                // s 是 sp的右孩子    
	                else
	                    sp.right = p;
	            }
	            // s的右指針指向了原先節點的右子樹,也就是接手他的右子樹
	            if ((s.right = pr) != null)
	            	// 原本的右子樹指針換個爸爸
	                pr.parent = s;
	        }
	        // 當前這個要刪除的節點已經換到右子樹的最左節點了,那麼他肯定沒有左孩子
	        p.left = null;
	        // 假如刪除的節點還有右孩子,那麼就接住
	        // 比如要刪除的節點是“4”,那麼把“4”和“5”換位置之後,原先“4”的右子樹還是得接住
	        if ((p.right = sr) != null)
	            sr.parent = p;
	        // s目前已經是替換位置完畢的了,已經上位的了,如果s有左子樹,那麼接住
	        if ((s.left = pl) != null)
	            pl.parent = s;
	        // s目前已經是替換位置完畢的了,如果他的爸爸是null,那麼s就是根節點root
	        if ((s.parent = pp) == null)
	            root = s;
	        // 如果一開始要刪除的節點p位於他父親pp的 左邊,那麼接住
	        else if (p == pp.left)
	            pp.left = s;
	        // 如果一開始要刪除的節點p位於他父親pp的 右邊,那麼也接住
	        else 
	            pp.right = s;
	        // 如果“替換節點”有右孩子,那麼替換他的右邊孩子。
	        // 比如上圖,你要刪除的是p節點“14”,那麼找到右子樹的最左節點s是“11”,那麼此時要把他的右孩子“13”給換上去
	        if (sr != null)
	            replacement = sr;
	        else
	        	// 如果“替換節點”沒有右孩子,那麼替換節點就是她自己
	        	// 比如上圖,你要刪除的p節點是“6”,由於他沒有右邊的孩子,所以把“5”直接換了即可
	            replacement = p;
	    }
	    // 情形1,只有一個孩子,那麼直接替換即可
	    else if (pl != null)
	        replacement = pl;
	    // 情形1,只有一個孩子,那麼直接替換即可
	    else if (pr != null)
	        replacement = pr;
	    // 沒有左子樹 並且沒有右子樹 ,那麼屬於情形1,直接刪除該節點即可
	    else
	        replacement = p;

	    //  ======  section 4:實現刪除樹節點邏輯 ======  

	    if (replacement != p) {
	        TreeNode<K,V> pp = replacement.parent = p.parent;
	        if (pp == null)
	            root = replacement;
	        else if (p == pp.left)
	            pp.left = replacement;
	        else
	            pp.right = replacement;
	        p.left = p.right = p.parent = null;
	    }

	    TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);

	    if (replacement == p) {  // detach
	        TreeNode<K,V> pp = p.parent;
	        p.parent = null;
	        if (pp != null) {
	            if (p == pp.left)
	                pp.left = null;
	            else if (p == pp.right)
	                pp.right = null;
	        }
	    }
	    if (movable)
	        moveRootToFront(tab, r);
	}

未完待續…

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