Java基礎系列--HashMap(JDK1.8)
Java基礎系列-HashMap 1.8
概述
HashMap是基於雜湊表實現的對映集合。 HashMap可以擁有null鍵和null值,但是null鍵只能有一個,null值不做限制。HashTable是不允許null鍵和值的。 HashMap是非執行緒安全的集合,HashTable是添加了同步功能的HashMap,是執行緒安全的。 HashMap是無序的,並不能保證其內部鍵值對的順序。 HashMap提供了常量級複雜度的元素獲取和新增操作(當然是在hash分散均勻的情況下)。 HashMap有兩個影響功能的因素:初始容量與負載因子,當集合中的元素數量超過了初始容量和負載因子的乘積值時,會觸發resize擴容 HashMap預設的初始容量是16,負載因子是0.75 HashMap在連結串列新增元素是採用尾插法,之前的版本採用頭插法,因為會導致迴圈連結串列的問題,改成了尾插法,並添加了紅黑樹來優化連結串列過長的情況下查詢慢的問題 HashMap底層結構為陣列+連結串列/紅黑樹 HashMap底層連結串列的元素達到8個的情況下,如果HasnMap內部桶個數(即桶容量)達到64個則進行樹形化,否則進行resize擴容
常量/變數解析
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { //... // 預設的初始容量,值為16,如果自定義也必須為2的次冪 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 最大容量值 static final int MAXIMUM_CAPACITY = 1 << 30; // 預設的負載因子,值為0.75,可自定義,必須小於1 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 連結串列轉樹結構的元素數量界限值,當某個hash值下的連結串列元素個數達到8個, // 則將其改為樹結構 static final int TREEIFY_THRESHOLD = 8; // 紅黑樹反轉連結串列的元素數量界限值,當數量小於6的時候才會反轉 static final int UNTREEIFY_THRESHOLD = 6; // 樹形閾值,這個閾值針對的是整個Map中桶的數量,表示只有在所擁有的桶數量 // 達到64時才能執行樹形化,否則先去擴容去吧,可見在桶數小於64時,優先執行擴容 static final int MIN_TREEIFY_CAPACITY = 64; // 表示桶陣列 transient Node<K,V>[] table; // 快取 transient Set<Map.Entry<K,V>> entrySet; // 集合中元素的數量, transient int size; // 集合結構的修改次數,包括集合元素的增刪,和集合結構的變化,僅僅更改已有 // 元素的值並不會增加該值,主要用於Iterator的快速失敗 transient int modCount; // 集合的擴容閾值 int threshold; // 集合的負載因子,預設0.75,時間與空間的折中,增加負載因子, // 能增加元素容納量,減小空間消耗,卻增加的查詢的時間消耗, // 減小負載因子,能減少元素容納量,減少查詢時間消耗,但卻要及早的去擴容, // 增加了空間消耗 final float loadFactor; // ... }
功能解析
新增元素操作
功能描述:
新增新的對映元素(newKey,newValue),首先通過特定的hash演算法計算newKey的hash值(newHash)。
Hash演算法:獲取newKey的hashCode值,然後進行高低位相異或。
hashCode值的獲取方法在Object類中已有定義,當然也有某些類進行了重寫,總的來說有以下幾種:
- String型別的hashCode:自定義演算法較複雜
- 包裝型別的hashCode:當前值
- 其他型別的hashCode:類名[email protected]+記憶體位置的16進製表示
如果是首次新增元素,那麼就意味著桶尚未初始化,所以這裡會先執行初始化操作(resize),
如果初始化成功或者非首次新增元素,那麼開始定位元素的桶位
桶定位演算法:用之前hash演算法的結果newHash與桶的個數-1進行與操作
該演算法的本意是保留newHash值的後幾進位制位來確定桶位,如何保留後幾位呢?我們知道二進位制演算法中1的與操作具有保留原值的效果
這裡正是使用這種原理來實現,假設桶位數為16位,16的二進位制位10000,16-1=15,15的二進位制位就是1111,末四位全是1,通過1的
保留原值的作用,當那它與newHash值的二進位制值進行與操作後,結果就是newHash保留後4位的結果。而4位正好在桶位0-15之內。
而這也就是桶位數必須是2的次冪的原因,因為2的次冪的數字的二進位制值全部是首位為1,其後全是0的值,當其-1之後就會變成首位
變0,其後全是1的值,而桶的下標是從0開始,最高位正好是-1之後的值。
檢視確認桶位是否已有元素,如果沒有,直接存放新元素到該桶位,
如果桶位已有元素存在,那麼就是出現碰撞,這時的解決辦法就是使用連結串列或者紅黑樹來儲存,
如果該桶位儲存的資料結構已經是紅黑樹,那麼執行紅黑樹新增元素操作,
否則執行連結串列的後插法,將新元素插入到連結串列的末尾。
後插法:1.7之前的版本全是前插法,將新元素作為表頭元素,1.8之後改成後插法,將新元素作為表尾元素
在執行後插法的時候需要遍歷連結串列,查詢是否存在相同key的元素,若存在則直接用newValue替換舊值,不再執行插入操作。
新元素插入完成之後,校驗Map中總元素個數是否達到了閾值(這個閾值是桶容量和負載因子的乘積),如果超過閾值則進行擴容。
原始碼解析:
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
//...
public V put(K key, V value) {
// 首先通過hash方法計算hash值,然後執行存值操作
return putVal(hash(key), key, value, false, true);
}
// hash演算法:首先獲取key的hashCode,然後將其高低16位相異或,全員參與(hashcode值的所有二進位制位都能參與hash運算)
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
// 首次新增值執行初始化
n = (tab = resize()).length;
// 定位桶下標,n的值為2的次冪,同時也是桶的數量,hash是之前通過hash演算法得出的結果,n-1之後末幾位全部是1,
// 再和hash與運算,等於保留hash的後幾位作為結果。比如:(1111)&(01010101)的結果為0101,保留了後四位
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 存在相同key的情況(桶位置)
e = p;
else if (p instanceof TreeNode)
// 紅黑樹的情況
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 連結串列的情況
for (int binCount = 0; ; ++binCount) {
// 遍歷連結串列採用尾插法新增新元素
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 存在相同key的情況(連結串列元素位置)
break;
p = e;
}
}
// 針對存在相同key的情況進行統一處理:替換value
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();// 擴容兩倍
afterNodeInsertion(evict);
return null;
}
//...
}
注意:桶容量為2的次冪的原因正是因為便於元素通過位運算實現定位。
初始化/擴容操作
功能描述
執行擴容方法的原因主要是集合中元素數量達到閾值或者是集合桶陣列某個桶位置的元素數量達到8個,但集合桶容量未超過64的情況下,
特殊的情況是首次新增元素時的初始化操作也走這個方法。
針對初始化操作:
只會計算初始化容量和初始化閾值然後建立一個初始桶陣列並返回結果。
對於使用了帶參構造器的情況,會定製初始容量和負載因子,如果只定制了初始容量則使用預設負載因子,
構造器會通過一個進位制運算根據自定義的容量算出一個大於等於自定義容量值的最小的2的次冪值作為真正的容量
比如:自定義容量為10,則計算容量值為16,然後再根據這個容量計算閾值為12。
而針對擴容操作:
首先校驗舊容量是否已經達到或者大於容量最大值MAXIMUM_CAPACITY,如果是則不再進行擴容操作,還在原桶陣列中儲存元素,
並將閾值設定為Integer的最大值,設定為最大值之後就不會再觸發擴容操作(因為Map中元素的總個數最大也就是Integer的最大值了,不可能比之更大),
然後校驗容量加倍後的新容量是否超過容量最大值MAXIMUM_CAPACITY,如果沒有的話則將閾值加倍。
新容量和新閾值都有了,然後建立新的桶陣列,在之後就是元素遷移了。
元素遷移:
遍歷舊桶陣列,校驗每個桶位的元素結構,
如果只有一個元素,直接在新桶陣列進行重定位,定位方式不變,
如果是紅黑樹,走樹結構遷移邏輯,
否則就是連結串列,進行連結串列遷移,連結串列遷移進行了平衡優化,由於新桶陣列和舊陣列的兩倍容量,
我們簡單的將新容量分成相等的兩半,稱之為低位區與高位區,低位區下標與舊陣列相同,
高位區下標為舊陣列下標+舊陣列容量。
連結串列遷移時,會根據該連結串列中元素的鍵的hash值與舊容量進行與運算,這就會有兩個結果,為0或者不為0。
舊容量也是2的次冪,高位為1,其後全是0,比如10000(表示容量為16),將其和hash結果相與,
只會保留舊容量二進位制為1的那一位對應的hash值的那一位,其餘位全變成0,如果hash值的那一
位為0結果就是0,hash值那一位為1結果就是10000。
根據相與的結果來進行連結串列分拆,將結果為0的連結串列元素還定位到相同的桶下標位,即新桶陣列的低位區,將結果為1的連結串列元素定位到原下標+舊桶容量的位置,即高位區。
這兩個連結串列會先組裝連結串列結構,然後將連結串列表頭元素定位到低位區或高位區。
原始碼解析
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
//...
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;// 舊容量
int oldThr = threshold;// 舊閾值
int newCap, newThr = 0;// 新容量、新閾值
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
// 如果舊容量已經達到最大容量值,將閾值設定為最大值,返回舊桶陣列
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 如果加倍後的新容量沒有超過最大容量,且舊容量大於等於16,則新閾值加倍
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
// 只在通過帶引數的構造器(初始容量和負載因子)
// 建立的容器首次新增元素進行桶陣列初始化時會走這裡
newCap = oldThr;
else { // zero initial threshold signifies using defaults
// 初始化容量和閾值,這就是首次新增元素時執行的初始化邏輯
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {// 計算新閾值
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];// 建立桶陣列
table = newTab;
// 如果是初始化操作,此處oldTab為null,會直接返回新建桶陣列,否則執行元素遷移
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
// 針對桶位置只有一個元素的情況,直接重定位元素,定位模式一致
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
// 針對紅黑樹的情況
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order 針對連結串列的情況
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {// 遍歷舊桶位的舊連結串列
next = e.next;
// 這個判斷的結果取決於在於hash值在於oldCap的1所在進位制位對應的進位制位是1還是0,
// 由於oldCap只有這一位為1,那麼hash的該位將保留原值,其餘位全部得0,增加這麼
// 一個貌似隨機的判斷,用於進一步分散元素到不同的桶。
// 其實就是將舊桶第i位桶的連結串列元素分散到新桶的第i和第i+oldCap桶位上,為0還是為1隨機
// 在迴圈中形成兩個小連結串列,然後將首個元素賦值給新桶的對應桶位即可。
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 定位兩個小連結串列的首元素
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
//...
}
獲取元素操作
操作描述
簡單看程式碼
原始碼解析
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
//...
public V get(Object key) {
Node<K,V> e;
// 計算key的hash值用作桶定位的基礎
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 只有一個元素、連結串列頭元素、樹根元素要單獨校驗
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 存在連結串列或者樹結構的情況
if ((e = first.next) != null) {
if (first instanceof TreeNode)
// 執行樹結構獲取元素邏輯
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 否則就是連結串列結構,遍歷連結串列尋找匹配的元素
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
//...
}
移除元素操作
操作描述
簡單看原始碼
原始碼解析
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
//...
public V remove(Object key) {
Node<K,V> e;
// 計算key的hash值,用作後面定位元素桶位的基礎
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
// 先進行桶定位,定位方式不變
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
// 針對只有一個元素、連結串列頭元素、樹根元素進行處理
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
// 如果第一個元素不是,針對連結串列和樹結構進行後續元素處理
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
// 執行樹結構獲取元素邏輯
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
// 連結串列迴圈獲取等key的元素
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
// 執行樹結構刪除元素操作
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
// 針對連結串列頭和樹根元素的情況
tab[index] = node.next;
else
// 針對連結串列內元素進行刪除
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
//...
}
紅黑樹
樹形化操作
操作描述
參照原始碼
原始碼解析
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
//...
// 樹形化準備
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
// 對於觸發了樹形化操作,但是桶容量還沒達到64的情況下優先去做擴容處理,擴容也會分拆連結串列
resize();
// 定位要做樹形下的桶位置,獲取桶位元素e
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
// 迴圈遍歷連結串列中的元素,將其改造成為雙向連結串列結構,表頭元素為hd
do {
// 將e元素封裝成為樹節點TreeNode
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
// 執行樹形化
hd.treeify(tab);
}
}
// 將Node節點封裝成樹節點
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
return new TreeNode<>(p.hash, p.key, p.value, next);
}
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
//...
// 樹形化操作
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;//代表根節點
// 此處迴圈將this賦值給x,this代表的是當前樹節點,這個類是HashMap的內部類用於標識樹節點,
// this就是當前類的例項,也就是一個樹節點,但是是哪個樹節點,就要依靠之間的程式碼上下文來判
// 斷了,看看呼叫該方法的地方有這樣的程式碼:hd.treeify(tab);這就表示當前節點就是那額hd節
// 點,而這個hd節點就是之前改造好的雙向連結串列的表頭結點
// 這裡迴圈的是雙向連結串列中的元素
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) {
// root == null的情況是連結串列頭結點的時候才會出現,這時候將這個頭結點作為樹根節點
x.parent = null;//根節點無父節點
x.red = false;//黑色
root = x;//賦值
}
else {
// 這裡只有非連結串列頭節點才能進來
K k = x.key;
int h = x.hash;
Class<?> kc = null;
// 此處迴圈的是已構建的紅黑樹的節點,從根節點開始,遍歷比較當前連結串列節點與當前紅黑樹節點的
// hash值,dir用於儲存比較結果,如果當前連結串列節點小,則dir為-1,否則為1,實際情況卻是,能
// 撥到同一個桶位的所有元素的hash值那是一樣的呀,所以dir的值是無法依靠hash值比較得出結果
// 的,那麼重點就靠最後一個條件判斷來得出結果了,
for (TreeNode<K,V> p = root;;) {
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);// 最後需要依靠這個方法來決定dir的值
TreeNode<K,V> xp = p;
// 根據dir的值來決定將當前連結串列節點儲存到當前樹節點的左邊還是右邊,
// 或者當前連結串列節點需要與當前樹節點的左節點還是右節點接著比較
// 主要尋找子節點為null的情況,將節點儲存到null位置
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
// dir<=0,將連結串列節點儲存到當前樹節點的左邊子節點位置
xp.left = x;
else
// dir<=0,將連結串列節點儲存到當前樹節點的右邊子節點位置
xp.right = x;
// 一旦新增的一個新節點,就要進行樹平衡操作,以此保證紅黑樹結構
// 樹的平衡操作依靠的就是其左右旋轉操作
root = balanceInsertion(root, x);
break;
}
}
}
}
// 最後將組裝好的樹的根節點儲存到桶下標位
moveRootToFront(tab, root);
}
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];
// 校驗當前桶下標位的值是否為根節點的值,可能會存在不同的原因是樹的平衡操作將原本的根節點挪移了
// 如果相同,那麼不作任何處理,如果不同,就需要替換桶位元素為樹根節點元素,然後改變雙向連結串列結構
// 將root根節點作為雙向連結串列表頭元素,為何要替換呢,因為在判斷桶位元素型別時會對連結串列進行遍歷,如
// 果桶位置放的不是連結串列頭或者尾元素,遍歷將變得非常麻煩
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;
}
// 校驗連結串列和樹的結構
assert checkInvariants(root);
}
}
//...
}
//...
}
紅黑樹分拆操作
操作描述
很簡單,看原始碼
原始碼解析
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
//...
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
//...
// 將一顆大樹分拆為兩顆小樹,如果樹太小,退化為單向連結串列
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
// this代表當前節點,也就是樹的根節點,桶位節點
// map代表當前集合
// tab代表新桶陣列
// index代表當前節點的桶位下標
// bit為舊桶容量
TreeNode<K,V> b = this;
// Relink into lo and hi lists, preserving order
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
int lc = 0, hc = 0;// lc表示低位樹容量,hc表示高位樹容量
for (TreeNode<K,V> e = b, next; e != null; e = next) {
next = (TreeNode<K,V>)e.next;
e.next = null;
// 分拆樹節點的依據,結果為0的一組(低位組),結果不為0的一組(高位組)
if ((e.hash & bit) == 0) {
// 組裝低位組雙向連結串列
if ((e.prev = loTail) == null)
loHead = e;
else
loTail.next = e;
loTail = e;
++lc;
}
else {
// 組裝高位組雙向連結串列
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
++hc;
}
}
// 針對低位組進行樹形化處理,如果該組元素數量少於6個則退化為單向連結串列
if (loHead != null) {
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
// 針對高位組進行樹形化處理,如果該組元素少於6個則退化為單向連結串列
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}
//...
}
//...
}
紅黑樹新增元素操作
操作描述
參照原始碼
原始碼解析
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
//...
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
//...
// 紅黑樹的新增元素,map為當前HashMap,tab為當前桶陣列,h為新增元素的key的hash值,k為新增元素的key,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;;) {
// dir代表當前樹節點與待新增節點的hash比較結果
// ph代表當前樹節點的hash值
// pk代表當前樹節點的key
// 由於一個桶位的所有元素hash值相等,所以最後得出結果需要依靠
int dir, ph; K pk;
if ((ph = p.hash) > h)
// 如果當前節點的hash值大,dir為-1
dir = -1;
else if (ph < h)
// 如果當前節點的hash值小,dir為1
dir = 1;
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
// hash值相等的情況下,如果key也一樣直接返回當前節點,返回去之後會執行value的替換操作
return p;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
if (((ch = p.left) != null &&
(q = ch.find(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.find(h, k, kc)) != null))
// 這個找到的q也是與待新增元素key相同的元素,執行替換
return q;
}
// 最終需要依靠這個方法來得出dir值的結果
dir = tieBreakOrder(k, pk);
}
TreeNode<K,V> xp = p;
// 根據dir的值來決定是當前節點的左側還是右側,如果該側右子節點則繼續迴圈尋找位置,否則直接將新元素新增到該側子節點位置
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)
// dir<=0,將新節點新增到當前節點左側
xp.left = x;
else
// 否則將新節點新增到當前節點右側
xp.right = x;
// 設定新節點的連結串列位置,將其作為xp的下級節點
xp.next = x;
x.parent = x.prev = xp;
if (xpn != null)
// 如果xp節點原本有下級節點xpn,則要將新節點插入到xp和xpn之間(指雙向連結串列中)
((TreeNode<K,V>)xpn).prev = x;
// 插入了新節點之後,要進行樹平衡操作,平衡操作完成,將根節點設定為桶位節點
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
// 一般我們在HashMap中儲存的鍵值對的型別都是不變的,這一般用泛型控制,
// 那麼就意味著,兩個元素的key的型別時一樣的,所以才需要靠其hashCode來決定大小
// System.identityHashCode(parameter)是本地方法,用於獲取和hashCode一樣的結果,
// 這裡的hashCode指的是預設的hashCode方法,與某些類重寫的無關
static int tieBreakOrder(Object a, Object b) {
int d;
if (a == null || b == null ||
(d = a.getClass().getName().
compareTo(b.getClass().getName())) == 0)
d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
-1 : 1);
return d;
}
//...
}
//...
}
紅黑樹新增元素平衡操作
操作描述
左旋操作描述
繞A節點左旋,等於將A的右子節點B甩上來替換自己的位置,而自己順勢下沉成為其左子節點,這時你會發現,B有三個子節點,明顯結構不對,將B的原來的左子節點C轉移到下沉的A上,成為其右子節點,旋轉結束
其實,要保證左子樹節點值小於其根節點,右子樹節點值大於其根節點,那麼在替換AB節點之後,C節點的值就出現了問題,只有將其挪到A節點右邊才能繼續保證上面的結構。
首先我們知道B節點為A的右節點,那麼B>A,而C為B的左節點,則C<B,而C又位於A的右子樹,則C>A,因此:A<C<B。要保證這個式子永遠成立,就必須依靠挪移節點來完成。
現在B為最頂部節點且為最大值,那麼A和C必須位於其左子樹,而C>A則,C必須位於A的右子樹,再看看之前的情況,如果A為頂點節點,那麼BC均應位於其右子樹,而B>C,那麼要麼B為C的右節點,要麼C為B的左節點
右旋操作描述
繞A幾點右旋,等於將A的左子節點B甩上來替換自己的位置,而自己順勢下沉成為其右子節點,這是你會發現,B有三個子節點,明顯結構不對,將B的原來的右子節點C轉移到下沉的A上,成為其左子節點,旋轉結束
首先我們知道B為A的左子節點,所以B<A,再者C為B的右子節點,那麼C>B,而C又位於A的左子樹,則C<A,最後:A>C>B。要保證這個結果成立,那麼再B替換A的位置之後,A下沉為B的右子節點,因為A>B,所以往右走,
這時C和A均位於B的右側,比較二者發現C<A,那麼將C放到A的左側成為其左子節點
新增平衡操作描述
新增節點全部初始化為紅色節點,然後分以下幾種情況:
- 新增節點為根節點:顏色置黑;
- 新增節點父節點為黑色節點或者父節點是根節點(原本為黑色):不操作;
- 新增節點x的父節點為其父節點(x祖節點)的左子節點:
- x祖父節點的右子節點存在併為紅色(那麼x祖父節點一定是黑色節點):將x的祖父節點置為紅色,x的父節點和其兄弟節點置為黑色,然後以x的祖父節點為新的x執行迴圈;
- x祖父節點無右子節點或為黑色節點:
- 如果x是其父節點的右子節點:執行以x父節點xp為基準的左旋操作,x被甩上來替換xp的位置,並置黑,原x祖父節點(現x節點父節點)置紅,然後以該祖父節點右旋,之後x節點再次被甩上來替換了祖父節點xpp的位置,然後以xp為新的x執行迴圈
- 新增節點x的父節點為其父節點的右子節點:
- x祖父節點的左子節點存在併為紅色(那麼x祖父節點一定為黑色節點):將x的祖父節點置為紅色,x的父節點和其兄弟節點置為黑色,然後以x的祖父節點為新的x執行迴圈;
- x祖父節點無左子節點或為黑色節點:
- 如果x是其父節點的左子節點:執行以x父節點xp為基準的右旋操作,x被甩上來替換xp的位置,並置黑,原x祖父節點(現x節點父節點)置紅,然後以該祖父節點右旋,之後x節點再次被甩上來替換了祖父節點xpp的位置,然後以xp為新的x執行迴圈。
原始碼解析
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
//...
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
//...
// 左旋操作,其中root為根節點,p為當前節點,r為p的右節點,rl為r的左節點,pp為p的父節點
// 左旋之後,r替換p的位置,rl挪到p的右節點
// 節點位置變換之後,既要改變其父節點的left/right值,也要改變當前節點中parent的值,
// 改變是雙向的,父子均有指向,改變之後均要修改
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) {
// 首先將r節點的左子節點(rl)送給p當其右子節點
if ((rl = p.right = r.left) != null)
rl.parent = p;//變換rl的父節點為p,原來為r
if ((pp = r.parent = p.parent) == null)
// 原p節點為根節點的情況,r替換之後,需要重新著色為黑色,保證根節點為黑色
(root = r).red = false;
else if (pp.left == p)
// 原p節點為其父節點pp的左子節點的情況,r替換後,需要修改pp節點的left指向r節點
pp.left = r;
else
// 原p節點為其父節點pp的右子節點的情況,r替換後,需要修改pp節點的right指向r節點
pp.right = r;
//然後將p節點作為r節點的左子節點,即為p節點順勢下沉為r的左子節點
r.left = p;
p.parent = r;//變換p的父節點為r
}
return root;
}
// 右旋操作,嘿,那就是左旋的反向操作罷了
// root為根節點,p為當前節點,l為其左子節點,lr為l的右子節點,pp為p的父節點
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) {
// 首先將l的右子節點lr挪給p
if ((lr = p.left = l.right) != null)
lr.parent = p;//變換lr的父節點為p,原來為l
if ((pp = l.parent = p.parent) == null)
// 如果p節點是根節點,替換為l之後,l便成為新的根節點,需要重新著色為黑色,保證紅黑樹結構
(root = l).red = false;
else if (pp.right == p)
// 如果原p節點是其父節點pp的右子節點,那麼需要將其右子節點改成l
pp.right = l;
else
// 如果原p節點是其父節點pp的左子節點,那麼需要將其左子節點改成l
pp.left = l;
// 最後將原p節點置為l節點的右子節點,並修改p的父節點為l
l.right = p;
p.parent = l;
}
return root;
}
// 平衡操作,x為新增節點,root為根節點
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) {
// 1 x為根節點的情況,將其重新著色為黑色
x.red = false;
return x;
}
else if (!xp.red || (xpp = xp.parent) == null)
// 2 如果x節點的父節點為黑色,又或者x的父節點是根節點,沒有影響,不操作
return root;
if (xp == (xppl = xpp.left)) {
// 3 如果x節點的父節點是其父節點(x的祖父節點)的左子節點
if ((xppr = xpp.right) != null && xppr.red) {
// 3-1 再如果x的祖父節點的右子節點存在且為紅色,則將這個節點和x的父節點統統改成黑色,
// 再把x的祖父節點改成紅色,將x祖父節點作為新的x節點執行迴圈
xppr.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
// 3-2 否則的情況
if (x == xp.right) {
// 3-2-1 如果x節點是其父節點的右子節點,則執行以x父節點為基準的左旋操作,
// 左旋之後新增節點x替了其原父節點xp,將原xp節點當做現在的x節點,原來的x
// 節點是現在x節點的父節點xp,原來的x節點的祖父節還是現在x的祖父節點
root = rotateLeft(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) {// xp為原來的x節點
// 將xp節點置為黑色
xp.red = false;
if (xpp != null) {// xpp還是之前的xpp
// 將xpp節點置為紅色,然後執行右旋,右旋可以將xpp節點用xp節點替換,紅黑交換
xpp.red = true;
root = rotateRight(root, xpp);
}
}
}
}
else {
// 4 如果x節點的父節點是其父節點(x的祖父節點)的右子節點
if (xppl != null && xppl.red) {
// 4-1 再如果x的祖父節點的左子節點存在並且為紅色,則將該節點置為黑色,
// 將x的父節點置為黑色,祖父節點置為紅色,然後把xpp祖父節點作為新的x節點
xppl.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
// 4-2 否則的情況
if (x == xp.left) {
// 4-2-1 如果x節點是其父節點的左子節點的情況,先以x父節點進行右旋,
// 右旋之後原來的xp節點被新的x節點替換,原來的xp節點作為新xp節點的右子節點,
// 現在看作為x,然後重新定義xpp,其實xpp位置不變
root = rotateRight(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) {
// 將現在的xp節點置為黑色
xp.red = false;
if (xpp != null) {
// 將祖父節點置為紅色。然後執行左旋,左旋之後,原來的xp節點接替了xpp節點的位置,xpp變成原來xp的左子節點
xpp.red = true;
root = rotateLeft(root, xpp);
}
}
}
}
}
}
//...
}
//...
}
紅黑樹刪除元素操作
操作描述
紅黑樹的節點刪除操作主要分為這麼三種:
- 待刪節點沒有子節點
- 待刪節點有一個子節點
- 待刪節點有兩個子節點
針對第一種情況,真的好簡單,待刪節點即為葉子節點,直接刪除即可;
針對第二種情況,也不難,將那個子節點替換待刪節點即可;
至於第三種情況,那就麻煩了,但通過變換,可以將其轉化為第一種或者第二種情況:處理方式是,找到待刪節點的右子樹中的最左節點(或者左子樹中的最右節點),將其與待刪節點互換位置,然後就將情況轉換為第一種或者第二種了。
針對第三種情況轉換方法的解析:為什麼要找到待刪節點的右子樹最左節點呢,因為紅黑樹是二叉搜尋樹,這個二叉搜尋樹中滿足"左子節點<其父節點<其父節點的右子節點"的規則,那麼找到的右子樹的最左節點,就是整顆樹中大於待刪節點值的最小值節點了,為了保證二叉搜尋樹的搜尋結構(也就是剛剛那個公式),我們只能找最接近待刪節點值的節點值來接替它的位置,如此能保證二叉搜尋的結構,但是可能會破壞紅黑樹的結構,因為如果待刪節點為紅色,而替換節點為黑色的話,那豈不是在待刪節點分支多加了一個黑色節點嘛,還有其他各種情況,種種,需要進行刪除節點後的樹平衡操作來保證紅黑樹的結構完整。
下面重點說說刪除後的平衡問題:
其實只要待刪節點是黑色節點,一旦刪除必然會導致分支中黑色節點缺一(紅黑樹不再平衡),具體情況又分為以下幾種:(基礎條件:待刪節點p為黑色,其只有一個子節點x,操作在待刪節點被刪除之後,子節點替換其位置之後)
- 如果子節點x為紅色節點,那麼只需要將其置黑即可;
- 如果子節點x為黑色節點,為保證本分支黑色節點不會再變少,那麼只能求助於其兄弟節點分支了:
- x為左子節點:
- x無兄弟節點xpr:以x的父節點xp為基準進行迴圈;
- x有兄弟節點xpr:
- xpr為紅色節點(那麼xp必然為黑色節點):將xp置紅,xpr置黑,以xp為基準左旋;
解析:開始情況是x分支刪除了一個黑色節點,即x分支缺少一個黑色幾點,而x的兄弟節點xpr為紅色節點,xp為黑色節點,我們將xp和xpr顏色互換,那麼在xpr分支黑色節點數量是不變的(只是位置變了),然後我麼以紅色的xp為基準執行左旋,將黑色的xpr甩上去替換xp的位置,xp作為xpr的左子節點,那麼x端分支便多出了xpr這個黑色節點來補足不夠的數量,而兄弟分支黑色節點數量還是不變的。 - xpr為黑色節點(那麼xp顏色不明):
- 兄弟節點的左子節點和右子節點全null或全黑或一黑一null:將兄弟節點置紅,然後以xp為基準進行迴圈;
- 兄弟節點的左子節點和右子節點全紅或一紅一null或一紅一黑:
- 兄弟節點的右子節點為黑色或null,即兄弟節點的左子節點為紅色:將兄弟節點與其做自己節點交換顏色,兄弟節點置紅,左子節點置黑,然後以兄弟節點為基準執行右旋操作,將其黑色的左子節點甩上去做自己的父節點,自己做其右子節點,然後將新的兄弟節點xpr(原來的xprl)的顏色置為與xp一致(不明,非黑即白),新的sr(即原來的xpr)置黑(這個置黑的原因是因為右旋操作之前執行了顏色替換,兄弟節點右側分支少了一個黑色節點,右旋之後變為黑色的sl補充了這個黑色節點,但是現在我們要用sl[新xpr]來替換xp節點[置為xp節點的顏色],那麼右側分支原本用來補充之前缺少的黑色節點又消失了,所以將已知的紅色節點sr置為黑色來進行補充),xp置黑,以xp左旋,xpr被甩上來替換xp的位置,xp則是補充給x分支的黑色節點,xpr與以前的xp顏色一致,所以兄弟分支黑色節點不變。
解析:新的sr(即原來的xpr)的原因是因為右旋操作之前執行了顏色替換,兄弟節點右側分支少了一個黑色節點,右旋之後變為黑色的sl補充了這個黑色節點,但是現在我們要用sl[新xpr]來替換xp節點[置為xp節點的顏色],那麼右側分支原本用來補充之前缺少的黑色節點又消失了,所以將已知的紅色節點sr置為黑色來進行補充)
- 兄弟節點的右子節點為黑色或null,即兄弟節點的左子節點為紅色:將兄弟節點與其做自己節點交換顏色,兄弟節點置紅,左子節點置黑,然後以兄弟節點為基準執行右旋操作,將其黑色的左子節點甩上去做自己的父節點,自己做其右子節點,然後將新的兄弟節點xpr(原來的xprl)的顏色置為與xp一致(不明,非黑即白),新的sr(即原來的xpr)置黑(這個置黑的原因是因為右旋操作之前執行了顏色替換,兄弟節點右側分支少了一個黑色節點,右旋之後變為黑色的sl補充了這個黑色節點,但是現在我們要用sl[新xpr]來替換xp節點[置為xp節點的顏色],那麼右側分支原本用來補充之前缺少的黑色節點又消失了,所以將已知的紅色節點sr置為黑色來進行補充),xp置黑,以xp左旋,xpr被甩上來替換xp的位置,xp則是補充給x分支的黑色節點,xpr與以前的xp顏色一致,所以兄弟分支黑色節點不變。
- xpr為紅色節點(那麼xp必然為黑色節點):將xp置紅,xpr置黑,以xp為基準左旋;
- x為右子節點:與上面的情況正好對稱(不再介紹)
- x為左子節點:
貌似有點難...大家要看進去思考才能理解,光看沒用!
原始碼解析
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
//...
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
//...
// 當前節點即為要刪除的節點,map為當前集合,tab為當前桶陣列
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
boolean movable) {
int n;
if (tab == null || (n = tab.length) == 0)
return;
// 定位待刪節點的桶位下標index
int index = (n - 1) & hash;
TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
if (pred == null)
// 如果當前節點是雙向連結串列頭節點/樹根節點,則將連結串列第二元素置為桶位元素,即刪除該節點
tab[index] = first = succ;
else
// 如果當前節點不是雙向連結串列頭節點,則將其後置節點賦給其前置節點作為後置節點,即刪除該節點
pred.next = succ;
if (succ != null)
// 修改後置節點中prev指向新的前置元素節點
succ.prev = pred;
if (first == null)
return;
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為p的左子節點,pr為p的右子節點,
TreeNode<K,V> p = this, pl = left, pr = right, replacement;
if (pl != null && pr != null) {
// 當前節點同時擁有左右子節點的情況,sl表示當前節點的右子樹的最左節點
// 要刪除當前節點,需要找到與當前節點值最靠近的左右兩側的節點之一,這
// 裡找的是右側的,即找的是整個樹中大於當前節點值的最小值節點,將找到
// 的節點與待刪節點互換,互換之後再刪除節點,如果原來的那個最左節點還
// 有右子節點,則將該右子節點替換其父節點(待刪節點)
TreeNode<K,V> s = pr, sl;
while ((sl = s.left) != null) // find successor
s = sl;// 找到右子樹的最左節點
boolean c = s.red; s.red = p.red; p.red = c; // swap colors 首先互換顏色
TreeNode<K,V> sr = s.right;// s為最左節點,那麼它不可能有左子節點,最多有右子節點
TreeNode<K,V> pp = p.parent;
if (s == pr) { // p was s's direct parent
// 如果找到的s即為待刪節點的直接右子節點(說明s無左子節點),那麼直接替換這兩個節點
p.parent = s;
s.right = p;
}
else {
// 否則的情況,先找到s的父節點sp,將其設定為p的父節點,
TreeNode<K,V> sp = s.parent;
if ((p.parent = sp) != null) {
if (s == sp.left)
// 將p作為原來s的父節點的左子節點(即替換p和s的位置)
sp.left = p;
else
// TODO 這裡是什麼意思呢?找的就是sp的最左節點,這裡怎麼跑到右節點上去了呢,雖然p是要刪除的節點
sp.right = p;
}
// 把p的右子節點pr置為s的右子節點
if ((s.right = pr) != null)
// 把s置為pr的父節點
pr.parent = s;
}
// 替換之後p是無左子節點的,(即原來的s是最左節點,無左子節點)
p.left = null;
// 把s的右子節點sr置為p的右子節點
if ((p.right = sr) != null)
// 把sr的父節點設定為p
sr.parent = p;
if ((s.left = pl) != null)
// 將p的左子節點置為s的左子節點
pl.parent = s;
// 把p的父節點設定為s的父節點
if ((s.parent = pp) == null)
// 如果p沒有父節點,將s節點設定為根節點
root = s;
// 否則如果p是其父節點pp的左子節點
else if (p == pp.left)
// 現在將s設定為pp的左子節點
pp.left = s;
else
// 否則如果p是其父節點的右子節點,則將s設定為pp的右子節點
pp.right = s;
if (sr != null)
// 如果s存在右子節點,則將其置為replacement,現在和待刪節點只有右子節點的情況一樣
replacement = sr;
else
// 否則將p置為replacement,至此第一種情況替換結束,現在和待刪節點沒子節點的情況一樣
replacement = p;
}
else if (pl != null)
// 待刪節點只有左子節點的情況,將其左子節點置為replacement
replacement = pl;
else if (pr != null)
// 當前節點只有右子節點的情況,將其右子節點置為replacement
replacement = pr;
else
// 待刪節點沒有子節點的情況,直接將其設定為replacement
replacement = p;
if (replacement != p) {// 如果待刪節點有子節點replacement的情況
// 準備替換replacement節點和p節點
TreeNode<K,V> pp = replacement.parent = p.parent;
if (pp == null)
// 待刪節點p為根節點的情況,將replacement設定為根節點即可
root = replacement;
else if (p == pp.left)
// p是作為其父節點pp的左子節點,則將replacement設定為pp的左子節點
pp.left = replacement;
else
// 否則p是作為其父節點pp的右子節點,則將replacement設定為pp的右子節點
pp.right = replacement;
// 最後將p節點的所有關係置空
p.left = p.right = p.parent = null;
}
// 如果待刪節點是紅色節點則不影響平衡,無需執行樹平衡操作,否則需要進行樹平衡操作
TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);
// 如果p節點沒有任何子節點的情況
if (replacement == p) { // detach
// 根據實際情況置空p節點的各屬性
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);
}
// 刪除節點後的平衡操作,root為根節點,x為上面提到的replacement節點,該節點其實為替換p節點,為其子節點
static <K,V> TreeNode<K,V> balanceDeletion(TreeNode<K,V> root,
TreeNode<K,V> x) {
for (TreeNode<K,V> xp, xpl, xpr;;) {
if (x == null || x == root)
// 如果x不存在或者其本來就是根節點,位元組返回根節點
return root;
else if ((xp = x.parent) == null) {
// 如果x節點不存在父節點,那麼則其為根節點,但是還未設定為root節點,那麼直接置黑色,並將其設定為root節點
x.red = false;
return x;
}
else if (x.red) {
// 如果x是紅色節點,則將其置黑,因為如果x為紅色,那麼其父節點p必然為黑色,
// 刪掉之後,會導致黑色節點減少,正好x為紅色拿來補充黑色節點,使黑色節點數不變,
// 如果x是黑色,那麼就會導致當前分支黑色節點減少,需要使用其他方法進行平衡
x.red = false;
return root;
}
// 如果x為其父節點左子節點(刪除後的結果)
else if ((xpl = xp.left) == x) {
// 如果x存在兄弟節點(其父節點的右子節點),且為紅色,那麼xp必定為黑色
// 那麼就表明x節點分支的黑色節點少了一個,也就是其兄弟節點多一個(其他所有分支都多一個),
if ((xpr = xp.right) != null && xpr.red) {
// 那麼將兄弟節點置黑,父節點置紅,這是x分支還是少一個黑節點,兄弟分支黑節點不變
xpr.red = false;
xp.red = true;
// 再然後執行xp節點的左旋,將其右子節點(即x的兄弟節點)甩上去,
// xp作為其左子節點,如此一來將兄弟節點這一黑色幾點變成兩份之共享,
// 無形之中使得x分支黑色節點加1,從而達到平衡
root = rotateLeft(root, xp);
xpr = (xp = x.parent) == nu