文章目錄
一、紅黑樹簡介
1.紅黑樹是什麼
之前閱讀HashMap源碼的時候,發現當鏈表長度大於8時會自動轉化成紅黑樹。那時就埋下了一個種子,想要弄清楚紅黑樹到底是什麼,於是就有了現在這一篇博文。
紅黑樹,實際上一種平衡的二叉排序樹。那麼,何爲二叉排序樹?根據百度百科的定義,二叉排序樹有三個特性:
(1) 如果它的左子樹不爲空,那麼左子樹上的所有結點的值都小於根結點的值。
(2) 如果它的右子樹不爲空,那麼右子樹上的所有結點的值都大於根結點的值。
(3) 它的左右子樹都爲二叉排序樹。
這兩個都是二叉排序樹,其中a是完全二叉樹,b是滿二叉樹。
上圖展示的是一個極度不平衡的二叉樹,已然退化成了鏈表,查詢效率很低。那麼,有沒有什麼辦法可以避免出現這種情況,在新增結點的同時保持二叉樹的平衡,不然它退化成鏈表呢?答案是紅黑樹。當然這只是其中一種方法,還有平衡二叉樹AVL等。
2.紅黑樹有什麼性質
紅黑樹具有五個特性:
(1) 結點是紅色或黑色
(2) 根結點是黑色
(3) 每個葉結點(NIL結點,空結點)是黑色的
(4) 每個紅色結點的兩個子結點都是黑色。(從每個葉子到根的所有路徑上不能有兩個連續的紅色結點)
(5) 從任一結點到其每個葉子的所有路徑都包含相同數目的黑色結點
這五個特性可以保證二叉樹的平衡,避免了退化成鏈表的情況。由性質4和性質5可以推出另一個性質:從根結點到葉子的最長可能路徑不大於最短可能路徑的兩倍長,這個性質讓紅黑樹即使在最壞情況下也能保持高效率。證明過程如下:
由性質4“不能有連續的兩個紅色結點”可得,最短路徑是連續的黑色結點。再看性質5 “從任一結點到其每個葉子的所有路徑都包含相同數目的黑色結點”可以知道,黑色結點數與最短路徑相同,要想路徑儘可能地長,就只能在黑色結點之間插入紅色結點,所以最長路徑應是一條紅黑結點交替出現的路徑,不大於最短路徑的兩倍長。
在百度百科紅黑樹的條目下,有提到這麼一段話,筆者覺得不錯,另外分享給大家。
在很多樹數據結構的表示中,一個結點有可能只有一個子結點,而葉子結點不包含數據。用這種範例表示紅黑樹是可能的,但是這會改變一些屬性並使算法複雜。爲此,本文中我們使用 “nil 葉子” 或"空(null)葉子",如上圖所示,它不包含數據而只充當樹在此結束的指示。這些結點在繪圖中經常被省略,導致了這些樹好象同上述原則相矛盾,而實際上不是這樣。與此有關的結論是所有結點都有兩個子結點,儘管其中的一個或兩個可能是空葉子。
3.紅黑樹有什麼操作
在閱讀源碼之前,如果能夠理解紅黑樹的操作,可以很大程度地降低閱讀的難度。
紅黑樹的基本操作和其他樹形結構一樣,一般都包括查找、插入、刪除等操作。
二、紅黑樹源碼
在對紅黑樹的性質、操作有了一定了解之後,我們接着對紅黑樹的源碼進行解讀。紅黑樹是在JDK1.8之後才被引入的數據結構,目的是優化傳統HashMap在哈希衝突頻發時查詢效率很低的問題,時間複雜度由O(n)變爲O(log(n))。
1.紅黑樹結構
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
//父結點
TreeNode<K,V> parent;
//左子結點
TreeNode<K,V> left;
//左子結點
TreeNode<K,V> right;
//前驅結點
TreeNode<K,V> prev; // needed to unlink next upon deletion
//是否是紅色結點
boolean red;
//哈希值(繼承自Map.Entry)
final int hash;
//關鍵字(繼承自Map.Entry)
final K key;
//值(繼承自Map.Entry)
V value;
//後繼結點(繼承自Map.Entry)
Node<K,V> next;
/**
* 調用的是LinkedHashMap.Entry的構造方法,而LinkedHashMap.Entry繼承HashMap.Node並且該構造方法調用的也是super方法
* 所以,實際上是調用HashMap.Node的Node(int hash, K key, V value, Node<K,V> next)方法
*/
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
}
2.putTreeVal方法
/**
* else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
* 調用hashMap的put方法時,如果鏈表已轉化成紅黑樹,則調用此方法。
(如果當前鏈表的長度大於8時,會調用treeifyBin方法將鏈表轉化成紅黑樹TreeNode)
此方法的目的是獲取與傳入hash值和key一一相等的紅黑樹結點,value的替換髮生在put方法裏。
* @param map 哈希表
* @param tab 數組
* @param h 哈希值
* @param k key值
* @param v value值
*/
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
//從根結點開始尋找合適位置的結點
TreeNode<K,V> root = (parent != null) ? root() : this;
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
//如果當前結點的hash值大於待查詢的hash值,則dir標記爲-1
if ((ph = p.hash) > h)
dir = -1;
//如果當前結點的hash值小於待查詢的hash值,則dir標記爲1
else if (ph < h)
dir = 1;
/**
* 如果當前結點的hash值等於待查詢的hash值且當前結點的key等於待查詢的key,則返回當前結點
*/
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
/**
* 如果傳入的key爲不可比較的類或者k和當前結點的k通過compareComparables方法比較後相等,
則繼續往下執行
* /
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
//整個循環只搜索一次,因爲第一次搜索就已經遍歷了全樹
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
/**
* 第一次進入時,p爲根結點
* 如果p的左/右子結點不爲空,且能夠從該結點出發找到hash和key一一相等的結點,
則返回該結點
*/
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;
}
/**
* 這個方法適用於兩個類hash值相等且不可比較的情況,可以得出
下一個結點的插入位置(左-1或右1)
*/
dir = tieBreakOrder(k, pk);
}
TreeNode<K,V> xp = p;
/**
* -1取p的左子結點,1則取p的右子結點
* 在相應位置插入結點,下面會詳細說明這一過程
* /
if ((p = (dir <= 0) ? p.left : p.right) == null) {
Node<K,V> xpn = xp.next;
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
if (dir <= 0)
xp.left = x;
else
xp.right = x;
xp.next = x;
x.parent = x.prev = xp;
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
爲了更好地理解結點是如何通過指針的變化插入到紅黑樹之中的,筆者將結合圖片一步步演示代碼是如何運作的。
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
Node<K,V> xpn = xp.next;
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
不妨假設,dir等於-1且p.left等於null,如上圖所示。
上文中有提到newTreeNode構造器實際上是調用HashMap.Node的Node(int hash, K key, V value, Node<K,V> next)方法,所以x結點的next指針指向了xpn結點。
if (dir <= 0)
xp.left = x;
else
xp.right = x;
xp.next = x;
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
如上圖所示,xp結點的left和next指針發生改變,由紅色虛線改成紅色實線的位置。如果xpn結點不爲空,則它的前驅結點指向x結點。
moveRootToFront(tab, balanceInsertion(root, x));
插入結點後,可能會引起紅黑樹失衡,違背了紅黑樹的五點性質。這時,需要調用moveRootToFront和balanceInsertion方法調整結點位置,使紅黑樹重新平衡。balanceInsertion方法是用於修復新紅黑樹,使得其滿足五點性質。moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root)方法是確保傳入的根結點是數組中的第一個結點。這兩個方法後面會詳細說到。
3.balanceInsertion方法
/**
* 修復紅黑樹
* @param root 根結點
* @param x 從這個結點開始向上修復紅黑樹
*/
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
TreeNode<K,V> x) {
/**
* x結點改爲紅色結點;
* 由此可知,插入的結點一開始都是紅色結點。如果插入的是黑色結點,會使得某一條路徑上的
黑色結點數目增加1,而其他路徑黑色結點的數目保持不變,這就違背了“從任一結點到其每個
葉子的所有路徑都包含相同數目的黑色結點”性質,徒增後續調整的成本。
*/
x.red = true;
/**
* 通過改變x的引用,使得循環繼續,直到x爲根結點或者x的父結點爲根結點。
*/
for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
/**
* 情況1:x爲根結點,根結點爲黑色
*/
if ((xp = x.parent) == null) {
x.red = false;
return x;
}
/**
* 情況2:x的父結點是黑色結點或者x的父結點是根結點(實際上前者包含後者)
*/
else if (!xp.red || (xpp = xp.parent) == null)
return root;
/**
* 如果x的父結點是x的祖父結點的左孩子
*/
if (xp == (xppl = xpp.left)) {
/**
* 情況3:如果x的父結點是x的祖父結點的左孩子,
祖父結點的右孩子不爲空,且爲紅色結點
*/
if ((xppr = xpp.right) != null && xppr.red) {
xppr.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
/**
* 情況4:如果x的父結點是x的祖父結點的左孩子,
祖父結點的右孩子爲空,且x是父結點的右孩子
*/
if (x == xp.right) {
/**
* 左旋,將xp旋轉爲其右孩子的左孩子
* 這裏的x = xp是爲了改變x的引用,使得下一次循環從x的父結點開始
* x = xp並不會把實際的x改成xp,因爲當方法參數爲引用變量時,實際上傳的是變量引用地址的副本。
這裏的x = xp只是修改了副本的值,對實際x的引用地址沒有影響。
*/
root = rotateLeft(root, x = xp);
//因爲x=xp,所以需要重新定義xp和xpp結點
xpp = (xp = x.parent) == null ? null : xp.parent;
}
/**
* 情況5:如果x的父結點是x的祖父結點的左孩子,
祖父結點的右孩子爲空,且x是父結點的左孩子
*/
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
/**
* 右旋,將xp旋轉爲其左孩子的右孩子
*/
root = rotateRight(root, xpp);
}
}
}
}
/**
* 如果x的父結點是x的祖父結點的右孩子
*/
else {
/**
* 情況6:如果x的父結點是x的祖父結點的右孩子,
祖父結點的左孩子不爲空,且爲紅色結點
*/
if (xppl != null && xppl.red) {
xppl.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
/**
* 情況7:如果x的父結點是x的祖父結點的右孩子,
祖父結點的左孩子爲空,且x是父結點的左孩子
*/
if (x == xp.left) {
/**
* 左旋,將xp旋轉爲其右孩子的左孩子
* 這裏的x = xp是爲了改變x的引用,使得下一次循環從x的父結點開始
* x = xp並不會把實際的x改成xp,因爲當方法參數爲引用變量時,實際上傳的是變量引用地址的副本。
這裏的x = xp只是修改了副本的值,對實際x的引用地址沒有影響。
*/
root = rotateRight(root, x = xp);
//因爲x=xp,所以需要重新定義xp和xpp結點
xpp = (xp = x.parent) == null ? null : xp.parent;
}
/**
* 情況8:如果x的父結點是x的祖父結點的右孩子,
祖父結點的左孩子爲空,且x是父結點的右孩子
*/
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
/**
* 左旋,將xp旋轉爲其右孩子的左孩子
*/
root = rotateLeft(root, xpp);
}
}
}
}
}
}
總結:
修復紅黑樹的情況有很多,光是看着註釋是很難理解的。最好還是要對着每一種情況去模擬結點的變化過程,在草稿紙上對着一行行代碼畫出此時的紅黑樹結構,畫完後就會有一種豁然開朗的感覺。
4.rotateLeft和rotateRight方法
/**
* 左旋,將p旋轉爲其右孩子的左孩子
* @param root 根結點
* @param x 以這個結點爲中心開始旋轉
*/
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
TreeNode<K,V> p) {
TreeNode<K,V> r, pp, rl;
if (p != null && (r = p.right) != null) {
if ((rl = p.right = r.left) != null)
rl.parent = p;
if ((pp = r.parent = p.parent) == null)
(root = r).red = false;
else if (pp.left == p)
pp.left = r;
else
pp.right = r;
r.left = p;
p.parent = r;
}
return root;
}
分爲兩步:
1.將p的右孩子r的左孩子rl引用指向p
2.如果原先rl引用指向的結點不爲空,則把該結點的引用指向p的右孩子
/**
* 右旋,將p旋轉爲其左孩子的右孩子
* @param root 根結點
* @param x 以這個結點爲中心開始旋轉
*/
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
TreeNode<K,V> p) {
TreeNode<K,V> l, pp, lr;
if (p != null && (l = p.left) != null) {
if ((lr = p.left = l.right) != null)
lr.parent = p;
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;
}
分爲兩步:
1.將p的左孩子l的右孩子lr引用指向p
2.如果原先lr引用指向的結點不爲空,則把該結點的引用指向p的左孩子
5.moveRootToFront方法
/**
* 確保給定的根結點是數組中的第一個結點
*/
static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
int n;
if (root != null && tab != null && (n = tab.length) > 0) {
int index = (n - 1) & root.hash;
TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
if (root != first) {
Node<K,V> rn;
tab[index] = root;
TreeNode<K,V> rp = root.prev;
if ((rn = root.next) != null)
((TreeNode<K,V>)rn).prev = rp;
if (rp != null)
rp.next = rn;
if (first != null)
first.prev = root;
root.next = first;
root.prev = null;
}
/**
* 檢查此時的紅黑樹是否符合紅黑樹的性質
如果不符合,程序會拋出AssertionError,並終止執行。
*/
assert checkInvariants(root);
}
}
假設給定的根結點root不是數組的第一個結點first,那麼會把root結點取出,放到first的前面。如下圖所示:
6.checkInvariants方法
/**
* 檢查此時的紅黑樹是否符合紅黑樹的性質
如果不符合,程序會拋出AssertionError,並終止執行。
* 一般從根結點開始,採用遞歸的方式檢查每一個結點
* @param t 從該結點開始檢查
*/
static <K,V> boolean checkInvariants(TreeNode<K,V> t) {
TreeNode<K,V> tp = t.parent, tl = t.left, tr = t.right,
tb = t.prev, tn = (TreeNode<K,V>)t.next;
//t的前驅結點不爲空,並且它的後繼節點不爲t
if (tb != null && tb.next != t)
return false;
//t的後繼結點不爲空,並且它的前驅節點不爲t
if (tn != null && tn.prev != t)
return false;
//t的父結點不爲空,並且它的左右孩子都不是t(相當於認了個乾爹)
if (tp != null && t != tp.left && t != tp.right)
return false;
//t的左孩子不爲空,與此同時,它的父結點不是t或者它的哈希值大於t的哈希值
if (tl != null && (tl.parent != t || tl.hash > t.hash))
return false;
//t的右孩子不爲空,與此同時,它的父結點不是t或者它的哈希值小於t的哈希值
if (tr != null && (tr.parent != t || tr.hash < t.hash))
return false;
//t、t的左孩子、t的右孩子都不爲空且都是紅色結點
if (t.red && tl != null && tl.red && tr != null && tr.red)
return false;
//如果t的左孩子不爲空,則繼續從左孩子開始檢查
if (tl != null && !checkInvariants(tl))
return false;
//如果t的右孩子不爲空,則繼續從右孩子開始檢查
if (tr != null && !checkInvariants(tr))
return false;
return true;
}
插入小結:
1.找到插入位置,根據給定的哈希值、value值、key值初始化一個結點並插入到該位置
2.調用balanceInsertion方法修復因爲插入新結點而可能失衡的紅黑樹
3.調用moveRootToFront方法,確保調用balanceInsertion後的根結點是數組的第一個元素
4.在moveRootToFront方法的最後會檢查是否符合紅黑樹的特性,如果不符合,則報錯並終止程序
7.getTreeNode方法
/**
* if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
如果該結點是紅黑樹結點,則調用getTreeNode獲取與傳入的哈希值和key值一一匹配的結點
*/
final TreeNode<K,V> getTreeNode(int h, Object k) {
//從根結點開始搜索
return ((parent != null) ? root() : this).find(h, k, null);
}
/**
* Finds the node starting at root p with the given hash and key.
* The kc argument caches comparableClassFor(key) upon first use
* comparing keys.
* 根據給定的hash值和關鍵字從根結點開始尋找一個結點。
* kc是key的class
*/
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
TreeNode<K,V> p = this;
do {
/**
* ph 當前結點hash值
* dir
* pk 當前結點key值
*/
int ph, dir; K pk;
/**
* pl 當前結點左孩子
* pr 當前結點右孩子
*/
TreeNode<K,V> pl = p.left, pr = p.right, q;
//如果當前結點的hash值大於待查詢的hash值
if ((ph = p.hash) > h)
//準備尋找當前結點的左孩子
p = pl;
//如果當前結點的hash值小於待查詢的hash值
else if (ph < h)
//準備尋找當前結點的右孩子
p = pr;
//如果當前結點的hash值等於待查詢的hash值且當前結點的key值等於待查詢的key值,則返回當前結點
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
//如果當前結點的左孩子爲空
else if (pl == null)
//準備尋找當前結點的右孩子
p = pr;
//如果當前結點的右孩子爲空
else if (pr == null)
//準備尋找當前結點的左孩子
p = pl;
//如果傳入的key不爲空且是可比較的類,k和當前結點的k通過compareComparables方法比較後不相等
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
//根據compareComparables方法的返回值決定繼續尋找的方向(左孩子還是右孩子)
p = (dir < 0) ? pl : pr;
//如果當前結點的右子樹中存在這樣的結點就返回該結點
else if ((q = pr.find(h, k, kc)) != null)
return q;
else
//如果當前結點的右子樹中不存在這樣的結點,就繼續尋找當前結點的左孩子
p = pl;
} while (p != null);
return null;
}
8.removeTreeNode方法
/**
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
如果該結點是紅黑樹結點,則調用removeTreeNode刪除結點。node爲待刪除結點
* @param map
* @param tab
* @param movable
*/
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
boolean movable) {
int n;
//如果是紅黑樹爲空,則不需要刪除結點
if (tab == null || (n = tab.length) == 0)
return;
/*
* 通過按位與操作算出該結點在數組中的下標
該算法與取餘效果一樣,但效率比取餘高
*/
int index = (n - 1) & hash;
//first、root是桶中的頭結點
TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
//succ是待刪除結點的後繼節點,pred是待刪除結點的前驅結點
TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
//如果pred等於空(待刪除結點爲根結點)
if (pred == null)
/*
* tab[index]、first都指向待刪除結點的後繼節點
*/
tab[index] = first = succ;
else
//如果pred不爲空,則待刪除結點的前驅節點的後繼結點指向待刪除結點的後繼節點
pred.next = succ;
//如果succ不爲空,則待刪除結點的後繼節點的前驅結點指向待刪除結點的前驅節點
if (succ != null)
succ.prev = pred;
//如果頭結點爲空,則結束程序
if (first == null)
return;
//如果頭結點有父結點,則root重新指向根結點
if (root.parent != null)
root = root.root();
/**
* 如果根結點爲空,或者根結點的右孩子爲空,或者根節點的左孩子爲空
或者根結點的左孩子的左孩子爲空,則把紅黑樹轉成鏈表
* 當結點數量太少的情況下,鏈表的查詢速度要優於紅黑樹,維護成本較低
*/
if (root == null || root.right == null ||
(rl = root.left) == null || rl.left == null) {
tab[index] = first.untreeify(map); // too small
return;
}
/**
* 上面這一段只是修改了待刪除結點前驅後繼結點的指針,
對實際的結點位置並沒有產生影響,待刪除結點仍在紅黑樹中。
*/
//p是待刪除結點,pl是待刪除結點的左孩子,pr是待刪除結點的右孩子
TreeNode<K,V> p = this, pl = left, pr = right, replacement;
//待刪除結點的左右孩子都不爲空
if (pl != null && pr != null) {
TreeNode<K,V> s = pr, sl;
//尋找待刪除結點右子樹中最左邊葉子的父結點s,也就是右子樹中哈希值最小的結點
while ((sl = s.left) != null) // find successor
s = sl;
//1.將s的顏色與待刪除結點對調
boolean c = s.red; s.red = p.red; p.red = c; // swap colors
//2.將s的右孩子、父結點指針與待刪除結點的右孩子、父結點指針對調
TreeNode<K,V> sr = s.right;
TreeNode<K,V> pp = p.parent;
if (s == pr) { // p was s's direct parent
p.parent = s;
s.right = p;
}
else {
TreeNode<K,V> sp = s.parent;
if ((p.parent = sp) != null) {
if (s == sp.left)
sp.left = p;
else
sp.right = p;
}
if ((s.right = pr) != null)
pr.parent = s;
}
//3.將s的左孩子指向待刪除結點的左孩子,待刪除結點的左孩子指向NIL結點
p.left = null;
if ((p.right = sr) != null)
sr.parent = p;
if ((s.left = pl) != null)
pl.parent = s;
if ((s.parent = pp) == null)
root = s;
else if (p == pp.left)
pp.left = s;
else
pp.right = s;
if (sr != null)
replacement = sr;
else
replacement = p;
}
//如果待刪除結點的左孩子不爲空,則將其設爲代替結點replacement
else if (pl != null)
replacement = pl;
//如果待刪除結點的右孩子不爲空,則將其設爲代替結點replacement
else if (pr != null)
replacement = pr;
//如果待刪除結點是葉子結點,則代替結點是它本身
else
replacement = p;
//如果代替結點不爲葉子結點,則開始刪除待刪除結點
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);
}
/**
* 將紅黑樹轉成鏈表
*/
final Node<K,V> untreeify(HashMap<K,V> map) {
Node<K,V> hd = null, tl = null;
for (Node<K,V> q = this; q != null; q = q.next) {
//根據q結點返回一個新的結點 new Node<>(q.hash, q.key, 0q.value, next);
Node<K,V> p = map.replacementNode(q, null);
if (tl == null)
hd = p;
else
tl.next = p;
tl = p;
}
return hd;
}
/**
* 修復紅黑樹,返回新的根結點
* @param root 根結點
* @param x 從這個結點開始向上修復紅黑樹
*/
static <K,V> TreeNode<K,V> balanceDeletion(TreeNode<K,V> root,
TreeNode<K,V> x) {
for (TreeNode<K,V> xp, xpl, xpr;;) {
//情況1:如果x爲空或根結點
if (x == null || x == root)
return root;
//情況2:如果x的父結點爲空
else if ((xp = x.parent) == null) {
//x塗爲黑色結點,並作爲新的根結點
x.red = false;
return x;
}
//情況3:如果x爲紅色結點,則塗黑
else if (x.red) {
x.red = false;
return root;
}
//情況4:如果x是父結點的左孩子
else if ((xpl = xp.left) == x) {
//情況4.1:x的兄弟結點不爲空且爲紅色結點
if ((xpr = xp.right) != null && xpr.red) {
xpr.red = false;
xp.red = true;
root = rotateLeft(root, xp);
xpr = (xp = x.parent) == null ? null : xp.right;
}
//情況4.2:x的兄弟結點爲空
if (xpr == null)
x = xp;
//情況4.3:x的兄弟結點不爲空且爲黑色結點
else {
TreeNode<K,V> sl = xpr.left, sr = xpr.right;
//情況4.3.1:x的兄弟結點左右孩子均不爲紅色結點
if ((sr == null || !sr.red) &&
(sl == null || !sl.red)) {
xpr.red = true;
x = xp;
}
//情況4.3.2:x的兄弟結點左右孩子至少有一個是紅色結點
else {
/**
* x兄弟結點的右孩子爲空或者是黑色結點
*/
if (sr == null || !sr.red) {
if (sl != null)
sl.red = false;
xpr.red = true;
root = rotateRight(root, xpr);
xpr = (xp = x.parent) == null ?
null : xp.right;
}
if (xpr != null) {
xpr.red = (xp == null) ? false : xp.red;
if ((sr = xpr.right) != null)
sr.red = false;
}
if (xp != null) {
xp.red = false;
root = rotateLeft(root, xp);
}
x = root;
}
}
}
//情況5:如果x是父結點的右孩子
else { // symmetric
//情況5.1:x的兄弟結點不爲空且爲紅色結點
if (xpl != null && xpl.red) {
xpl.red = false;
xp.red = true;
root = rotateRight(root, xp);
xpl = (xp = x.parent) == null ? null : xp.left;
}
//情況5.2:x的兄弟結點爲空
if (xpl == null)
x = xp;
//情況5.3:x的兄弟結點不爲空且爲黑色結點
else {
TreeNode<K,V> sl = xpl.left, sr = xpl.right;
//情況5.3.1:x的兄弟結點左右孩子均不爲紅色結點
if ((sl == null || !sl.red) &&
(sr == null || !sr.red)) {
xpl.red = true;
x = xp;
}
//情況5.3.2:x的兄弟結點左右孩子至少有一個是紅色結點
else {
if (sl == null || !sl.red) {
if (sr != null)
sr.red = false;
xpl.red = true;
root = rotateLeft(root, xpl);
xpl = (xp = x.parent) == null ?
null : xp.left;
}
if (xpl != null) {
xpl.red = (xp == null) ? false : xp.red;
if ((sl = xpl.left) != null)
sl.red = false;
}
if (xp != null) {
xp.red = false;
root = rotateRight(root, xp);
}
x = root;
}
}
}
}
}
總結:
相對於插入操作,刪除操作更爲複雜。在閱讀完源碼後,筆者對刪除的一系列操作有了一定的認知。刪除結點,實際上是從紅黑樹中刪除具有特定哈希值的結點,這個過程一共分爲五個步驟:
此時,不妨假設待刪除結點爲X,X的後繼節點爲nextNode結點,X的前驅結點爲prevNode結點。
1、上文中有提到紅黑樹結點具有next和prev指針,分別指向後繼節點和前驅結點。這一步是將nextNode結點的prev指針指向prevNode結點,將prevNode結點的next指針指向nextNode。
2、如果紅黑樹結點數量太少的話,會將紅黑樹轉化成鏈表,提升查詢速度,不需要進行後續操作。
3、如果X的左右孩子均不爲空,則尋找X結點右子樹中最左邊葉子的父結點s(右子樹中哈希值最小的結點),將s結點和x結點互換位置,此時X結點仍在紅黑樹中;如果X只有一個孩子,那麼這個孩子將替換掉X,此時X結點已經從紅黑樹中刪除了。
4、如果X爲紅色,則直接刪除即可;如果X爲黑色,則需要調用balanceDeletion方法調整紅黑樹結構。如果刪除黑色結點,會使得某一條路徑上的黑色結點數量減少1,違背了“從任一結點到其每個葉子的所有路徑都包含相同數目的黑色結點”性質,所以需要修復紅黑樹。
5、如果X左右孩子均不爲空,在修復紅黑樹後,還需要將X結點從紅黑樹中刪除。
以上就是刪除結點的大致步驟。
接下來,該說一說修復紅黑樹了。一開始,閱讀源碼並不順利,源碼艱澀難懂,看得人頭皮發麻,幾經放棄。後來,筆者還是咬咬牙關,在草稿紙上一遍遍模擬源碼的運行過程,耐心地品味源碼設計的用意,漸漸地對這一塊有了更深入地瞭解。所以,希望讀者不要輕言放棄,無從下手的時候,多多在草稿紙上畫圖,熟悉之後也就那麼一回事。
字醜見諒。回到正題,如果待刪除結點是紅色結點,直接刪除就可以,並不會違背性質。如果待刪除結點是黑色結點,就需要分情況討論了。筆者根據balanceDeletion源碼,將待刪除結點爲黑色結點時的場景分爲8種。如果漏了某一種,還需要讀者可以指正,大家共同進步。此時,不妨設待刪除結點爲X,待替換結點爲S。
1、X爲根結點。這種情況直接刪除即可。
2、X是紅色結點。這種情況也是直接刪除。
3、X是黑色結點,這種情況較爲複雜。刪除了黑色結點會導致某一條路徑上的黑色結點數量減少一,違背了“從任一結點到其每個葉子的所有路徑都包含相同數目的黑色結點”性質。通過閱讀源碼,可知道此時是以X兄弟結點的顏色以及它左右孩子的顏色爲依據進行劃分,再根據此時的情況採用不同的方法調整紅黑樹結構。不妨設X兄弟結點爲B(brother),B的左孩子爲bl,右孩子爲br
3.1、B是紅色結點
3.2、B是黑色結點,且bl、br都是黑色結點
3.3、B是黑色結點,且bl、br都是紅色結點
3.4、B是黑色結點,且bl是黑色結點,br是紅色結點
3.5、B是黑色結點,且bl是紅色結點,br是黑色結點
筆者看過很多詳細講解調整過程的文章,大多看完後仍然是一頭霧水,倒不如自己在草稿紙上就着這五種情況對着源碼畫出紅黑樹的變換過程,這樣更能加深理解。所以,筆者在這裏就不對這五種情況進行說明了,希望讀者可以動起手來。