本文詳細介紹了紅黑樹的概念和實現原理,並且提供了Java代碼的完全實現。
本文內容較多,歡迎收藏。
1 紅黑樹的概述
- 二叉排序樹的詳解以及Java代碼的完全實現,其中有關於插入和刪除的詳細原理解釋,這是必須掌握的前置知識。
- 平衡二叉樹(AVL樹)的詳解以及Java代碼的完全實現,其中有關於節點旋轉的詳細原理解釋,這是必須掌握的前置知識。
- 多路查找樹中的2-3樹、2-3-4樹、B樹、B+樹的詳解,很多人說可以利用2-3樹來理解紅黑樹,或許可以,但實際上2-3樹不是必須掌握的前置知識。
1.1 AVL樹與紅黑樹
AVL樹是一種自平衡的二叉查找樹,又稱平衡二叉樹。AVL用平衡因子判斷是否平衡並通過旋轉來實現平衡,它的平衡的要求是:所有節點的左右子樹高度差不超過1。AVL樹是一種高平衡度的二叉樹,執行插入或者刪除操作之後,只要不滿足上面的平衡條件,就要通過旋轉來保持平衡,而的由於旋轉比較耗時,由此我們可以知道AVL樹適合用於插入與刪除次數比較少,但查找多的情況。
由於維護這種高度平衡所付出的代價可能比從中獲得的效率收益還大,故而實際的應用不多,更多的地方是用追求局部而不是非常嚴格整體平衡的紅黑樹。
紅黑樹(Red Black Tree),它一種特殊的二叉查找樹,是AVL樹的特化變種,都是在進行插入和刪除操作時通過特定操作保持二叉查找樹的平衡,從而獲得較高的查找性能。
紅黑樹的平衡的要求是:從根到葉子的最長的路徑不會比於最短的路徑的長超過兩倍。 因此,紅黑樹是一種弱平衡二叉樹,在相同的節點情況下,AVL樹的高度<=紅黑樹。
紅黑樹是用弱平衡來換取增刪節點時候旋轉次數的降低,任何不平衡都會在三次旋轉之內解決,降低了對旋轉的要求,從而提高了性能,所以對於查詢,插入,刪除操作都較多的情況下,用紅黑樹。
1.2 紅黑樹的定義
AVL樹的定義如下:
- 它一定是一棵二叉排序樹;
- 它是一棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,並且左右兩個子樹都是一棵平衡二叉樹,遞歸定義。
相對於AVL樹,紅黑樹的定義明顯更加複雜:
- 它一定是一棵二叉排序樹;
- 每個節點或者爲黑色或者爲紅色;
- 根節點一定爲黑色;
- 如果一個節點是紅色的,那麼它的子節點要麼是null要麼是黑色的(也就是說,不能有兩個相鄰的紅色節點,即紅色節點的父、左子、右子節點只能是黑色節點)。
- 對於每個節點,從該節點到其所有葉子節點的路徑中都包含相同數量的黑色節點
根據上面的定義,可以推算出:
- 因爲黑色節點數量要一樣,紅色不能連着來,從而路徑全黑時最短,紅黑交替時最長。因此可以推算出:紅黑樹從根到葉子節點的最長的路徑不會比於最短的路徑的長超過兩倍。紅黑樹是一種弱平衡二叉樹,在相同的節點情況下,AVL樹的高度<=紅黑樹。
- 紅黑樹的高度最壞情況下爲2log(N+1)。因此它也可以在O(log n)時間內做查找,插入和刪除。
有一些紅黑樹定義還有一個性質:“紅黑樹中葉子節點爲最後的空節點,並且每個葉子節點是黑色的”。該定義並不會對之前的定義產生影響,其目的更多是爲了簡化平衡操作的情況,平衡時可以認爲:null就是黑色節點。此時只需要考慮紅和黑這兩種情況就行,而不用考慮非紅非黑的null。
如下圖(源自網絡):
1.3 紅黑樹的應用
紅黑樹是在1972年由Rudolf Bayer發明的,當時被稱爲平衡二叉B樹(symmetric binary B-trees)。後來,在1978年被 Leo J. Guibas 和 Robert Sedgewick 修改爲如今的“紅黑樹”。
實際上,Robert Sedgewick在《算法(第4版)》 中說過,紅黑樹等價於2-3樹。其中2-節點等價於普通平衡二叉樹的節點,3-節點本質上是非平衡性的緩存。
也就是說在添加、刪除節點之後需要重平衡時,相當於2-節點 與3-節點間的轉換,由於3-節點的緩存作用,能夠吸收一部分不平衡性,從而減少旋轉次數,減少重平衡時間。
儘管由於紅黑樹的最大高度高於AVL樹導致此查詢時稍慢,但是差距不大,而添加、刪除數據時紅黑樹快於AVL樹,紅黑樹的旋轉次數爲O(1),即最多旋轉3次;而AVL的旋轉次數爲O(logN),即最多向上旋轉至根節點。整體來看,紅黑樹的綜合效率高於AVL樹,紅黑樹比AVL樹的應用範圍更廣泛!
AVL的應用:
- Windows NT內核
紅黑樹的應用:
- JDK1.8及之後版本的Map實現,比如HashMap、TreeMap。
- 廣泛用於C++的STL中,map和set都是用紅黑樹實現的.
- 著名的linux進程調度Completely Fair Scheduler,用紅黑樹管理進程控制塊,進程的虛擬內存區域都存儲在一顆紅黑樹上,每個虛擬地址區域都對應紅黑樹的一個節點,左指針指向相鄰的地址虛擬存儲區域,右指針指向相鄰的高地址虛擬地址空間.
- IO多路複用epoll的實現採用紅黑樹組織管理sockfd,以支持快速的增刪改查.
- ngnix中,用紅黑樹管理timer,因爲紅黑樹是有序的,可以很快的得到距離當前最小的定時器。
2 自底向上實現原理
和AVL樹一樣,在插入和刪除之後,可能需要重平衡。紅黑樹的重平衡除了旋轉之外,還有改變着色。在下面的實現原理中,我們就會將null節點當成黑色節點,主要是爲了方便理解。
下面主要講解紅黑樹的插入和刪除操作,理解了這兩個操作,那麼其他操作比如求最值就非常簡單了。
要想理解紅黑樹的實現原理,必須先理解二叉排序樹的和平衡二叉樹的實現原理,其中紅黑樹的插入、刪除操作依賴於二叉排序樹的插入、刪除操作,插入、刪除之後的重平衡中的“旋轉”操作依賴於平衡二叉樹中的“旋轉”操作,同時紅黑二叉樹的重平衡中還增加了“變色”的操作。
本文中,對於基礎的插入操作和旋轉操作並沒有詳細講解,沒有掌握的的應該先看看下列文章:
- 二叉排序樹的詳解以及Java代碼的完全實現,其中有關於插入和刪除的詳細原理解釋,這是必須掌握的前置知識。
- 平衡二叉樹(AVL樹)的詳解以及Java代碼的完全實現,其中有關於節點旋轉的詳細原理解釋,這是必須掌握的前置知識。
- 多路查找樹中的2-3樹、2-3-4樹、B樹、B+樹的詳解,很多人說可以利用2-3樹來理解紅黑樹,或許可以,但實際上2-3樹不是必須掌握的前置知識。
2.1 插入操作
由於紅黑樹是一顆二叉排序樹,通常把節點作爲樹葉放到樹中。
如果我們把該節點塗成黑色,那麼肯定違反條件5,因爲將會建立一條更長的黑節點的路徑。因此,新節點必須塗成紅色。
如果它的父節點是黑色的,則插入完成。如果它的父節點已經是紅色的,那麼得到連續紅色的節點,這就違反了條件4。在這種悄況下,我們必須調整該樹以確保滿足條件5且又不引起條件4被破壞。用於完成這項任務的基本操作是節點顏色的改變和節點的旋轉。
插入步驟大概分爲兩步:
- 使用二叉查找樹的插入方法,將紅色節點插入;
- 如果此時樹結構不符合紅黑樹的要求,那麼通過旋轉和重新着色等一系列操作使之重新成爲一顆紅黑樹。
第一步插入就是使用的二叉排序樹的插入的方法,比較簡單。下面主要來看看插入節點之後是如何調整樹結構的!爲了方便,我們規定所有null節點都是黑色的!
插入過程中可能會用的關鍵節點如下:
插入之後的情況總結起來分爲如下幾種:
- 新插入節點N作爲根基點,簡稱“新根”;
- 新插入節點N的父節點爲黑色,簡稱“父黑”;
- 新插入節點N的父節點爲紅色,叔節點爲黑色,簡稱“父紅叔黑”;
- 新插入節點N的父節點爲紅色,叔節點爲紅色,簡稱“父紅叔紅”;
2.1.1 新根
新節點N作爲根節點,爲了滿足條件3,此時直接將N節點塗黑即可。
2.1.2 父黑
父節點爲黑色時,此時是天然的平衡。不需要任何調整。
2.1.3 父紅叔黑
若是叔節點是null節點則同樣默認爲黑色。父紅叔黑的情況又分爲四種情況:
- 在祖父節點G的左孩子節點P的左子樹中插入節點N,簡稱“LL”;
- 在祖父節點G的左孩子節點P的右子樹中插入節點N,簡稱“LR”;
- 在祖父節點G的右孩子節點P的左子樹中插入節點N,簡稱“RL”;
- 在祖父節點G的右孩子節點P的右子樹中插入節點N,簡稱“RR”。
其中第1種情況和第4種情況是對稱的,被稱爲發生“外邊”的情況或者稱爲外側插入,可以通過單旋轉來解決,而第2種情況和第3種情況是對稱的,被稱爲發生在“內邊”的情況或者稱爲內測插入,需要雙旋轉來解決。
如果瞭解AVL樹,那麼對上面的四種情況肯定不會陌生,因爲這就是AVL樹需要調整平衡的四種情況。 AVL樹對這四種情況的調整方法是:情況1採用單右旋、情況4採用單左旋即可解決問題。情況2需要採用左-右雙旋、情況3需要採用右-左雙旋。
紅黑樹的平衡也是採用同樣的方式旋轉,但是多了一個調整顏色的步驟。
2.1.3.1 LL
在節點G的左孩子節點P的左子樹中插入元素N,簡稱LL,即情況1,此時將G右旋,然後P塗黑,G塗紅;
2.1.3.2 RR
在節點G的右孩子節點P的右子樹中插入元素N,簡稱RR,即情況4,此時將G左旋,然後P塗黑,G塗紅;
2.1.3.3 LR
在節點G的左孩子節點P的右子樹中插入元素N,簡稱LR,即情況2,此時先將P左旋,然後將G右旋;然後N塗黑,G塗紅。
2.1.3.4 RL
在節點G的右孩子節點P的左子樹中插入元素N,簡稱RL,即情況3,此時先將P右旋,然後將G左旋;然後N塗黑,G塗紅。
2.1.3.5 總結
在上面4種情況中,旋轉後的新根節點最後均被塗成了黑色,這樣無論曾祖父節點(原祖父節點的父節點,即新根節點的父節點)是什麼顏色都能兼容。並且旋轉變色的結果對於原來通向各個子樹的黑色節點個數保持不變。
2.1.4 父紅叔紅
父節點和叔節點爲紅色時,此時就不能直接採用上面方法處理了。
此時可以將父/叔 (P/U) 節點塗黑,祖父節點(G)塗紅;而後以祖父節點(G)作爲新的平衡節點N,向上遞歸的執行平衡操作,直到不再發生兩個相連的紅色節點或者達到根(它將被重新塗成黑色)爲止。
該遞歸操作藉助棧結構來保存路徑,Java的實現直接使用方法遞歸就行了,因爲方法的啊遞歸就是借用的棧結構!
2.2 刪除操作
如同其他的二叉樹一樣,紅黑樹的刪除操作難於插入操作。紅黑樹的刪除操作任然和二叉排序樹的刪除類似,只不過加上旋轉和顏色屬性。
操作的步驟大概分爲兩步爲:
- 通過二叉排序樹的刪除方法,將該節點從紅黑樹中刪除;
- 如果此時樹結構不符合紅黑樹的要求,那麼通過旋轉和重新着色等一系列手段使之重新成爲一棵紅黑樹。
刪除操作的第一步就是根據二叉排序樹的規則尋找最終被刪除節點,對於真正需要被刪除的節點,二叉排序樹的尋找過程如下:
- 沒有子節點,那麼就是找到的節點;
- 要麼只有一個左或右子節點,那麼同樣就是找到的節點;
- 對於具有兩個子節點的節點,這裏採用尋找被刪除節點的右子樹的最小節點,然後交換該節點和右子樹最小節點值,這樣真正被刪除的節點還是轉換成爲了1或者2的情況(二叉排序樹的右子樹的最小節點只可能是“沒有子節點”或者“只有右子節點”的情況)。
因此,最終被刪除節點只能是沒有子節點,或者只有一個左或右子節點的情況。在下面的介紹中,“被刪除的節點“均是指“真正被刪除的節點”。
在刪除操作中,可能會用到的關鍵節點如下:
單獨的刪除過程比較簡單,我們主要看在刪除之後是如何重新調整平衡成爲紅黑樹的。爲了方便,同樣將null節點看成黑色!
根據最終被刪除節點的情況,可以分爲以下幾種情況:
- 被刪除的節點N爲紅色,簡稱“刪紅”;
- 被刪除的節點N爲黑色,子節點C爲紅色,簡稱“刪黑子紅”;
- 被刪除的節點N爲黑色,子節點C爲黑色(null算黑色),簡稱“刪黑子黑”;
2.2.1 刪紅
如果被刪除節點N是紅色的,那麼一切好辦,由於是N紅色節點,刪除N後不會影響紅黑樹的平衡性,與其他節點的顏色無關,因此不需要做任何調整。
上圖的二叉樹中,如果要刪除BR,那麼直接刪除該節點就行了,很簡單;
如果要刪除P,實際上真正應該被刪除的節點是BL節點,那麼實際上也被轉換成“刪紅”的情況,只需要在刪除BL之前交換P和BL的key、value值。
2.2.2 刪黑子紅
如果被刪除節點N爲黑色,則該節點所在的路徑上的黑色節點總數將會減少1,紅黑樹失去平衡,需要調整。
如果被刪除節點N的子節點C(無論左右)爲紅色,那麼C肯定存在並且C是N路徑上的唯一後繼,如果把C的顏色設置爲黑色,那麼就能恢復N之前所在路徑的黑色節點的總數,與其他節點的顏色無關,平衡調整完成。
2.2.3 刪黑子黑
如果被刪除節點N爲黑色,子節點C也爲黑色(null也看成黑色),因爲子節點本身就是黑色,那麼就不能簡單的後繼子節點C塗黑來補充黑色這麼簡單了,那麼需要分三種情況考慮:
- 被刪除節點N是根節點,簡稱“刪根”;
- 被刪除節點N的兄弟節點B爲紅色,簡稱“刪兄紅”;
- 被刪除節點N的兄弟節點B爲黑色(null也看成黑色),簡稱“刪兄黑”;
2.2.3.1 刪根
被刪除節點N是根節點,那好辦,直接刪除就行了,任何無需平衡操作。
2.2.3.2 刪兄紅
如果不是根節點,並且被刪除節點N的兄弟節點B爲紅色。
這裏如果子節點C爲null也看成黑色節點,並且由於紅黑樹的定義,我們可以斷定B的子節點一定都是黑色(null也看成黑色),P一定是黑色。
刪除節點N之後,C代替了N的位置,繼續判定原兄弟節點B是左兄弟還是右兄弟,又分兩種情況討論:
- B在右邊,簡稱“右兄弟”;
- B在左邊,簡稱“左兄弟”;
2.2.3.2.1 右兄弟
如果B是右兄弟,則以父節點P爲基點左旋,然後B塗黑、P塗紅,最後將BL看成新的兄弟節點newB;
2.2.3.2.2 左兄弟
如果B是左兄弟,則以父節點P爲基點右旋,然後B塗黑、P塗紅,最後將BR看成新的兄弟節點newB;
2.2.3.2.3 總結
我們發現,實際上情況2的處理就是先轉變成情況3-“刪兄黑”,然後統一處理。
2.2.3.3 刪兄黑
如果不是根節點,並且被刪除節點的兄弟節點爲黑色(null也當成黑色),這裏又分爲兩種情況:
- 兄節點B的子節點全是黑色節點,簡稱“兄子全黑”;
- 兄節點B的子節點不全是黑色節點,簡稱“兄子非全黑”;
2.2.3.3.1 兄子全黑
兄弟節點的子節點全是黑色(null節點當成黑色節點,如果兄弟節點也是null節點,也,兄子節點同樣算黑色節點);此時,不用區分左右,但根據父節點又分爲兩種情況:
- 父節點P是黑色,簡稱“父黑”;
- 父節點P是紅色,簡稱“父紅”;
2.2.3.3.1.1 父黑
父節點P是黑色。
將兄弟節點B塗紅,將父節點P設爲新的C節點,將U設爲新B節點,將G設爲新P節點,回到 刪黑子黑的 情況,即向上遞歸進行處理,直到C成爲根節點或者達到平衡。
2.2.3.3.1.2 父黑
父節點P是紅色。
將兄弟節點B塗紅,父節點P塗黑即可。此時原本刪除黑色節點的路徑補充了黑色節點,即可達到平衡。
2.2.3.3.2 兄子非全黑
兄弟節點的子節點不全是黑色,那麼可以分爲左紅右黑、左黑右紅、左紅右紅;此時分爲以下幾種情況:
- 兄弟節點B在右邊,且兄右子節點BR爲黑色,簡稱“兄右,右子黑”;
- 兄弟節點B在右邊,且兄右子節點BR爲紅色,簡稱“兄右,右子紅”;
- 兄弟節點B在左邊,且兄左子節點BL爲黑色,簡稱“兄左,左子黑”;
- 兄弟節點B在左邊,且兄左子節點BL爲紅色,簡稱“兄左,左子紅”;
2.2.3.3.2.1 兄右,右子黑
兄弟節點B在右邊,且兄右子節點BR爲黑色。
此時兄左子節點BL肯定爲紅色,以兄弟節點B爲基點右旋,然後BL塗黑,B塗紅,然後將BL當成新B,B當成新BR,這樣就轉換成了情況2-“兄右,右子紅”。
2.2.3.3.2.2 兄右,右子紅
兄弟節點B在右邊,且兄右子節點BR爲紅色;
此時兄左子節點BL可紅可黑,父節點P可紅可黑。以父節點P爲基點左旋,然後交換P和B的顏色,BR塗黑,平衡完畢!
2.2.3.3.2.3 兄左,左子黑
兄弟節點B在左邊,且兄左子節點BL爲黑色。
該情況是情況1的對稱情況。此時兄右子節點肯定爲紅色,以兄弟節點B爲基點左旋,然後BR塗黑,B塗紅,然後將BR當成新B,B當成新BL,這樣就轉換成了情況4-“兄左,左子紅”。
2.2.3.3.2.4 兄左,左子紅
兄弟節點B在左邊,且兄左子節點BL爲紅色。
該情況是情況2的對稱情況。此時兄右子節點BR可紅可黑,父節點P可紅可黑。以父節點P爲基點右旋,然後交換P和B的顏色,BL塗黑,平衡完畢!
2.2.3.3.2.5 總結
實際上情況1、3是對稱的,情況2、4是對稱的。上面的步驟的實質是利用旋轉恢復被刪除節點N路徑上的黑色節點總數,最後P和B雖然交換了顏色,但它們最後都作爲N路徑的祖先節點,因此n路徑上的黑色節點數增加1,恢復了N路徑的黑色節點數量。同時紅色子節點最終塗黑,保證了該條路徑的黑色節點數目。
2.3 總結
上面的只是理論原理,在下面的實現中,提供了根據實際案例進行插入和刪除的執行流程詳盡分析!
2.3.1 插入
2.3.2 刪除
3 紅黑樹的實現
3.1 實現代碼
key-value形式的紅黑樹的簡單實現,該實現並沒有藉助NIL哨兵節點,因此額外處理了節點爲null的情況,增加了代碼量,實際上藉助NIL的實現更加簡單和易懂,如果看懂了沒有藉助NIL的實現,那麼藉助NIL的實現應該很容易改該寫出來。
本實現的註釋非常詳盡,如果瞭解了上面講的原理應該很容易看懂!
/**
* key-value形式的紅黑樹的簡單實現,該實現並沒有藉助NIL哨兵節點
* 主要方法:
* =====public=====
* {@link RedBlackTree#RedBlackTree()} 構建紅黑樹對象,使用自然比較器
* {@link RedBlackTree#RedBlackTree(Comparator)} 構建紅黑樹對象,使用自定義比較器
* {@link RedBlackTree#put(Object, Object)} 存放k-v鍵值對,將按照key的大小排序
* {@link RedBlackTree#remove(Object)} 根據key嘗試查找並嘗試刪除對應的鍵值對
* {@link RedBlackTree#toInorderTraversalString()} 中序遍歷紅黑樹(順序輸出)
* {@link RedBlackTree#minKey()} 獲取最小的key
* {@link RedBlackTree#maxKey()} 獲取最大的key
* {@link RedBlackTree#get(Object)} 根據key獲取value
* =====private=====
* {@link RedBlackTree#binaryTreePut(RedBlackNode, RedBlackNode)} 使用二叉排序樹的方式嘗試插入節點
* {@link RedBlackTree#rebalanceAfterPut(RedBlackNode)} 插入節點之後進行重平衡
* {@link RedBlackTree#searchRemoveNode(Object, RedBlackNode)} 使用二叉排序樹的方式嘗試尋找真正需要被刪除的節點
* {@link RedBlackTree#removeNode(RedBlackNode)} 使用二叉排序的方式刪除節點,並對部分簡單情況進行重平衡
* {@link RedBlackTree#rebalanceAfterRemove(RedBlackNode, RedBlackNode)} 對複雜情況進行重平衡
* {@link RedBlackTree#rotateLeft(RedBlackNode)} 左旋
* {@link RedBlackTree#rotateRight(RedBlackNode)} 右旋
* {@link RedBlackTree#rotateLeftAndRight(RedBlackNode)} 左-右雙旋
* {@link RedBlackTree#rotateRightAndLeft(RedBlackNode)} 右-左雙旋
*
* @author lx
*/
public class RedBlackTree<K, V> {
/**
* 自定義比較器
*/
private Comparator<? super K> cmp;
/**
* 樹節點的數量
*/
private int size;
/**
* 紅黑樹的根節點,默認爲null
*/
private RedBlackNode<K, V> root;
/**
* 紅黑樹節點對象
*
* @param <K> key類型
* @param <V> value類型
*/
private static final class RedBlackNode<K, V> {
/**
* 節點顏色,true 紅色 false 黑色
*/
boolean red;
/**
* 關鍵字
*/
K key;
/**
* 值
*/
V value;
/**
* 左子節點
*/
RedBlackNode<K, V> left;
/**
* 左子節點
*/
RedBlackNode<K, V> right;
/**
* 父節點
*/
RedBlackNode<K, V> parent;
public RedBlackNode(boolean red, K key, V value, RedBlackNode<K, V> left, RedBlackNode<K, V> right) {
this.red = red;
this.key = key;
this.value = value;
this.left = left;
this.right = right;
}
public RedBlackNode(boolean red, K key, V value) {
this.red = red;
this.key = key;
this.value = value;
}
public RedBlackNode(K key, V value) {
this.key = key;
this.value = value;
}
public RedBlackNode() {
}
public RedBlackNode(RedBlackNode<K, V> parent) {
this.parent = parent;
}
@Override
public String toString() {
return "{" +
"key=" + key +
", value=" + value +
'}';
}
}
/**
* 空的構造器,使用自然順序比較
*/
public RedBlackTree() {
}
/**
* 指定比較器
*
* @param cmp 指定比較器
*/
public RedBlackTree(Comparator<? super K> cmp) {
this.cmp = cmp;
}
/**
* 插入,開放給外部使用的api
* 插入步驟大概分爲兩步:
* 1) 使用二叉查找樹的插入方法,將紅色節點插入;
* 2) 如果此時樹結構不符合紅黑樹的要求,那麼通過旋轉和重新着色等一系列操作使之重新成爲一顆紅黑樹。
*
* @param key 插入的key
* @param value 插入的value
*/
public void put(K key, V value) {
/*0檢查key*/
checkKey(key);
/*1二叉排序樹的插入,需要增加的步驟是記錄父節點*/
//創建被插入的節點,默認紅色
RedBlackNode<K, V> kvRedBlackNode = new RedBlackNode<>(true, key, value, null, null);
//返回root,但此時新的節點可能已經被插入進去了
int oldSize = size;
root = binaryTreePut(kvRedBlackNode, root);
/*2如果確實插入了節點元素,那麼調整平衡*/
if (size > oldSize) {
rebalanceAfterPut(kvRedBlackNode);
}
}
/**
* 根據key刪除鍵值對,大概分爲兩步:
* 1) 通過二叉排序樹的刪除方法,將該節點從紅黑樹中刪除;
* 2) 如果此時樹結構不符合紅黑樹的要求,那麼通過旋轉和重新着色等一系列手段使之重新成爲一棵紅黑樹。
* <p>
* 該實現實際上分爲三步:
* searchRemoveNode: 使用二叉排序的刪除方法,嘗試尋找需要真正被刪除的Node節點,沒有進行刪除
* removeNode: 用二叉排序的方法刪除節點,並進行部分調整,對於複雜的情況,返回需要進行重平衡的c節點
* rebalanceAfterRemove: 如果c不等於null,說明被刪除節點N是黑色節點且需要進行進一步的重平衡
*
* @param key 需要刪除的key
* @return 被刪除的key對應的value
*/
public V remove(K key) {
checkKey(key);
/*1使用二叉排序的刪除方法,嘗試尋找需要真正被刪除的Node節點,沒有進行刪除*/
RedBlackNode<K, V> removeNode = searchRemoveNode(key, root);
if (removeNode == null) {
return null;
}
V value = removeNode.value;
/*2、用二叉排序的方法刪除節點,並進行部分調整,對於複雜的情況,返回需要進行重平衡的c節點*/
RedBlackNode<K, V> n = removeNode(removeNode);
/*3、如果c不等於null,說明被刪除節點N是黑色節點且需要進行進一步的重平衡*/
if (n != null) {
rebalanceAfterRemove(n.right, n.parent);
}
return value;
}
/**
* 用於刪除節點並且進行部分重平衡:對於“刪紅”,“刪黑子紅”,“刪黑子黑-刪根”這三種簡單的情況進行了判斷和重平衡
*
* @param n 真正需要被刪除的節點,該節點要麼沒有子節點,要麼只有一個左或者右子節點
* @return 還需要進一步被重平衡操作的節點,或者null-表示不需要進一步重平衡操作
*/
private RedBlackNode<K, V> removeNode(RedBlackNode<K, V> n) {
/*首先刪除子節點*/
RedBlackNode<K, V> left = n.left;
RedBlackNode<K, V> right = n.right;
RedBlackNode<K, V> parent = n.parent;
//n的子節點
RedBlackNode<K, V> child = null;
/*如果父節點不爲null*/
if (parent != null) {
//如果n是父節點的左子節點
if (parent.left == n) {
//如果n的左右子節點都爲null
if (left == null && right == null) {
parent.left = null;
child = null;
}
//如果n的左子節點不爲null
else if (left != null) {
parent.left = left;
left.parent = parent;
child = left;
}
//如果n的右子節點不爲null
else {
parent.left = right;
right.parent = parent;
child = right;
}
}
//如果n是父節點的右子節點
if (parent.right == n) {
//如果n的左右子節點都爲null
if (left == null && right == null) {
parent.right = null;
child = null;
}
//如果n的左子節點不爲null
else if (left != null) {
parent.right = left;
left.parent = parent;
child = left;
}
//如果n的右子節點不爲null
else {
parent.right = right;
right.parent = parent;
child = right;
}
}
}
/*如果父節點爲null,則說明要刪除的節點是根節點*/
else {
//如果n的左右子節點都爲null
if (left == null && right == null) {
root = null;
child = null;
}
//如果n的左子節點不爲null
else if (left != null) {
left.parent = null;
root = left;
child = left;
}
//如果n的右子節點不爲null
else {
right.parent = null;
root = right;
child = right;
}
}
/*1 被刪除的節點爲紅色,這裏處理了;*/
if (n.red) {
/*1.2由於是N紅色節點,刪除N後不會影響紅黑樹的平衡性,與其他節點的顏色無關,因此不需要做任何調整。*/
return null;
}
/*2 被刪除的節點爲黑色,子節點爲紅色,這裏處理了;*/
else if (child != null && child.red) {
/*2.1首先刪除節點*/
/*2.2將後繼C塗黑,平衡調整完成。*/
child.red = false;
return null;
}
/*3 被刪除的節點爲黑色,子節點爲黑色;
* 這裏又要分爲多種情況:
* 1) 被刪除節點N是根節點,簡稱“刪根”,這裏處理了;
* 2) 被刪除節點N的兄弟節點B爲紅色,簡稱“刪兄紅”,這裏沒有處理,在rebalanceAfterRemove方法中處理;
* 3) 被刪除節點N的兄弟節點B爲黑色(null也看成黑色),簡稱“刪兄黑”,這裏沒有處理,在rebalanceAfterRemove方法中處理;
* */
else {
/*3.1 被刪除節點N是根節點,簡稱“刪根”*/
/*那好辦,任何無需平衡操作*/
if (n.parent == null) {
return null;
}
/*剩下兩種複雜情況,此時需要進一步進行復雜的平衡操作*/
//將n的子節點(null或左子節點或右子節點)設置爲右子節點,方便後面調整的時候取出來
n.right = child;
return n;
}
}
/**
* 刪除節點之後進行重平衡,用於處理下面兩種複雜的情況
* 2) 被刪除節點N的兄弟節點B爲紅色,簡稱“刪兄紅”;
* 3) 被刪除節點N的兄弟節點B爲黑色(null也看成黑色),簡稱“刪兄黑”;
*
* @param c 需要進行平衡的節點
* @param p 需要進行平衡的節點的父節點
*/
private void rebalanceAfterRemove(RedBlackNode<K, V> c, RedBlackNode<K, V> p) {
//獲取兄弟節點
RedBlackNode<K, V> brother;
/*如果c是左子節點,那麼brother就是右兄弟*/
if (c == p.left) {
brother = p.right;
/*3.2 被刪除節點N的兄弟節點爲紅色,簡稱刪兄紅; 且是右兄弟*/
if (brother != null && brother.red) {
/*以父節點P爲基點左旋*/
rotateLeft(p);
/*然後B塗黑P塗紅(交換顏色)*/
brother.red = false;
p.red = true;
/*最後將BL看成新的兄弟節點newB;將情況二轉換爲情況三*/
brother = p.right;
}
/*3.3 如果被刪除節點N的兄弟節點爲黑色,簡稱刪兄黑;且是右兄弟*/
/*3.3.1 兄節點的子節點不全是黑色節點,簡稱兄子非全黑;下面針對右兄弟分爲兩種情況*/
if (brother != null) {
//排除都爲null
if (!(brother.right == null && brother.left == null)) {
//排除都爲黑色
if (!(brother.right != null && !brother.right.red && brother.left != null && !brother.left.red)) {
/*3.3.1.1 兄弟節點在右邊,且兄右子節點爲黑色;*/
if (brother.right == null || !brother.right.red) {
if (brother.left != null) {
brother.left.red = false;
}
brother.red = true;
/*以兄弟節點B爲基點右旋*/
rotateRight(brother);
/*將BL當成新B,B當成新BR,這樣就轉換成了情況2。*/
brother = p.right;
}
/*3.3.1.2 兄弟節點在右邊,且兄右子節點爲紅色;*/
if (brother.right != null && brother.right.red) {
/*交換P和B的顏色*/
boolean color = p.red;
p.red = brother.red;
brother.red = color;
/*BR塗黑*/
brother.right.red = false;
/*以父節點P爲基點左旋*/
rotateLeft(p);
return;
}
}
}
}
}
/*如果c是右子節點,那麼brother就是左兄弟*/
else {
brother = p.left;
/*3.2 被刪除節點N的兄弟節點爲紅色,簡稱刪兄紅;且是左兄弟*/
if (brother != null && brother.red) {
/*以父節點P爲基點右旋,然後B塗黑P塗紅(交換顏色),最後將BR看成新的兄弟節點newB;
* 將情況二轉換爲情況三*/
/*以父節點P爲基點右旋*/
rotateRight(p);
/*然後B塗黑P塗紅(交換顏色)*/
brother.red = false;
p.red = true;
/*最後將BR看成新的兄弟節點newB;將情況二轉換爲情況三*/
brother = p.left;
}
/*3.3 如果被刪除節點N的兄弟節點爲黑色,簡稱刪兄黑;且是左兄弟*/
/*3.3.1 兄節點的子節點不全是黑色節點,簡稱兄子非全黑;下面針對左兄弟分爲兩種情況*/
if (brother != null) {
//排除都爲null
if (!(brother.right == null && brother.left == null)) {
//排除都爲黑色
if (!(brother.right != null && !brother.right.red && brother.left != null && !brother.left.red)) {
/*3.3.1.3 兄弟節點在左邊,且兄左子節點爲黑色;*/
if (brother.left == null || !brother.left.red) {
if (brother.right != null) {
brother.right.red = false;
}
brother.red = true;
/*以兄弟節點B爲基點左旋*/
rotateLeft(brother);
/*將BR當成新B,B當成新BL,這樣就轉換成了情況4。*/
brother = p.left;
}
/*3.3.1.4 兄弟節點在左邊,且兄左子節點爲紅色;*/
if (brother.left != null && brother.left.red) {
/*交換P和B的顏色*/
boolean color = p.red;
p.red = brother.red;
brother.red = color;
/*BL塗黑*/
brother.left.red = false;
/*以父節點P爲基點右旋*/
rotateRight(p);
return;
}
}
}
}
}
/*3.3.2 兄節點的子節點全是黑色節點,簡稱兄子全黑;*/
/*如果兄弟節點爲null,也算全黑*/
if (brother == null) {
/*3.3.2.1 父節點P是黑色;*/
/*如果父節點還有父節點,那麼進行遞歸,否則沒有意義*/
if (p.parent != null && !p.red) {
rebalanceAfterRemove(p, p.parent);
}
/*3.3.2.2 父節點P是紅色;*/
else {
p.red = false;
}
} else {
//都爲null
if (brother.left == null && brother.right == null) {
/*3.3.2.1 父節點P是黑色;*/
/*如果父節點還有父節點,那麼進行遞歸,否則沒有意義*/
if (p.parent != null && !p.red) {
/*將兄弟節點B塗紅,將父節點P設爲新的C節點,將U設爲新B節點,將G設爲新P節點,
回到刪黑子黑的情況,即向上遞歸進行處理,直到C成爲根節點或者達到平衡。*/
brother.red = true;
rebalanceAfterRemove(p, p.parent);
}
/*3.3.2.2 父節點P是紅色;*/
else {
brother.red = true;
p.red = false;
}
}
//都爲黑色
else if (brother.left != null && brother.right != null && !brother.left.red && !brother.right.red) {
/*3.3.2.1 父節點P是黑色;*/
/*如果父節點還有父節點,那麼進行遞歸,否則沒有意義*/
if (p.parent != null && !p.red) {
/*將兄弟節點B塗紅,將父節點P設爲新的C節點,將U設爲新B節點,將G設爲新P節點,
回到刪黑子黑的情況,即向上遞歸進行處理,直到C成爲根節點或者達到平衡。*/
brother.red = true;
rebalanceAfterRemove(p, p.parent);
}
/*3.3.2.2 父節點P是紅色;*/
else {
brother.red = true;
p.red = false;
}
}
}
}
/**
* 嘗試尋找需要真正被刪除的Node節點
*
* @param key 匹配的key
* @param root 從根節點開始遞歸查找
* @return 找到的節點, 或者爲null, 表示沒找到
*/
private RedBlackNode<K, V> searchRemoveNode(K key, RedBlackNode<K, V> root) {
if (root == null) {
return null;
}
/*調用比較的方法*/
int i = compare(key, root.key);
/*如果大於0,則說明e>root.data 繼續查詢右子樹*/
if (i > 0) {
return searchRemoveNode(key, root.right);
}
/*如果小於0,則說明e<root.data 繼續查詢左子樹*/
else if (i < 0) {
return searchRemoveNode(key, root.left);
} else {
/*如果等於0,則說明key=root 即查詢成功 開始做好刪除的準備,返回真正需要被刪除的節點*/
size--;
/*如果兩個子節點都不爲null*/
if (root.left != null && root.right != null) {
/*遞歸查找最小的節點*/
RedBlackNode<K, V> min = findMin(root.right);
/*然後交換元素值*/
K tempKey = min.key;
min.key = root.key;
root.key = tempKey;
V tempValue = min.value;
min.value = root.value;
root.value = tempValue;
/*返回真正需要被刪除的節點,即在右子樹中找到的大於root的最小節點*/
return min;
} else {
/*如果一個子節點不爲null,則返回該子節點;或者兩個子節點都爲null,則返回該節點*/
return root;
}
}
}
/**
* 查找真正被刪除的最小的節點
*
* @param root 根節點
* @return 最小的節點
*/
private RedBlackNode<K, V> findMin(RedBlackNode<K, V> root) {
/*如果該節點沒有左子節點,那麼該節點就是最小的節點,返回*/
if (root.left == null) {
return root;
}
/*如果該節點存在左子節點,那麼繼續向左遞歸查找*/
return findMin(root.left);
}
/**
* 查找最小的key
*
* @return 最小的節點的key
*/
public K minKey() {
if (root == null) {
return null;
}
/*如果該節點存在左子節點,那麼繼續向左遞歸查找*/
return findMin(root).key;
}
private RedBlackNode<K, V> findMax(RedBlackNode<K, V> root) {
/*如果該節點沒有右子節點,那麼該節點就是最小的節點,返回*/
if (root.right == null) {
return root;
}
/*如果該節點存在左子節點,那麼繼續向左遞歸查找*/
return findMax(root.right);
}
/**
* 查找最大的key
*
* @return 最大的節點的key
*/
public K maxKey() {
if (root == null) {
return null;
}
/*如果該節點存在左子節點,那麼繼續向左遞歸查找*/
return findMax(root).key;
}
/**
* 根據key,查找value
*
* @return 最大的節點的key
*/
public V get(K Key) {
/*如果該節點存在左子節點,那麼繼續向左遞歸查找*/
RedBlackNode<K, V> kvRedBlackNode = searchRemoveNode(Key, root);
if (kvRedBlackNode != null) {
return kvRedBlackNode.value;
}
return null;
}
/**
* 對元素進行比較大小的方法,如果傳遞了自定義比較器,則使用自定義比較器,否則則需要數據類型實現Comparable接口
*
* @param k1 被比較的第一個對象
* @param k2 被比較的第二個對象
* @return 0 相等 ;小於0 k1 < k2 ;大於0 k1 > k2
*/
private int compare(K k1, K k2) {
if (cmp != null) {
return cmp.compare(k1, k2);
} else {
return ((Comparable<K>) k1).compareTo(k2);
}
}
/**
* 添加節點之後再平衡,需要分多種情況討論:
* 1) 新插入節點N作爲根基點,簡稱“新根”;
* 2) 新插入節點N的父節點爲黑色,簡稱“父黑”;
* 3) 新插入節點N的父節點爲紅色,叔節點爲黑色,簡稱“父紅叔黑”;
* 4) 新插入節點N的父節點爲紅色,叔節點爲紅色,簡稱“父紅叔紅”;
*
* @param newNode 新增加的節點
*/
private void rebalanceAfterPut(RedBlackNode<K, V> newNode) {
//獲取父節點
RedBlackNode<K, V> parent = newNode.parent;
/*1 新插入節點N作爲根基點,簡稱“新根”*/
if (parent == null) {
//直接塗黑即可
newNode.red = false;
return;
}
/*2 新插入節點N的父節點爲黑色,簡稱“父黑”*/
else if (!parent.red) {
//無需調整
return;
}
/*3 新插入節點N的父節點爲紅色 下面需要分情況討論*/
/*3.1首先獲取一系列與新節點相關的的節點*/
//獲取祖父節點
RedBlackNode<K, V> grandParent = parent.parent;
//獲取叔節點
RedBlackNode<K, V> uncle = parent == grandParent.left ? grandParent.right : grandParent.left;
/*3.2 叔節點爲紅色,簡稱“父紅叔紅”*/
if (uncle != null && uncle.red) {
/*將newNode節點的父節點叔節點顏色染成黑色,祖父節點染成紅色。*/
parent.red = false;
uncle.red = false;
grandParent.red = true;
/*此時祖父節點可能與其父節點顏色衝突,因此需要遞歸的解決*/
rebalanceAfterPut(grandParent);
}
/*3.3 如果叔節點爲黑色,,簡稱“父紅叔黑” 需要分四種情況討論
* 1) 在祖父節點G的左孩子節點P的左子樹中插入節點N,簡稱“LL”; 右旋,換色
* 2) 在祖父節點G的左孩子節點P的右子樹中插入節點N,簡稱“LR”; 左旋+右旋,換色
* 3) 在祖父節點G的右孩子節點P的左子樹中插入節點N,簡稱“RL”; 右旋+左旋,換色
* 4) 在祖父節點G的右孩子節點P的右子樹中插入節點N,簡稱“RR”。 左旋,換色
* */
else {
/*3.3.1 在祖父節點G的左孩子節點P的左子樹中插入節點N,簡稱“LL”; 右旋,換色*/
if (grandParent.left == parent && parent.left == newNode) {
/*P塗黑,G塗紅*/
parent.red = false;
grandParent.red = true;
/*以G爲基點右旋*/
rotateRight(grandParent);
}
/*3.3.2 在祖父節點G的左孩子節點P的右子樹中插入節點N,簡稱“LR”; 左旋+右旋,換色*/
else if (grandParent.left == parent && parent.right == newNode) {
/*N塗黑,G塗紅*/
newNode.red = false;
grandParent.red = true;
/*左-右雙旋*/
rotateLeftAndRight(grandParent);
}
/*3.3.3 在祖父節點G的右孩子節點P的左子樹中插入節點N,簡稱“RL”; 右旋+左旋,換色*/
else if (grandParent.right == parent && parent.left == newNode) {
/*N塗黑,G塗紅*/
newNode.red = false;
grandParent.red = true;
/*右-左雙旋*/
rotateRightAndLeft(grandParent);
}
/*3.3.4 在祖父節點G的右孩子節點P的右子樹中插入節點N,簡稱“RR”。 左旋,換色*/
else {
/*P塗黑,G塗紅*/
parent.red = false;
grandParent.red = true;
/*以G爲基點左旋*/
rotateLeft(grandParent);
}
}
}
/**
* LL,右旋,類似於AVL樹的右旋操作,但是需要更新父節點,並且不需要返回值,因爲這並不是遞歸的再平衡
* 這裏的k1k2對應着右旋模型中的點位
* 通解:右旋之後,k2成爲根節點,k1成爲k2的右子節點,k2的右子樹2成爲k1的左子樹
*
* @param k1 右旋的基點
*/
private void rotateRight(RedBlackNode<K, V> k1) {
//獲取基點的父節點
RedBlackNode<K, V> parent = k1.parent;
//獲取k2,k2是k1的左子節點
RedBlackNode<K, V> k2 = k1.left;
//k2的右子樹成爲k1的左子樹
k1.left = k2.right;
//如果k2的右子樹不是null樹,則更新右子樹的父節點的引用
if (k2.right != null) {
k2.right.parent = k1;
}
//k1成爲k2的右子節點
k2.right = k1;
//更新k1的父節點的引用
k1.parent = k2;
//如果k1之前是根節點,則k2成爲根節點,更新根節點的引用
if (parent == null) {
this.root = k2;
//如果k1是父節點的左子節點,那麼更新父節點的左子節點的引用
} else if (k1 == parent.left) {
parent.left = k2;
} else {
//如果k1是父節點的右子節點,那麼更新父節點的右子節點的引用
parent.right = k2;
}
//最後更新k2的父節點的引用
k2.parent = parent;
}
/**
* RR,左旋,類似於AVL樹的左旋操作,屬於右旋的鏡像,但是需要更新父節點,並且不需要返回值,因爲這並不是遞歸的再平衡
* 這裏的k1k2對應着左旋模型中的點位
* 通解:左旋之後,k2成爲根節點,k1成爲k2的左子節點,k2的左子樹2成爲k1的右子樹
*
* @param k1 左旋的基點
*/
private void rotateLeft(RedBlackNode<K, V> k1) {
//獲取基點的父節點
RedBlackNode<K, V> parent = k1.parent;
//獲取k2,k2是k1的右子節點
RedBlackNode<K, V> k2 = k1.right;
//k2的左子樹成爲k1的右子樹
k1.right = k2.left;
//如果k2的左子樹不是null樹,則更新左子樹的父節點的引用
if (k2.left != null) {
k2.left.parent = k1;
}
//k1成爲k2的左子節點
k2.left = k1;
//更新k1的父節點的引用
k1.parent = k2;
//如果k1之前是根節點,則k2成爲根節點,更新根節點的引用
if (parent == null) {
this.root = k2;
//如果k1是父節點的左子節點,那麼更新父節點的左子節點的引用
} else if (k1 == parent.left) {
parent.left = k2;
} else {
//如果k1是父節點的右子節點,那麼更新父節點的右子節點的引用
parent.right = k2;
}
//最後更新k2的父節點的引用
k2.parent = parent;
}
/**
* RL,右-左雙旋,類似於AVL樹的右-左雙旋
* 通解:將k3當作新的根節點,並且先使得k2右旋成爲k3的右子樹,然後k1左旋成爲k3的左子樹,並且左子樹2成爲k1的右子樹,右子樹2成爲k2的左子樹
*
* @param k1 需要旋轉的最小不平衡樹根節點
*/
private void rotateRightAndLeft(RedBlackNode<K, V> k1) {
/*1先對k1的右子節點k2進行右旋,然後使得成爲k3成爲的k1的左子樹*/
rotateRight(k1.right);
/*2然後對k1進行左旋,成爲k3的左子樹,返回的根節點就是k3,即返回旋轉之後的根節點*/
rotateLeft(k1);
}
/**
* LR,左-右雙旋,類似於AVL樹的左-右雙旋,很簡單,實際上就是右-左雙旋的鏡像
* 通解: 將k3當作新的根節點,並且先使得k2左旋成爲k3的左子樹,然後k1右旋成爲k3的右子樹,並且左子樹2成爲k2的右子樹,右子樹2成爲k1的左子樹
*
* @param k1 需要旋轉的最小不平衡樹根節點
*/
private void rotateLeftAndRight(RedBlackNode<K, V> k1) {
/*1先對k1的左子節點k2進行左旋,使得k3成爲的k1的左子樹*/
rotateLeft(k1.left);
/*2然後對k1進行右旋,成爲k3的右子樹,此時根節點就變成了k3*/
rotateRight(k1);
}
/**
* 檢查key
*
* @param key key
*/
private void checkKey(K key) {
if (key == null) {
throw new NullPointerException("key爲null");
}
}
/**
* 二叉排序樹的方式插入節點,同時記錄父節點
*
* @param kvRedBlackNode 需要被插入的節點
* @param root 根節點
*/
private RedBlackNode<K, V> binaryTreePut(RedBlackNode<K, V> kvRedBlackNode, RedBlackNode<K, V> root) {
/*沒有查找到,那麼直接構建新的節點返回,將會在上一層方法中被賦值給其父節點的某個引用,這個插入的位置肯定是該遍歷路徑上的最後一點
* 即插入的元素節點肯定是屬於葉子節點*/
if (root == null) {
size++;
return kvRedBlackNode;
}
/*調用比較的方法*/
int i = compare(kvRedBlackNode.key, root.key);
/*如果大於0,則說明kvRedBlackNode>root 繼續查詢右子樹*/
if (i > 0) {
//增加更新父節點的操作
kvRedBlackNode.parent = root;
root.right = binaryTreePut(kvRedBlackNode, root.right);
/*如果小於0,則說明kvRedBlackNode<root 繼續查詢左子樹*/
} else if (i < 0) {
//增加更新父節點的操作
kvRedBlackNode.parent = root;
root.left = binaryTreePut(kvRedBlackNode, root.left);
} else {
/*如果等於0,則說明kvRedBlackNode=root 即已經存在節點 替換value*/
root.value = kvRedBlackNode.value;
}
//沒查詢到最底層,則返回該節點
return root;
}
/**
* 保存遍歷出來的節點數據
*/
List<RedBlackNode<K, V>> str = new ArrayList<>();
/**
* 中序遍歷,提供給外部使用的api
*
* @return 遍歷的數據
*/
public String toInorderTraversalString() {
//如果是空樹,直接返回空
if (root == null) {
return null;
}
//從根節點開始遞歸
inorderTraversal(root);
//獲取遍歷結果
String s = str.toString();
str.clear();
return s;
}
/**
* 中序遍歷 內部使用的遞歸遍歷方法,借用了棧的結構
*
* @param root 節點,從根節點開始
*/
private void inorderTraversal(RedBlackNode<K, V> root) {
RedBlackNode<K, V> left = getLeft(root);
if (left != null) {
//如果左子節點不爲null,則繼續遞歸遍歷該左子節點
inorderTraversal(left);
}
//添加數據節點
str.add(root);
//獲取節點的右子節點
RedBlackNode<K, V> right = getRight(root);
if (right != null) {
//如果右子節點不爲null,則繼續遞歸遍歷該右子節點
inorderTraversal(right);
}
}
/**
* 獲取左子節點
*
* @param parent 父節點引用
* @return 左子節點或者null--表示沒有左子節點
*/
private RedBlackNode<K, V> getLeft(RedBlackNode<K, V> parent) {
return parent == null ? null : parent.left;
}
/**
* 獲取右子節點
*
* @param parent 父節點引用
* @return 右子節點或者null--表示沒有右子節點
*/
private RedBlackNode<K, V> getRight(RedBlackNode<K, V> parent) {
return parent == null ? null : parent.right;
}
}
3.2 測試代碼
public class RedBlackTreeTest<E> {
@Test
public void test3() {
RedBlackTree<Integer, Integer> integerIntegerRedBlackTree = new RedBlackTree<>();
integerIntegerRedBlackTree.put(3, 47);
integerIntegerRedBlackTree.put(2, 16);
integerIntegerRedBlackTree.put(1, 73);
integerIntegerRedBlackTree.put(4, 1);
integerIntegerRedBlackTree.put(5, 24);
integerIntegerRedBlackTree.put(6, 59);
integerIntegerRedBlackTree.put(7, 59);
integerIntegerRedBlackTree.put(16, 59);
integerIntegerRedBlackTree.put(15, 59);
integerIntegerRedBlackTree.put(14, 11);
integerIntegerRedBlackTree.put(13, 59);
integerIntegerRedBlackTree.put(12, 59);
integerIntegerRedBlackTree.put(11, 12);
integerIntegerRedBlackTree.put(10, 59);
integerIntegerRedBlackTree.put(8, 59);
integerIntegerRedBlackTree.put(9, 59);
/*中序遍歷輸出,即根據排序順序從小到大輸出*/
System.out.println(integerIntegerRedBlackTree.toInorderTraversalString());
System.out.println(integerIntegerRedBlackTree.remove(2));
System.out.println(integerIntegerRedBlackTree.remove(4));
System.out.println(integerIntegerRedBlackTree.remove(5));
System.out.println(integerIntegerRedBlackTree.remove(12));
System.out.println(integerIntegerRedBlackTree.remove(7));
System.out.println(integerIntegerRedBlackTree.remove(13));
System.out.println(integerIntegerRedBlackTree.remove(8));
System.out.println(integerIntegerRedBlackTree.remove(16));
System.out.println(integerIntegerRedBlackTree.remove(11));
System.out.println(integerIntegerRedBlackTree.remove(10));
System.out.println(integerIntegerRedBlackTree.remove(14));
System.out.println(integerIntegerRedBlackTree.remove(3));
System.out.println(integerIntegerRedBlackTree.remove(9));
System.out.println(integerIntegerRedBlackTree.remove(6));
System.out.println(integerIntegerRedBlackTree.remove(15));
System.out.println(integerIntegerRedBlackTree.remove(1));
System.out.println(integerIntegerRedBlackTree.toInorderTraversalString());
}
@Test
public void test1() {
RedBlackTree<Integer, Integer> integerIntegerRedBlackTree = new RedBlackTree<>();
integerIntegerRedBlackTree.put(47, 47);
integerIntegerRedBlackTree.put(16, 16);
integerIntegerRedBlackTree.put(73, 73);
integerIntegerRedBlackTree.put(1, 1);
integerIntegerRedBlackTree.put(24, 24);
integerIntegerRedBlackTree.put(59, 59);
integerIntegerRedBlackTree.put(20, 20);
integerIntegerRedBlackTree.put(35, 35);
integerIntegerRedBlackTree.put(62, 62);
integerIntegerRedBlackTree.put(77, 77);
integerIntegerRedBlackTree.put(77, 101);
/*中序遍歷輸出,即根據排序順序從小到大輸出*/
System.out.println(integerIntegerRedBlackTree.toInorderTraversalString());
System.out.println(integerIntegerRedBlackTree.remove(47));
System.out.println(integerIntegerRedBlackTree.remove(62));
System.out.println(integerIntegerRedBlackTree.remove(77));
System.out.println(integerIntegerRedBlackTree.remove(16));
System.out.println(integerIntegerRedBlackTree.remove(20));
System.out.println(integerIntegerRedBlackTree.remove(59));
System.out.println(integerIntegerRedBlackTree.remove(1));
System.out.println(integerIntegerRedBlackTree.remove(24));
System.out.println(integerIntegerRedBlackTree.remove(35));
System.out.println(integerIntegerRedBlackTree.remove(73));
System.out.println(integerIntegerRedBlackTree.toInorderTraversalString());
}
@Test
public void test2() {
RedBlackTree<Integer, Integer> integerIntegerRedBlackTree = new RedBlackTree<>();
integerIntegerRedBlackTree.put(1, 47);
integerIntegerRedBlackTree.put(2, 16);
integerIntegerRedBlackTree.put(4, 73);
integerIntegerRedBlackTree.put(9, 1);
integerIntegerRedBlackTree.put(5, 24);
integerIntegerRedBlackTree.put(7, 59);
integerIntegerRedBlackTree.put(6, 59);
/*中序遍歷輸出,即根據排序順序從小到大輸出*/
System.out.println(integerIntegerRedBlackTree.toInorderTraversalString());
System.out.println(integerIntegerRedBlackTree.remove(2));
System.out.println(integerIntegerRedBlackTree.remove(4));
System.out.println(integerIntegerRedBlackTree.remove(1));
System.out.println(integerIntegerRedBlackTree.remove(6));
System.out.println(integerIntegerRedBlackTree.remove(7));
System.out.println(integerIntegerRedBlackTree.remove(5));
System.out.println(integerIntegerRedBlackTree.remove(9));
System.out.println(integerIntegerRedBlackTree.toInorderTraversalString());
}
@Test
public void test4() {
RedBlackTree<Integer, Integer> integerIntegerRedBlackTree = new RedBlackTree<>();
integerIntegerRedBlackTree.put(1, 47);
integerIntegerRedBlackTree.put(2, 16);
integerIntegerRedBlackTree.put(4, 73);
integerIntegerRedBlackTree.put(9, 1);
integerIntegerRedBlackTree.put(5, 24);
integerIntegerRedBlackTree.put(7, 59);
integerIntegerRedBlackTree.put(6, 59);
/*中序遍歷輸出,即根據排序順序從小到大輸出*/
System.out.println(integerIntegerRedBlackTree.toInorderTraversalString());
System.out.println("maxkey=====>" + integerIntegerRedBlackTree.maxKey());
System.out.println(integerIntegerRedBlackTree.remove(9));
System.out.println("maxkey=====>" + integerIntegerRedBlackTree.maxKey());
System.out.println("minkey=====>" + integerIntegerRedBlackTree.minKey());
System.out.println(integerIntegerRedBlackTree.remove(1));
System.out.println(integerIntegerRedBlackTree.remove(1));
System.out.println("minkey=====>" + integerIntegerRedBlackTree.minKey());
System.out.println("get 2=====>" + integerIntegerRedBlackTree.get(2));
System.out.println(integerIntegerRedBlackTree.remove(2));
System.out.println("get 2=====>" + integerIntegerRedBlackTree.get(2));
}
}
3.3 執行流程
3.3.1 插入
針對上面的測試案例詳細說明執行流程!我對需要對下面一批數據構建紅黑樹!
(3, 47);
(2, 16);
(1, 73);
(4, 1);
(5, 24);
(6, 59);
(7, 59);
(16, 59);
(15, 59);
(14, 59);
(13, 59);
(12, 59);
(11, 59);
(10, 59);
(8, 59);
(9, 59);
首先建立紅黑樹類對象,注意:在我們的實現原理的講解過程中,null節點使用一個特殊哨兵節點NIL來代替,那樣方便理解,而在具體的實現代碼中,並沒有NIL哨兵節點,對於null節點,我們知道他作爲黑色節點並且注意不要造成空指針異常即可,還能節省空間。
使用無參構造器,新建紅黑樹類之後,此時該類的結構爲:
可以看到,自定義比較器爲null,說明會使用自然比較器比較;
節點數量size爲0;
根節點引用指向null,說明還沒有節點。
3.3.1.1 插入3
首先插入(3, 47),默認創建紅色節點,插入之後,判斷該節點明顯是根節點,那麼該節點塗黑:
3.3.1.2 插入2
然後插入(2, 16),默認創建紅色節點,插入之後符合紅黑樹的規定義,不用調整平衡;
3.3.1.3 插入1
然後插入(1, 73),默認創建紅色節點,插入之後違反了紅黑樹的定義4,即不能有相連的紅色節點。
這裏明顯是屬於“父紅叔黑(null算黑色)-LL”的情況,按照LL的要求來進行調整:將G右旋,然後P塗黑,G塗紅。
3.3.1.4 插入4
然後插入(4, 1),默認創建紅色節點,插入之後違反了紅黑樹的定義4,即不能有相連的紅色節點。
這裏明顯是屬於“父紅叔紅”的情況,按照要求來進行調整:將父/叔 (P/U) 節點塗黑,祖父節點(G)塗紅;而後以祖父節點(G)作爲新的平衡節點N,向上遞歸的執行平衡操作,直到不再發生兩個相連的紅色節點或者達到根(它將被重新塗成黑色)爲止。
3.3.1.5 插入5
然後插入(5, 24),默認創建紅色節點,插入之後違反了紅黑樹的定義4,即不能有相連的紅色節點。
這裏明顯是屬於“父紅叔黑(null算黑色)-RR”的情況,按照要求來進行調整:將G左旋,然後P塗黑,G塗紅。
3.3.1.6 插入6
然後插入(6, 59),默認創建紅色節點,插入之後違反了紅黑樹的定義4,即不能有相連的紅色節點。
這裏明顯是屬於“父紅叔紅”的情況,按照要求來進行調整:將父/叔 (P/U) 節點塗黑,祖父節點(G)塗紅;而後以祖父節點(G)作爲新的平衡節點N,向上遞歸的執行平衡操作,直到不再發生兩個相連的紅色節點或者達到根(它將被重新塗成黑色)爲止。
3.3.1.7 插入7
然後插入(7, 59),默認創建紅色節點,插入之後違反了紅黑樹的定義4,即不能有相連的紅色節點。
這裏明顯是屬於“父紅叔黑(null算黑色)-RR”的情況,按照要求來進行調整:將G左旋,然後P塗黑,G塗紅。
3.3.1.8 插入16
然後插入(16, 59),默認創建紅色節點,插入之後違反了紅黑樹的定義4,即不能有相連的紅色節點。
這裏明顯是屬於“父紅叔紅”的情況,按照要求來進行調整:將父/叔 (P/U) 節點塗黑,祖父節點(G)塗紅;而後以祖父節點(G)作爲新的平衡節點N,向上遞歸的執行平衡操作,直到不再發生兩個相連的紅色節點或者達到根(它將被重新塗成黑色)爲止。
上圖可以看到,一次調整之後,成爲了“父紅叔黑-RR”的情況,按照要求來進行調整:將G左旋,然後P塗黑,G塗紅。
3.3.1.9 插入15
然後插入(15, 59),默認創建紅色節點,插入之後違反了紅黑樹的定義4,即不能有相連的紅色節點。
這裏明顯是屬於“父紅叔黑(null算黑色)-RL”的情況,按照要求來進行調整:先將P右旋,然後將G左旋;然後N塗黑,G塗紅。
P右旋:
G左旋:
N塗黑,G塗紅:
3.3.1.10 插入14
然後插入(14, 11),默認創建紅色節點,插入之後違反了紅黑樹的定義4,即不能有相連的紅色節點。
這裏明顯是屬於“父紅叔紅”的情況,按照要求來進行調整:將父/叔 (P/U) 節點塗黑,祖父節點(G)塗紅;而後以祖父節點(G)作爲新的平衡節點N,向上遞歸的執行平衡操作,直到不再發生兩個相連的紅色節點或者達到根(它將被重新塗成黑色)爲止。
遞歸之後,這裏明顯又是屬於“父紅叔紅”的情況,按照要求來進行調整:將父/叔 (P/U) 節點塗黑,祖父節點(G)塗紅;而後以祖父節點(G)作爲新的平衡節點N,向上遞歸的執行平衡操作,直到不再發生兩個相連的紅色節點或者達到根(它將被重新塗成黑色)爲止。
之後發現N屬於節點,此時只需要將N塗黑即可:
3.3.1.11 插入13
然後插入(13, 59),默認創建紅色節點,插入之後違反了紅黑樹的定義4,即不能有相連的紅色節點。
這裏明顯是屬於“父紅叔黑(null算黑色)-RL”的情況,按照要求來進行調整:先將P右旋,然後將G左旋;然後N塗黑,G塗紅。
P右旋:
G左旋:
N塗黑,G塗紅:
3.3.1.12 插入12
然後插入(12, 59),默認創建紅色節點,插入之後違反了紅黑樹的定義4,即不能有相連的紅色節點。
這裏明顯是屬於“父紅叔紅”的情況,按照要求來進行調整:將父/叔 (P/U) 節點塗黑,祖父節點(G)塗紅;而後以祖父節點(G)作爲新的平衡節點N,向上遞歸的執行平衡操作,直到不再發生兩個相連的紅色節點或者達到根(它將被重新塗成黑色)爲止。
遞歸之後,這裏明顯屬於“父紅叔黑-RL”的情況,按照要求來進行調整:先將P右旋,然後將G左旋;然後N塗黑,G塗紅。
P右旋:
G左旋:
N塗黑,G塗紅:
3.3.1.13 插入11
然後插入(11, 12),默認創建紅色節點,插入之後違反了紅黑樹的定義4,即不能有相連的紅色節點。
這裏明顯是屬於“父紅叔黑(null算黑色)-RL”的情況,按照要求來進行調整:先將P右旋,然後將G左旋;然後N塗黑,G塗紅。
P右旋:
G左旋:
N塗黑,G塗紅:
3.3.1.14 插入10
然後插入(10, 59),默認創建紅色節點,插入之後違反了紅黑樹的定義4,即不能有相連的紅色節點。
這裏明顯是屬於“父紅叔紅”的情況,按照要求來進行調整:將父/叔 (P/U) 節點塗黑,祖父節點(G)塗紅;而後以祖父節點(G)作爲新的平衡節點N,向上遞歸的執行平衡操作,直到不再發生兩個相連的紅色節點或者達到根(它將被重新塗成黑色)爲止。
第一次遞歸時,還是屬於“父紅叔紅”,按照要求來進行調整:將父/叔 (P/U) 節點塗黑,祖父節點(G)塗紅;而後以祖父節點(G)作爲新的平衡節點N,向上遞歸的執行平衡操作,直到不再發生兩個相連的紅色節點或者達到根(它將被重新塗成黑色)爲止。
13作爲紅色“插入”之後,這次就很幸運了,父節點爲黑色,不需要調整了,平衡完成。
3.3.1.15 插入8
然後插入(8, 59),默認創建紅色節點,插入之後違反了紅黑樹的定義4,即不能有相連的紅色節點。
這裏明顯是屬於“父紅叔黑(null算黑色)-RL”的情況,按照要求來進行調整:先將P右旋,然後將G左旋;然後N塗黑,G塗紅。
P右旋:
G左旋:
N塗黑,G塗紅:
3.3.1.16 插入9
然後插入(9, 59),默認創建紅色節點,插入之後違反了紅黑樹的定義4,即不能有相連的紅色節點。
這裏明顯是屬於“父紅叔紅”的情況,按照要求來進行調整:將父/叔 (P/U) 節點塗黑,祖父節點(G)塗紅;而後以祖父節點(G)作爲新的平衡節點N,向上遞歸的執行平衡操作,直到不再發生兩個相連的紅色節點或者達到根(它將被重新塗成黑色)爲止。
第一次遞歸時,是屬於 “父紅叔黑-RL”的情況,按照要求來進行調整:先將P右旋,然後將G左旋;然後N塗黑,G塗紅。
P右旋:
G左旋:
N塗黑,G塗紅:
到此,插入完畢!
3.3.2 刪除
刪除操作在代碼中共分爲三步,首先searchRemoveNode尋找真正應該被刪除的節點,然後removeNode對該節點進行刪除,並且調整部分情況下的節點的平衡,對於複雜的節點的平衡,比如涉及到遞歸操作的,我們在最後單獨的rebalanceAfterRemove方法中處理!
3.3.2.1 刪除2
首先searchRemoveNode根據二叉排序樹的刪除原理查找需要真正被刪除的節點,由於2具有左右子樹,因此尋找右子樹的最小值,很快找到了3,它是右子樹的最小節點,這就是真正需要被刪除的節點,然後對2節點和3節點交換key和vaue的值,返回該需要被刪除的節點!
然後調用removeNode對該節點進行刪除,同時對部分情況進行調整平衡!這裏被刪除的節點是黑色節點,它的子節點爲null(null節點看成黑色節點),簡稱“刪黑子黑”;
然後繼續劃分,被刪除節點的兄弟節點1爲黑色,並且屬於“兄子全黑(null算黑色)-父黑”的情況!
對於這種複雜情況,在removeNode中沒有處理,刪除該節點,並且改變引用,然後然後返回原來被刪除的節點,然後繼續進行復雜的平衡操作。
最後調用rebalanceAfterRemove方法進行平衡,使用“兄子全黑-父黑”的調整方法:將兄弟節點B塗紅,將父節點P設爲新的C節點,將U設爲新B節點,將G設爲新P節點,回到刪黑子黑的情況,即向上遞歸進行處理,直到C成爲根節點或者達到平衡。
第一次遞歸時,明顯是“刪黑子黑-刪兄紅-右兄弟”的情況,使用對應的刪除方法:以父節點P爲基點左旋,然後B塗黑、P塗紅,最後將BL看成新的兄弟節點newB,轉換爲“刪兄黑”的情況!
以父節點P爲基點左旋:
B塗黑、P塗紅,最後將BL看成新的兄弟節點newB,轉換爲“刪兄黑”的情況!
轉換之後,根據上圖可以知道,具體是屬於“刪兄黑-兄子非全黑-兄右,右子紅”的情況,使用對應的刪除方法:以父節點P爲基點左旋,然後交換P和B的顏色,BR塗黑,平衡完畢!
以父節點P爲基點左旋:
交換P和B的顏色,BR塗黑,平衡完畢!
3.3.2.2 刪除4
首先searchRemoveNode根據二叉排序樹的刪除原理查找需要真正被刪除的節點,由於4具有左右子樹,因此尋找右子樹的最小值,很快找到了5,它是右子樹的最小節點,這就是真正需要被刪除的節點,然後對4節點和5節點交換key和vaue的值,返回該需要被刪除的節點!
然後調用removeNode對該節點進行刪除,同時對部分情況進行調整平衡!這裏被刪除的節點是黑色節點,它的子節點爲黑色,簡稱“刪黑子黑”;
然後繼續劃分,被刪除節點4的兄弟節點7黑色,並且屬於“刪兄黑-兄子全黑-父紅”的情況!
對於這種複雜情況,在removeNode中沒有處理,刪除該節點,並且改變引用,然後然後返回原來被刪除的節點,然後繼續進行復雜的平衡操作。
對於“兄子全黑-父紅”的情況,使用對應的調整方法:將兄弟節點B塗紅,父節點P塗黑即可。此時原本刪除黑色節點的路徑補充了黑色節點,即可達到平衡。
此時因爲刪除4而損失的黑色節點通過父節點6變黑補充回來了,同時兄弟節點路徑中的黑色節的數量點因爲7節點的變紅也沒變。
3.3.2.3 刪除5
首先searchRemoveNode根據二叉排序樹的刪除原理查找需要真正被刪除的節點,由於5具有左右子樹,因此尋找右子樹的最小值,很快找到了6,它是右子樹的最小節點,這就是真正需要被刪除的節點,然後對5節點和6節點交換key和vaue的值,返回該需要被刪除的節點!
然後調用removeNode對該節點進行刪除,同時對部分情況進行調整平衡!這裏被刪除的節點是黑色節點,它的右子節點爲紅色,簡稱**“刪黑子紅**”;
對於這種簡單的情況,在removeNode方法中已經處理了,使用“刪黑子紅”的調整方法:刪除5之後,把後繼C的顏色塗黑,即將失去的黑色補充回來,即可達到平衡!
3.3.2.4 刪除12
首先searchRemoveNode根據二叉排序樹的刪除原理查找需要真正被刪除的節點,由於12沒有左右子樹,因此12就是真正需要被刪除的節點,返回該需要被刪除的節點!
然後調用removeNode對該節點進行刪除,同時對部分情況進行調整平衡!這裏被刪除的節點是黑色節點,它的子節點爲null(null節點看成黑色節點),簡稱“刪黑子黑”。
然後繼續劃分,被刪除節點12的兄弟節點10爲黑色,並且屬於“兄子非全黑-兄左,左子紅”的情況!
對於這種複雜情況,在removeNode中沒有處理,刪除該節點,並且改變引用,然後然後返回原來被刪除的節點,然後繼續進行復雜的平衡操作。
使用“兄子非全黑-兄左,左子紅”的調整方法:以父節點P爲基點右旋,然後交換P和B的顏色,BL塗黑,平衡完畢!
以父節點P爲基點右旋:
交換P和B的顏色,BL塗黑,平衡完畢!
3.3.2.5 刪除7
首先searchRemoveNode根據二叉排序樹的刪除原理查找需要真正被刪除的節點,由於7沒有左右子樹,因此7就是真正需要被刪除的節點,返回該需要被刪除的節點!
然後調用removeNode對該節點進行刪除,同時對部分情況進行調整平衡!這裏被刪除的節點是黑色節點,它的子節點爲null(null節點看成黑色節點),簡稱“刪黑子黑”;
然後繼續劃分,被刪除節點12的兄弟節點10爲黑色,並且屬於“兄子非全黑-兄左,左子紅”的情況!
對於這種複雜情況,在removeNode中沒有處理,刪除該節點,並且改變引用,然後然後返回原來被刪除的節點,然後繼續進行復雜的平衡操作。
使用“兄子非全黑-兄左,左子紅”的調整方法:以父節點P爲基點右旋,然後交換P和B的顏色,BL塗黑,平衡完畢!
以父節點P爲基點右旋:
交換P和B的顏色,BL塗黑,平衡完畢!
3.3.2.6 刪除13
首先searchRemoveNode根據二叉排序樹的刪除原理查找需要真正被刪除的節點,由於13具有左右子樹,因此尋找右子樹的最小值,很快找到了14,它是右子樹的最小節點,這就是真正需要被刪除的節點,然後對13節點和14節點交換key和vaue的值,返回該需要被刪除的節點!
然後調用removeNode對該節點進行刪除,同時對部分情況進行調整平衡!這裏被刪除的節點是黑色節點,它的子節點爲null(null節點看成黑色節點),簡稱“刪黑子黑”;
然後繼續劃分,被刪除節點的兄弟節點16爲黑色,並且屬於“兄子全黑(null算黑色)-父黑”的情況!
對於這種複雜情況,在removeNode中沒有處理,刪除該節點,並且改變引用,然後然後返回原來被刪除的節點,然後繼續進行復雜的平衡操作。
使用“兄子全黑-父黑”的調整方法:將兄弟節點B塗紅,將父節點P設爲新的C節點,將U設爲新B節點,將G設爲新P節點,回到刪黑子黑的情況,即向上遞歸進行處理,直到C成爲根節點或者達到平衡。
繼續使用“刪兄紅-左兄弟”的調整方法:以父節點P爲基點右旋,然後B塗黑、P塗紅,最後將BR看成新的兄弟節點newB,即變成“刪兄黑”的情況
以父節點P爲基點右旋:
B塗黑、P塗紅,最後將BR看成新的兄弟節點newB,即變成“刪兄黑”的情況。
從圖上可以看出來,這是屬於“刪兄黑-兄子全黑-父紅”的情況,使用對應的調整方法:將兄弟節點B塗紅,父節點P塗黑即可達到平衡。
3.3.2.7 刪除8
首先searchRemoveNode根據二叉排序樹的刪除原理查找需要真正被刪除的節點,由於8具有左右子樹,因此尋找右子樹的最小值,很快找到了9,它是右子樹的最小節點,這就是真正需要被刪除的節點,然後對8節點和9節點交換key和vaue的值,返回該需要被刪除的節點!
然後調用removeNode對該節點進行刪除,同時對部分情況進行調整平衡!這裏被刪除的節點是黑色節點,它的子節點爲null(null節點看成黑色節點),簡稱“刪黑子黑”;
然後繼續劃分,被刪除節點的兄弟節點11爲黑色,並且屬於“兄子全黑(null算黑色)-父紅”的情況!
對於這種複雜情況,在removeNode中沒有處理,刪除該節點,並且改變引用,然後然後返回原來被刪除的節點,然後繼續進行復雜的平衡操作。
使用“兄子全黑(null算黑色)-父紅”的調整方法:將兄弟節點B塗紅,父節點P塗黑即可達到平衡!
3.3.2.8 刪除16
首先searchRemoveNode根據二叉排序樹的刪除原理查找需要真正被刪除的節點,由於16沒有左右子樹,因此16就是真正需要被刪除的節點,返回該需要被刪除的節點!
然後調用removeNode對該節點進行刪除,同時對部分情況進行調整平衡!這裏被刪除的節點是紅色節點,這是最簡單的刪除了,簡稱“刪紅”;刪除N後不會影響紅黑樹的平衡性,與其他節點的顏色無關,因此不需要做任何調整。
對於這種簡單情況,在removeNode中已經處理好了。
3.3.2.9 刪除11
首先searchRemoveNode根據二叉排序樹的刪除原理查找需要真正被刪除的節點,由於11沒有左右子樹,因此11就是真正需要被刪除的節點,返回該需要被刪除的節點!
然後調用removeNode對該節點進行刪除,同時對部分情況進行調整平衡!這裏被刪除的節點是紅色節點,這是最簡單的刪除了,簡稱“刪紅”;刪除N後不會影響紅黑樹的平衡性,與其他節點的顏色無關,因此不需要做任何調整。
對於這種簡單情況,在removeNode中已經處理好了。
3.3.2.10 刪除10
首先searchRemoveNode根據二叉排序樹的刪除原理查找需要真正被刪除的節點,由於10沒有左右子樹,因此10就是真正需要被刪除的節點,返回該需要被刪除的節點!
然後調用removeNode對該節點進行刪除,同時對部分情況進行調整平衡!這裏被刪除的節點是黑色節點,它的子節點爲黑色,簡稱“刪黑子黑”;
然後繼續劃分,被刪除節點10的兄弟節點15黑色,並且屬於“刪兄黑-兄子全黑-父黑”的情況!
對於這種複雜情況,在removeNode中沒有處理,刪除該節點,並且改變引用,然後然後返回原來被刪除的節點,然後繼續進行復雜的平衡操作。
使用“刪兄黑-兄子全黑-父黑”的情況的調整方法:將兄弟節點B塗紅,將父節點P設爲新的C節點,將U設爲新B節點,將G設爲新P節點,回到刪黑子黑的情況,即向上遞歸進行處理,直到C成爲根節點或者達到平衡。
繼續,使用“刪兄黑-兄子全黑-父黑”的情況的調整方法:將兄弟節點B塗紅,將父節點P設爲新的C節點,將U設爲新B節點,將G設爲新P節點,回到刪黑子黑的情況,即向上遞歸進行處理,直到C成爲根節點或者達到平衡。
3.3.2.11 刪除14
首先searchRemoveNode根據二叉排序樹的刪除原理查找需要真正被刪除的節點,由於14沒有左右子樹,因此14就是真正需要被刪除的節點,返回該需要被刪除的節點!
然後調用removeNode對該節點進行刪除,同時對部分情況進行調整平衡!這裏被刪除的節點是黑色節點,它的子節點爲紅色,簡稱“刪黑子紅”;
對於這種簡單的情況,在removeNode中已經處理了:把子節點C的顏色塗黑,那麼就能恢復N之前所在路徑的黑色節點的總數,與其他節點的顏色無關,平衡調整完成。
3.3.2.12 刪除3
首先searchRemoveNode根據二叉排序樹的刪除原理查找需要真正被刪除的節點,由於3具有左右子樹,因此尋找右子樹的最小值,很快找到了6,它是右子樹的最小節點,這就是真正需要被刪除的節點,然後對3節點和6節點交換key和vaue的值,返回該需要被刪除的節點!
然後調用removeNode對該節點進行刪除,同時對部分情況進行調整平衡!這裏被刪除的節點是黑色節點,它的子節點爲黑色,簡稱“刪黑子黑”;
然後繼續劃分,被刪除節點3的兄弟節點1爲黑色,並且屬於“刪兄黑-兄子全黑-父紅”的情況!
對於這種複雜情況,在removeNode中沒有處理,刪除該節點,並且改變引用,然後然後返回原來被刪除的節點,然後繼續進行復雜的平衡操作。
使用“刪兄黑-兄子全黑-父紅”相應的調整方法:將兄弟節點B塗紅,父節點P塗黑即可達到平衡。
3.3.2.13 刪除9
首先searchRemoveNode根據二叉排序樹的刪除原理查找需要真正被刪除的節點,由於9具有左右子樹,因此尋找右子樹的最小值,很快找到了15,它是右子樹的最小節點,這就是真正需要被刪除的節點,然後對9節點和15節點交換key和vaue的值,返回該需要被刪除的節點!
然後調用removeNode對該節點進行刪除,同時對部分情況進行調整平衡!這裏被刪除的節點是黑色節點,它的子節點爲黑色(null),簡稱“刪黑子黑”;
然後繼續劃分,被刪除節點9的兄弟節點6黑色,並且屬於“刪兄黑-兄子非全黑-兄左,左子紅”的情況!
對於這種複雜情況,在removeNode中沒有處理,刪除該節點,並且改變引用,然後然後返回原來被刪除的節點,然後繼續進行復雜的平衡操作。
使用“刪兄黑-兄子非全黑-兄左,左子紅”的調整方法:以父節點P爲基點右旋,然後交換P和B的顏色,BL塗黑,平衡完畢!
3.3.2.14 刪除6
首先searchRemoveNode根據二叉排序樹的刪除原理查找需要真正被刪除的節點,由於6具有左右子樹,因此尋找右子樹的最小值,很快找到了15,它是右子樹的最小節點,這就是真正需要被刪除的節點,然後對6節點和15節點交換key和vaue的值,返回該需要被刪除的節點!
然後調用removeNode對該節點進行刪除,同時對部分情況進行調整平衡!這裏被刪除的節點是黑色節點,它的子節點爲黑色(null),簡稱“刪黑子黑”;
然後繼續劃分,被刪除節點6的兄弟節點1黑色,並且屬於“刪兄黑-兄子全黑-父黑”的情況!
對於這種複雜情況,在removeNode中沒有處理,刪除該節點,並且改變引用,然後然後返回原來被刪除的節點,然後繼續進行復雜的平衡操作。
使用“刪兄黑-兄子全黑-父黑”的調整方法:將兄弟節點B塗紅,將父節點P設爲新的C節點,將U設爲新B節點,將G設爲新P節點,回到刪黑子黑的情況,即向上遞歸進行處理,直到C成爲根節點或者達到平衡。
3.3.2.15 刪除15
首先searchRemoveNode根據二叉排序樹的刪除原理查找需要真正被刪除的節點,由於15沒有左右子樹,因此15就是真正需要被刪除的節點,返回該需要被刪除的節點!
然後調用removeNode對該節點進行刪除,同時對部分情況進行調整平衡!這裏被刪除的節點是黑色節點,它的子節點爲紅色,簡稱“刪黑子紅”;
對於這種簡單的情況,在removeNode中已經處理了:把子節點C的顏色塗黑,那麼就能恢復N之前所在路徑的黑色節點的總數,與其他節點的顏色無關,平衡調整完成。
3.3.2.16 刪除1
很明顯,這裏屬於“刪黑子黑-刪根”的情況,直接刪除該節點,任何無需平衡操作。此時所有元素已經全部刪除完畢!紅黑樹對象又回到了初始的狀態:
4 總結
紅黑樹具有非常複雜的原理、超高的實現難度、以及良好的使用性能。如果理解了紅黑樹,那麼對其他高級的數據結構的理解比如伸展樹、Treap、跳躍表、k-d 樹 等應該是比較簡單的事。
從上面的實現可以看出來,紅黑樹的實現結構不唯一。比如刪除的時候,上面的實現是查找右子樹的最小節點,也可以是查找左子樹的最大節點,這樣紅黑樹的結構會有所改變=。
最後提供一個在線演示各種數據結構的網站:Data Structure Visualizations,做的非常不錯。
參考
《算法》
《數據結構與算法分析 Java語言描述 原書第3版 》
百度百科
如果有什麼不懂或者需要交流,可以留言。另外希望點贊、收藏、關注,我將不間斷更新各種Java學習博客!