(這篇文章暫時鴿了,有點理解不能,點進來的小伙伴可以撤了)
剛開始准備在HashMap中直接把紅黑樹也過了的,結果發現這個類不是一般的麻煩,所以單獨開一篇。
由於紅黑樹之前完全沒接觸過,所以這篇博客相當於探索(其實之前的博客都是邊看源碼邊寫的,全是探索)。
紅黑樹沒見過,樹我還是知道的,所以先上一張帥圖:
紅黑樹在這個基本樹的基礎上還多了red,暫時不知道啥意思,慢慢探索。
先來一個類總覽:
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { // ... }
這個紅黑樹繼承了一個另外一個類中的靜態內部類:
static class Entry<K,V> extends HashMap.Node<K,V> {}
這個類也繼承了一個靜態內部類,竟然是HashMap中的Node,真是無語的循環!
這些東西雖然繞來繞去,但是總的特性就是兩個字:鏈表!!!!!!!!!
變量
廢話不多說,首先來看一個這個類的內部變量:
// 父節點 TreeNode<K,V> parent; // 左右子節點 TreeNode<K,V> left; TreeNode<K,V> right; // ??? TreeNode<K,V> prev; // 紅黑樹的精髓 => red! boolean red;
這些節點的意思都比較直接,按理講在正常的樹中只有父、左、右三個,這里的prev和red暫時不清楚干嘛用的。
構造函數
接下來是構造函數
TreeNode(int hash, K key, V val, Node<K,V> next) { super(hash, key, val, next); }
super!然后我跑去看了下LinkedHashMap.Entry的構造函數,還是super!!!!
繞了一圈,最后還是回到了Node的構造函數,如下:
Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; }
沒啥好講的。
需要注意的是,靜態內部類都是作為工具來使用的,所以不從常規的添加節點、查詢來講解,直接從鏈表轉紅黑樹的方法入手,看到什么方法講什么方法,Let's go!
// tab為HashMap的數組 final void treeify(Node<K,V>[] tab) { TreeNode<K,V> root = null; // 這里的this是需要樹轉換數組索引處的第一個鏈表元素 for (TreeNode<K,V> x = this, next; x != null; x = next) { // 依次往上獲取節點 next = (TreeNode<K,V>)x.next; x.left = x.right = null; // 第一個元素被設置為樹的根節點 if (root == null) { x.parent = null; x.red = false; root = x; } else { K k = x.key; int h = x.hash; Class<?> kc = null; for (TreeNode<K,V> p = root;;) { int dir, ph; K pk = p.key; // 根據hash值的大小區分左右 if ((ph = p.hash) > h) dir = -1; else if (ph < h) dir = 1; // 當出現hash碰撞 暫時不管這個 else if ((kc == null && (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0) dir = tieBreakOrder(k, pk); // 這里的xp變成了root TreeNode<K,V> xp = p; // 這個表達式不加括號看起來真是惡心 // 根據dir判斷root.left或者root.right是否為null if ((p = (dir <= 0) ? p.left : p.right) == null) { // 設置下一個元素的parent為root x.parent = xp; // 設置root的left或right if (dir <= 0) xp.left = x; else xp.right = x; root = balanceInsertion(root, x); break; } } } } moveRootToFront(tab, root); }
在前面的轉換中,其實參數tab並沒有用上,所以暫時只需要關注鏈表本身。
在balanceInsertion方法之前,只完成了兩件事:
1、將鏈表的第一個元素設置為根節點
2、將第二個元素的hash與根節點做比較,然后設置根節點的left或right為該元素
畫個帥圖:
接下來看balanceInsertion方法做了什么,該方法接受兩個參數:根節點、根節點的left(right)節點。
這個方法真長,讓我深深的吸了一口氣。
static <K, V> TreeNode<K, V> balanceInsertion(TreeNode<K, V> root,TreeNode<K, V> x) { // 紅屬性 x.red = true; // xp => x的父節點 // xpp => xp的父節點 // xppl => xpp.left // xppr => xpp.right for (TreeNode<K, V> xp, xpp, xppl, xppr;;) { // 當x為根節點時 // 這時xp已經被賦值 xp => root if ((xp = x.parent) == null) { x.red = false; return x; } // 根節點red為false 所以直接返回root else if (!xp.red || (xpp = xp.parent) == null) return root; // more code... } }
很遺憾,返回的特別快,這里的x為根節點的子節點,而根節點的父節點為null,所以這里直接返回root,返回后break,進入下一個循環。
總的來說,這個函數在第一次什么都沒有做。
下一次循環時,x為鏈表中第三個元素,這里就存在一種新情況:dir的值。
首先當dir的值與上次不同時,我們假設在第二次判斷中,x的hash值小於根節點root,於是dir為-1,這樣就有:
xp.left = x;
而第三次,x的hash值比根節點大,而root.right此時仍為null,所以有
xp.right = x;
這樣,根節點的兩個兒子就集齊了。
另外一種情況就是dir的值與上次相同,此時p.left即root.left不為null,所以會進入下一輪內循環。此時的p不再是root,而是root.left,即第二個鏈表元素。
同樣,第二個鏈表元素作為父節點與當前節點的hash作比較,然后設置對應的left/right。
此時,balanceInsertion函數就會進入下一個判斷分支:
static <K, V> TreeNode<K, V> balanceInsertion(TreeNode<K, V> root,TreeNode<K, V> x) { // 紅屬性 x.red = true; for (TreeNode<K, V> xp, xpp, xppl, xppr;;) { if ((xp = x.parent) == null) {/**/} else if (!xp.red || (xpp = xp.parent) == null){/**/ } // 此時xpp為root 父元素正好等於root.left if (xp == (xppl = xpp.left)) { // 此時xpp.right還沒有值 if ((xppr = xpp.right) != null && xppr.red) { // ... } else { if (x == xp.right) { // ... } if (xp != null) { // ... } } } else { // ... } } }
這里的分支特別多。。。先看看當前的情況,並假設2次都是left,父、父父均無右節點,如圖:
一個一個情況的討論,反正基本的塞節點已經明白了。這種情況下,會進入如下分支:
if (xp == (xppl = xpp.left)) { // 此時xpp.right還沒有值 if ((xppr = xpp.right) != null && xppr.red) { // ... } else { if (x == xp.right) { // ... } if (xp != null) { // 父節點黑了 xp.red = false; if (xpp != null) { // 父父節點紅了 xpp.red = true; root = rotateRight(root, xpp); } } } } else { // ... }
這種情況下,將父節點置黑,父父節點置紅,並調用另外一個方法rotateRight。
直接看這個方法,接受兩個參數:根節點、父父節點(還是root):
static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,TreeNode<K,V> p) { TreeNode<K,V> l, pp, lr; if (p != null && (l = p.left) != null) { // p.left = l.right if ((lr = p.left = l.right) != null) lr.parent = p; // l.parent = p.parent if ((pp = l.parent = p.parent) == null) (root = l).red = false; else if (pp.right == p) pp.right = l; else pp.left = l; l.right = p; p.parent = l; } return root; }
這個函數應該是叫做向右翻轉紅黑樹,在理解的時候盡量不要把p當做根節點,而是一個普通的節點。
另外,這里就直接講解各種情況下的翻轉效果。
每一個if語句中的賦值都會改變樹的結構,這里不太好講,用圖來一步一步解釋,當前例子:
可見,翻轉后,原來的root被轉移到了l的右邊,l變成了新的root且red被置false,函數返回新的root。
現在討論更加普遍的情況,首先看在什么情況下會調用右翻轉,將上一個函數中的第一個判斷分支抽出如下:
// true xp == (xppl = xpp.left) // false (xppr = xpp.right) != null && xppr.red // true xp != null // true xpp != null
false代表這是else分支。
1、父元素為父父元素的left
2、父父元素的right為null或者父父元素的red為真
3、父元素及父父元素均不為null
很明顯,上面的翻轉符合這個條件,這里還有另外兩種情況,即:
、
針對這兩種情況的翻轉給出對應的結果圖:
、
可以看出,在p為根節點時,l會被轉換為新的根節點,並且有:
l.right = p;
p.left = l.right
然而,在p不為根節點時,情況稍微會不一樣:
這種情況下不會根節點替換,僅僅是p與l進行換位。
下面來看第二個分支:
// false xp == (xppl = xpp.left) // false xppl != null && xppl.red // true x == xp.left
即:
1、父元素為父父元素的right
2、父父元素的left為null或者red為真
3、當前元素為父元素的left
這里的參數不太一樣:
root = rotateRight(root, x = xp);
直接看圖吧!
我要瘋了!!!!
技術太渣,暫時不搞這個數據結構。。。