1 平衡因子:
左右子樹 高度之差
LL型 右旋
LR型 -->LL 右旋
RR -->左旋
RL -->RR 左旋
左旋:逆時針旋轉紅黑樹的兩個節點,使得父節點被自己的右孩子取代,而自己成爲自己的左孩子。
右旋:順時針旋轉紅黑樹的兩個節點,使得父節點被自己的左孩子取代,而自己成爲自己的右孩子
左旋和右旋的代碼
/**
* 左旋
*/
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
TreeNode<K,V> p) {
//這裏的p即上圖的A節點,r指向右孩子即C,rl指向右孩子的左孩子即D,pp爲p的父節點
TreeNode<K,V> r, pp, rl;
if (p != null && (r = p.right) != null) {
if ((rl = p.right = r.left) != null)
rl.parent = p;
//將p的父節點的孩子節點指向r
if ((pp = r.parent = p.parent) == null)
(root = r).red = false;
else if (pp.left == p)
pp.left = r;
else
pp.right = r;
//將p置爲r的左節點
r.left = p;
p.parent = r;
}
return root;
}
/**
* 右旋
*/
static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,
TreeNode<K,V> p) {
//這裏的p即上圖的A節點,l指向左孩子即C,lr指向左孩子的右孩子即E,pp爲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;
}
2 紅黑樹的性質:
1)節點是黑色或者紅色
2)根節點是黑色的
3)每個葉結點是黑色的
4)每個紅色節點的兩個子節點都是黑色的
5)從任一節點到其他每個葉子節點的所有路徑都包含相同數目的黑色節點
只要考慮到每個節點都遵循左小右大就很容易判斷出是左旋還是右旋
3 紅黑樹調整
a 插入的節點爲紅色
因爲插入的節點如果爲黑色,就可能違背紅黑樹特性的最後一條,所以每次插入的都是紅色節點
b 具體情況
根據被插入節點的父節點的情況,可以將"當節點z被着色爲紅色節點,並插入二叉樹"劃分爲三種情況來處理。
① 情況說明:被插入的節點是根節點。
處理方法:直接把此節點塗爲黑色。
② 情況說明:被插入的節點的父節點是黑色。
處理方法:什麼也不需要做。節點被插入後,仍然是紅黑樹。
③ 情況說明:被插入的節點的父節點是紅色。
處理方法:那麼,該情況與紅黑樹的“特性(5)”相沖突。這種情況下,被插入節點是一定存在非空祖父節點的;進一步的講,被插入節點也一定存在叔叔節點(即使叔叔節點爲空,我們也視之爲存在,空節點本身就是黑色節點)。理解這點之後,我們依據"叔叔節點的情況",將這種情況進一步劃分爲3種情況(Case)。
現象說明 | 處理策略 | |
---|---|---|
Case 1 | 當前節點的父節點是紅色,且當前節點的祖父節點的另一個子節點(叔叔節點)也是紅色 | (01) 將“父節點”設爲黑色。(02) 將“叔叔節點”設爲黑色。(03) 將“祖父節點”設爲“紅色”。(04) 將“祖父節點”設爲“當前節點”(紅色節點);即,之後繼續對“當前節點”進行操作。 |
Case 2 | 當前節點的父節點是紅色,叔叔節點是黑色,且當前節點是其父節點的右孩子 | (01) 將“父節點”作爲“新的當前節點”。(02) 以“新的當前節點”爲支點進行左旋。 |
Case 3 | 當前節點的父節點是紅色,叔叔節點是黑色,且當前節點是其父節點的左孩子 | (01) 將“父節點”設爲“黑色”。(02) 將“祖父節點”設爲“紅色”。(03) 以“祖父節點”爲支點進行右旋 |
c 刪除
第一步:將紅黑樹當作一顆二叉查找樹,將節點刪除。
這和"刪除常規二叉查找樹中刪除節點的方法是一樣的"。分3種情況:
① 被刪除節點沒有兒子,即爲葉節點。那麼,直接將該節點刪除就OK了。
② 被刪除節點只有一個兒子。那麼,直接刪除該節點,並用該節點的唯一子節點頂替它的位置。
③ 被刪除節點有兩個兒子。那麼,先找出它的後繼節點;然後把“它的後繼節點的內容”複製給“該節點的內容”;之後,刪除“它的後繼節點”。在這裏,後繼節點相當於替身,在將後繼節點的內容複製給"被刪除節點"之後,再將後繼節點刪除。這樣就巧妙的將問題轉換爲"刪除後繼節點"的情況了,下面就考慮後繼節點。 在"被刪除節點"有兩個非空子節點的情況下,它的後繼節點不可能是雙子非空。既然"的後繼節點"不可能雙子都非空,就意味着"該節點的後繼節點"要麼沒有兒子,要麼只有一個兒子。若沒有兒子,則按"情況① "進行處理;若只有一個兒子,則按"情況② "進行處理。
後繼節點就是刪除節點比它大的最小子節點
第二步:通過"旋轉和重新着色"等一系列來修正該樹,使之重新成爲一棵紅黑樹。
因爲"第一步"中刪除節點之後,可能會違背紅黑樹的特性。所以需要通過"旋轉和重新着色"來修正該樹,使之重新成爲一棵紅黑樹。
源碼詳解
1)TreeNode
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links 父節點
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion 和next都是用來構建雙向鏈表
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
treeifyBin將普通鏈表轉成爲由 TreeNode 型節點組成的鏈表,並在最後調用 treeify 是將該鏈表轉爲紅黑樹
樹化要滿足兩個條件:
1.鏈表長度大於等於 TREEIFY_THRESHOLD 8
2.桶數組容量大於等於 MIN_TREEIFY_CAPACITY 64
當桶數組比較小時,鍵值對節點hash的碰撞率會比較高,進而導致鏈表長度較長。這個時候應該擴容,而不是樹化,而不是立馬樹化。畢竟高碰撞率是因爲桶數組容量較小引起的,這個是主因。同時,擴容時需要拆分紅黑樹並重新映射,耗費時間。
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)
resize();
//符合樹化條件
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;// hd 爲頭節點(head),tl 爲尾節點(tail)
do {
//把普通節點轉化爲樹節點
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);
}
}
將鏈表轉化爲紅黑樹
final void treeify(Node<K, V>[] tab)
{ //樹的根節點
TreeNode<K, V> root = null;
// 以for循環的方式遍歷剛纔我們創建的鏈表。x是當前節點,next是後繼節點
for (TreeNode<K, V> x = this, next; x != null; x = next)
{
// 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;
// 此時紅黑樹已經有了根節點,上面獲取了當前加入紅黑樹的項的key和hash值進入核心循環。
// 這裏從root開始,是以一個自頂向下的方式遍歷添加。
// for循環沒有控制條件,由代碼內break跳出循環。
for (TreeNode<K, V> p = root;;)
{
//p指向遍歷中的當前節點,x爲待插入節點,k是x的key,h是x的hash值,ph是p的hash值,dir用來指示x節點與p的比較,-1表示比p小,1表示比p大,不存在相等情況,因爲HashMap中是不存在兩個key完全一致的情況。
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
//如果hash值相等,那麼判斷k是否實現了comparable接口,如果實現了comparable接口就使用compareTo進行進行比較,如果仍舊相等或者沒有實現comparable接口,則在tieBreakOrder中比較
else if ((kc == null && (kc = comparableClassFor(k)) == null)
|| (dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
// xp:x parent。
TreeNode<K, V> xp = p;
// 找到符合x添加條件的節點。
if ((p = (dir <= 0) ? p.left : p.right) == null)
{
x.parent = xp;
// 如果xp的hash值大於x的hash值,將x添加在xp的左邊。
if (dir <= 0)
xp.left = x;
// 反之添加在xp的右邊。
else
xp.right = x;
// 維護添加後紅黑樹的紅黑結構。
root = balanceInsertion(root, x);
// 跳出循環當前鏈表中的項成功的添加到了紅黑樹中。
break;
}
}
}
}
// Ensures that the given root is the first node of its bin,自己翻譯一下。
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;
//first指向鏈表第一個節點
TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
if (root != first) {
//如果root不是第一個節點,則將root放到第一個首節點位置
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;
}
//這裏是防禦性編程,校驗更改後的結構是否滿足紅黑樹和雙鏈表的特性
//因爲HashMap並沒有做併發安全處理,可能在併發場景中意外破壞了結構
assert checkInvariants(root);
}
}
調整樹的結構
static <K, V> TreeNode<K, V> balanceInsertion(TreeNode<K, V> root, TreeNode<K, V> x)
{
// 正如開頭所說,新加入樹節點默認都是紅色的,不會破壞樹的結構。
x.red = true;
// xp:x parent,代表x的父節點。
// xpp:x parent parent,代表x的祖父節點
// xppl:x parent parent left,代表x的祖父的左節點。
// xppr:x parent parent right,代表x的祖父的右節點。
for (TreeNode<K, V> xp, xpp, xppl, xppr;;)
{
// 如果x的父節點爲null說明只有一個節點,該節點爲根節點,根節點爲黑色,red = false。
if ((xp = x.parent) == null)
{
x.red = false;
return x;
}
// 進入else說明不是根節點。
// 如果父節點是黑色,紅色的x節點可以直接添加到黑色節點後面,返回根就行了不需要任何多餘的操作。
// 如果父節點是紅色的,但祖父節點爲空的話也可以直接返回根此時父節點就是根節點,因爲根必須是黑色 的,添加在後面沒有任何問題。
else if (!xp.red || (xpp = xp.parent) == null)
return root;
// 一旦我們進入到這裏就說明了兩件是情
// 1.x的父節點xp是紅色的,這樣就遇到兩個紅色節點相連的問題,所以必須經過旋轉變換。
// 2.x的祖父節點xpp不爲空。
// 判斷如果父節點是否是祖父節點的左節點
if (xp == (xppl = xpp.left))
{
// 父節點xp是祖父的左節點xppr
// 判斷祖父節點的右節點不爲空並且是否是紅色的
// 此時xpp的左右節點都是紅的,所以直接進行上面所說的第三種變換,將兩個子節點變成黑色,將xpp變成紅色,然後將紅色節點x順利的添加到了xp的後面。
// 這裏大家有疑問爲什麼將x = xpp?
// 這是由於將xpp變成紅色以後可能與xpp的父節點發生兩個相連紅色節點的衝突,這就又構成了第二種旋轉變換,所以必須從底向上的進行變換,直到根。
// 所以令x = xpp,然後進行下下一層循環,接着往上走。
if ((xppr = xpp.right) != null && xppr.red)
{
xppr.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
// 進入到這個else裏面說明。
// 父節點xp是祖父的左節點xppr。
// 祖父節點xpp的右節點xppr是黑色節點或者爲空,默認規定空節點也是黑色的。
// 下面要判斷x是xp的左節點還是右節點。
else
{
// x是xp的右節點,此時的結構是:xpp左->xp右->x。這明顯是第二中變換需要進行兩次旋轉,這裏先進行一次旋轉。
// 下面是第一次旋轉。
if (x == xp.right)
{
root = rotateLeft(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
// 針對本身就是xpp左->xp左->x的結構或者由於上面的旋轉造成的這種結構進行一次旋轉。
if (xp != null)
{
xp.red = false;
if (xpp != null)
{
xpp.red = true;
root = rotateRight(root, xpp);
}
}
}
}
// 這裏的分析方式和前面的相對稱只不過全部在右測不再重複分析。
else
{
if (xppl != null && xppl.red)
{
xppl.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
} else
{
if (x == xp.left)
{
root = rotateRight(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null)
{
xp.red = false;
if (xpp != null)
{
xpp.red = true;
root = rotateLeft(root, xpp);
}
}
}
}
}
}
將紅黑樹轉化爲鏈表
HashMap 通過兩個額外的引用 next 和 prev 保留了原鏈表的節點順序。這樣再對紅黑樹進行重新映射時,完全可以按照映射鏈表的方式進行。這樣就避免了將紅黑樹轉成鏈表後再進行映射,無形中提高了效率。
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int 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;
/*
* 紅黑樹節點仍然保留了 next 引用,故仍可以按鏈表方式遍歷紅黑樹。
* 下面的循環是對紅黑樹節點進行分組,與上面類似
*/
for (TreeNode<K,V> e = b, next; e != null; e = next) {
next = (TreeNode<K,V>)e.next;
e.next = null;
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;
}
}
if (loHead != null) {
// 如果 loHead 不爲空,且鏈表長度小於等於 6,則將紅黑樹轉成鏈表
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
/*
* hiHead == null 時,表明擴容後,
* 所有節點仍在原位置,樹結構不變,無需重新樹化
*/
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}
刪除節點
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab, boolean movable) {
......
//p是待刪除節點,replacement用於後續的紅黑樹調整,指向的是p或者p的繼承者。
//如果p是葉子節點,p==replacement,否則replacement爲p的右子樹中最左節點
if (replacement != p) {
//若p不是葉子節點,則讓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;
}
//若待刪除的節點p時紅色的,則樹平衡未被破壞,無需進行調整。
//否則刪除節點後需要進行調整
TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);
//p爲葉子節點,則直接將p從樹中清除
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;
}
}
}
刪除後的調整
static <K,V> TreeNode<K,V> balanceDeletion(TreeNode<K,V> root, TreeNode<K,V> x) {
for (TreeNode<K,V> xp, xpl, xpr;;) {
//x爲空或x爲根節點,直接返回
if (x == null || x == root)
return root;
//x爲根節點,染成黑色,直接返回(因爲調整過後,root並不一定指向刪除操作過後的根節點,如果之前刪除的是root節點,則x將成爲新的根節點)
else if ((xp = x.parent) == null) {
x.red = false;
return x;
}
//如果x爲紅色,則無需調整,返回
else if (x.red) {
x.red = false;
return root;
}
//x爲其父節點的左孩子
else if ((xpl = xp.left) == x) {
//如果它有紅色的兄弟節點xpr,那麼它的父親節點xp一定是黑色節點
if ((xpr = xp.right) != null && xpr.red) {
xpr.red = false;
xp.red = true;
//對父節點xp做左旋轉
root = rotateLeft(root, xp);
//重新將xp指向x的父節點,xpr指向xp新的右孩子
xpr = (xp = x.parent) == null ? null : xp.right;
}
//如果xpr爲空,則向上繼續調整,將x的父節點xp作爲新的x繼續循環
if (xpr == null)
x = xp;
else {
//sl和sr分別爲其近侄子和遠侄子
TreeNode<K,V> sl = xpr.left, sr = xpr.right;
if ((sr == null || !sr.red) &&
(sl == null || !sl.red)) {
xpr.red = true; //若sl和sr都爲黑色或者不存在,即xpr沒有紅色孩子,則將xpr染紅
x = xp; //本輪結束,繼續向上循環
}
else {
//否則的話,就需要進一步調整
if (sr == null || !sr.red) {
if (sl != null) //若左孩子爲紅,右孩子不存在或爲黑
sl.red = false; //左孩子染黑
xpr.red = true; //將xpr染紅
root = rotateRight(root, xpr); //右旋
xpr = (xp = x.parent) == null ?
null : xp.right; //右旋後,xpr指向xp的新右孩子,即上一步中的sl
}
if (xpr != null) {
xpr.red = (xp == null) ? false : xp.red; //xpr染成跟父節點一致的顏色,爲後面父節點xp的左旋做準備
if ((sr = xpr.right) != null)
sr.red = false; //xpr新的右孩子染黑,防止出現兩個紅色相連
}
if (xp != null) {
xp.red = false; //將xp染黑,並對其左旋,這樣就能保證被刪除的X所在的路徑又多了一個黑色節點,從而達到恢復平衡的目的
root = rotateLeft(root, xp);
}
//到此調整已經完畢,進入下一次循環後將直接退出
x = root;
}
}
}
//x爲其父節點的右孩子,跟上面類似
else { // symmetric
if (xpl != null && xpl.red) {
xpl.red = false;
xp.red = true;
root = rotateRight(root, xp);
xpl = (xp = x.parent) == null ? null : xp.left;
}
if (xpl == null)
x = xp;
else {
TreeNode<K,V> sl = xpl.left, sr = xpl.right;
if ((sl == null || !sl.red) &&
(sr == null || !sr.red)) {
xpl.red = true;
x = xp;
}
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;
}
}
}
}
}
參考博客:https://www.cnblogs.com/xuxinstyle/p/9556998.html
https://www.cnblogs.com/qingergege/p/7351659.html
https://www.cnblogs.com/mfrank/p/9227097.html