Java集合原始碼分析之基礎(六):紅黑樹(RB Tree)

大大紙飛機發表於2018-08-13

紅黑樹和AVL樹的思想是類似的,都是在插入過程中對二叉排序樹進行調整,從而提升效能,它的增刪改查均可以在**O(lg n)**內完成。

本文會從定義到實現一棵紅黑樹展開,還會簡單介紹其與AVL樹的異同。

定義

紅黑樹是一棵二叉排序樹。且滿足以下特點:

  1. 每個節點或者是黑色,或者是紅色。
  2. 根節點是黑色。
  3. 每個葉子節點(NIL)是黑色。 [注意:這裡葉子節點,是指為空(NIL或NULL)的葉子節點!]
  4. 如果一個節點是紅色的,則它的兩個兒子都是黑色的。
  5. 從一個節點到該節點的子孫節點的所有路徑上包含相同數目的黑節點。

下圖就是一棵簡單的紅黑樹示例:

紅黑樹示例

示例中每個結點最後都是一個NIL結點,它是黑色的,不過我們畫圖時通常會省略它。所以下文以及後續文章中繪製時都會省略NIL結點,大家記得還有它就可以。

實現原理

紅黑樹的插入與刪除和AVL樹類似,也是每插入一個結點,都檢查是否破壞了樹的結構,然後進行調整。紅黑樹每個結點插入時預設都為紅色,這樣做可以降低黑高,也可以減少調整的次數。

插入元素

紅黑樹的概念理解起來較為複雜,我們以一個簡單的示例,看看如何構造一棵紅黑樹。

現有陣列int[] a = {1, 10, 9, 2, 3, 8, 7, 4, 5, 6};我們要將其變為一棵紅黑樹。

首先插入1,此時樹是空的,1就是根結點,根結點是黑色的:

插入1

然後插入元素10,此時依然符合規則,結果如下:

插入10

當插入元素9時,這時是需要調整的第一種情況,結果如下:

插入9

紅黑樹規則4中強調不能有兩個相鄰的紅色結點,所以此時我們需要對其進行調整。調整的原則有多個相關因素,這裡的情況是,父結點10是其祖父結點1(父結點的父結點)的右孩子,當前結點9是其父結點10的左孩子,且沒有叔叔結點(父結點的兄弟結點),此時需要進行兩次旋轉,第一次,以父結點10右旋:

右旋

然後將父結點**(此時是9)染為黑色,祖父結點1**染為紅色,如下所示:

染色

然後以祖父結點1左旋:

左旋

下一步,插入元素2,結果如下:

插入2

此時情況與上一步類似,區別在於父結點1是祖父結點9的左孩子,當前結點2是父結點的右孩子,且叔叔結點10是紅色的。這時需要先將叔叔結點10染為黑色,再進行下一步操作,具體做法是將父結點1和叔叔結點10染為黑色,祖父結點9染為紅色,如下所示:

染色

由於結點9是根節點,必須為黑色,將它染為黑色即可:

染色

下一步,插入元素3,如下所示:

插入3

這和我們之前插入元素10的情況一模一樣,需要將父結點2染為黑色,祖父結點1染為紅色,如下所示:

染色

然後左旋:

左旋

下一步,插入元素8,結果如下:

插入8

此時和插入元素2有些類似,區別在於父結點3是右孩子,當前結點8也是右孩子,這時也需要先將叔叔結點1染為黑色,具體操作是先將13染為黑色,再將祖父結點2染為紅色,如下所示:

染色

此時樹已經平衡了,不需要再進行其他操作了,現在插入元素7,如下所示:

插入7

這時和之前插入元素9時一模一樣了,先將78右旋,如下所示:

右旋

然後將7染為黑色,3染為紅色,再進行左旋,結果如下:

左旋

下一步要插入的元素是4,結果如下:

插入4

這裡和插入元素2是類似的,先將38染為黑色,7染為紅色,如下所示:

染色

但此時27相鄰且顏色均為紅色,我們需要對它們繼續進行調整。這時情況變為了父結點2為紅色,叔叔結點10為黑色,且2為左孩子,7為右孩子,這時需要以2左旋。這時左旋與之前不同的地方在於結點7旋轉完成後將有三個孩子,結果類似於下圖:

錯誤示意圖

這種情況處理起來也很簡單,只需要把7原來的左孩子3,變成2的右孩子即可,結果如下:

調整

然後再把2的父結點7染為黑色,祖父結點9染為紅色。結果如下所示:

染色

此時又需要右旋了,我們要以9右旋,右旋完成後7又有三個孩子,這種情況和上述是對稱的,我們把7原有的右孩子8,變成9的左孩子即可,如下所示:

右旋

下一個要插入的元素是5,插入後如下所示:

插入5

有了上述一些操作,處理5變得十分簡單,將3染為紅色,4染為黑色,然後左旋,結果如下所示:

左旋

最後插入元素6,如下所示:

插入6

又是叔叔結點3為紅色的情況,這種情況我們處理過多次了,首先將35染為黑色,4染為紅色,結果如下:

染色

此時問題向上傳遞到了元素4,我們看2479的顏色和位置關係,這種情況我們也處理過,先將29染為黑色,7染為紅色,結果如下:

染色

最後7是根結點,染為黑色即可,最終結果如下所示:

最終結果

可以看到,在插入元素時,叔叔結點是主要影響因素,待插入結點與父結點的關係決定了是否需要多次旋轉。可以總結為以下幾種情況:

  • 如果父結點是黑色,插入即可,無需調整。

  • 如果叔叔結點是紅色,就把父結點和叔叔結點都轉為黑色,祖父結點轉為紅色,將不平衡向上傳遞。

  • 如果叔叔結點是黑色或者沒有叔叔結點,就看父結點和待插入結點的關係。如果待插入結點和父結點的關係,與父結點與祖父結點的關係一致,比如待插入結點是父結點的左孩子,父結點也是祖父結點的左孩子,就無需多次旋轉。否則就先通過相應的旋轉將其關係變為一致。

刪除元素

要從一棵紅黑樹中刪除一個元素,主要分為三種情況。

情況1:待刪除元素沒有孩子

沒有孩子指的是沒有值不為NIL的孩子。這種情況下,如果刪除的元素是紅色的,可以直接刪除,如果刪除的元素是黑色的,就需要進行調整了。

例如我們從下圖中刪除元素1:

紅黑樹

刪除元素1後,2的左孩子為NIL,這條支路上的黑色結點數就比其他支路少了,所以需要進行調整。

這時,我們的關注點從叔叔結點轉到兄弟結點,也就是結點4,此時4是紅色的,就把它染為黑色,把父結點2染為紅色,如下所示:

染色

然後以2左旋,結果如下:

左旋

此時兄弟結點為3,且它沒有紅色的孩子,這時只需要把它染為紅色,父結點2染為黑色即可。結果如下所示:

調整完畢

情況2:待刪除元素有一個孩子

這應該是刪除操作中最簡單的一種情況了,根據紅黑樹的定義,我們可以推測,如果一個元素僅有一個孩子,那麼這個元素一定是黑色的,而且其孩子是紅色的。

假設我們有一個紅色節點,它是樹中的某一個節點,且僅有一個孩子,那麼根據紅色節點不能相鄰的條件,它的孩子一定是黑色的,如下所示:

紅色節點僅一個孩子

但這個子樹的黑高卻不再平衡了(注意每個節點的葉節點都是一個NIL節點),因此紅色節點不可能只有一個孩子。

而若是一個黑色節點僅有一個孩子,如果其孩子是黑色的,同樣會打破黑高的平衡,所以其孩子只能是紅色的,如下所示:

黑色節點僅一個孩子

只有這一種情況符合紅黑樹的定義,這時要刪除這個元素,只需要使用其孩子代替它,僅代替值而不代替顏色即可,上圖的情況刪除完後變為:

刪除完畢

可以看到,樹的黑高並沒有發生變化,因此也不需要進行調整。

情況3:待刪除元素有兩個孩子

我們在討論二叉排序樹時說過,如果刪除一個有兩個孩子的元素,可以使用它的前驅或者後繼結點代替它。因為它的前驅或者後繼結點最多隻會有一個孩子,所以這種情況可以轉為情況1或情況2處理。

總結

刪除元素最複雜的是情況1,這主要由其兄弟結點以及兄弟結點的孩子顏色共同決定。這裡簡要做下總結。

我們以N代表當前待刪除節點,以P代表父結點,以S代表兄弟結點,以SL代表兄弟結點的左孩子,SR代表兄弟結點的右孩子,如下所示:

圖樣

根據紅黑樹定義,這種情況下S要麼有紅色的子結點,要麼只有NIL結點,以下對S有黑色結點的情況均表示NIL

主要有以下幾種:

  1. S是紅色,P一定是黑色,S也不會有紅色的孩子,如下:

紅色兄弟結點

此時把PS顏色變換,再左旋,如下:

左旋

這樣變換後,N支路上的黑色結點並沒有增加,所以依然少一個,

  1. P,S以及S的全部孩子都是黑色

無論S有幾個孩子,或者沒有孩子,只要不是紅色都是這種情況,此時情況如下:

全黑色

我們把S染為紅色,這樣一來,NS兩個支路都少了一個黑色結點,所以可以把問題向父結點轉移,通過遞迴解決。染色後如下:

染色

  1. P為紅(S一定為黑),S的孩子都為黑

這種情況最為簡單,只需要把P和S顏色交換即可。這樣N支路多了一個黑色元素,而S支路沒有減少,所以達到了平衡。

交換前

交換後

  1. P任意色,S為黑,N是P的左孩子,S的右孩子SR為紅,S的左孩子任意

如下所示

任意色

此時將S改為P的顏色,SRP改為黑色,然後左旋,結果如下:

左旋

可以發現,此時N支路多了一個黑色結點,而其餘支路均沒有收到影響,所以調整完畢。

  1. P任意色,S為黑,N是P的左孩子,S的左孩子SL為紅,S的右孩子SR為黑,如下所示:
    SR黑色

此時變換SSL的顏色,然後右旋,結果如下:

右旋

這時,所有分支的黑色結點數均沒有改變,但情況5轉為了情況4,再進行一次操作即可。

還有一些情況與上述是對稱的,我們進行相應的轉換即可。

#總結 紅黑樹的操作比較複雜,插入元素可能需要多次變色與旋轉,刪除也是。這些操作的目的都是為了保證紅黑樹的結構不被破壞。這些複雜的插入與刪除操作希望大家可以親手嘗試一下,以加深理解。

紅黑樹是JDK中TreeMapTreeSet的底層資料結構,在JDK1.8中HashMap也用到了紅黑樹,所以掌握它對我們後續的分析十分重要。

關於紅黑樹與AVL樹的區別,以及為何選用紅黑樹,已經不屬於我們的討論範圍,大家可以查閱相關資料進一步瞭解。

上一篇:Java集合原始碼分析之基礎(五):平衡二叉樹(AVL Tree)

下一篇:Java集合原始碼分析之Iterable概述

本文到此就結束了,如果您喜歡我的文章,可以關注我的微信公眾號: 大大紙飛機

或者掃描下方二維碼直接新增:

公眾號

您也可以關注我的github:github.com/LtLei/artic…

程式設計之路,道阻且長。唯,路漫漫其修遠兮,吾將上下而求索。

相關文章