1. 程式人生 > >Java 集合 | 紅黑樹 | 前置知識

Java 集合 | 紅黑樹 | 前置知識

一、前言

0tnv1e.png

為啥要學紅黑樹吖?

因為筆者最近在趕專案的時候,不忘抽出時間來複習 Java 基礎知識,現在準備看集合的原始碼啦啦。聽聞,HashMapjdk 1.8 的時候,底層的資料結構發生了變化,變成了陣列+連結串列+紅黑樹。很好,沒了解過紅黑樹,所以就趁今天閒暇學習一下啦

二、什麼是紅黑樹?

2.1 有啥用處?

紅黑樹從本質上來說就是一顆二叉查詢樹,但是在二叉樹的基礎上增加了著色相關的性質,使得紅黑樹可以保證相對平衡,從而保證紅黑樹的增刪改查的時間複雜度最壞也能達到 O(log N)

2.2 紅黑樹的六條性質你知道嗎?

  1. 每個節點要麼是黑的,要麼是紅的
  2. 根節點是黑的
  3. 葉節點是黑的
  4. 如果一個節點是紅的,他的兩個兒子節點都是黑的
  5. 對於任一節點而言,其到葉節點樹尾端NIL指標的每一條路徑都包含相同數目的黑節點。這其實就是黑高啦!
  6. 新插入的節點必須是紅色噢!
0tVeC4.png

2.3 插入操作

首先,先看這個圖吧,這就是全部的插入操作後,平衡的方法啦!其實我還是喜歡用筆畫 hhh

0tZCJe.png

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

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

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

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

img

插入 1

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

img

插入 10

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

img

插入 9

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

img

右旋

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

img

染色

然後以祖父結點 1 左旋:

img

左旋

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

img

插入 2

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

img

染色

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

img

染色

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

img

插入 3

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

img

染色

然後左旋:

img

左旋

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

img

插入 8

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

img

染色

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

img

插入 7

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

img

右旋

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

img

左旋

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

img

插入 4

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

img

染色

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

img

錯誤示意圖

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

img

調整

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

img

染色

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

img

右旋

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

img

插入 5

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

img

左旋

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

img

插入 6

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

img

染色

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

img

染色

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

img

2.4 刪除操作

刪除的規則如下:

0tmMKs.png

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

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

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

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

img

紅黑樹

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

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

img

染色

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

img

左旋

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

img

調整完畢

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

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

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

img

紅色節點僅一個孩子

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

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

img

黑色節點僅一個孩子

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

img

刪除完畢

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

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

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

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

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

img

圖樣

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

主要有以下幾種:

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

紅色兄弟結點

此時把 P 和 S 顏色變換,再左旋,如下:

img

左旋

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

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

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

img

全黑色

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

img

染色

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

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

img

交換前

img

交換後

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

如下所示

img

任意色

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

img

左旋

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

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

    img

    SR 黑色

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

img

右旋

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

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

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

三、結語

紅黑樹其實一開始看起來有點懵逼懵逼的,但是,其實你看完了全文,然後手動模擬一下插入刪除操作,發現也不是想象中的很難啦!

附上筆者學習紅黑樹做的草稿hhh 如果還是看不明白,推薦一個視訊:紅黑樹原理原始碼講解(java),全B站講解最細緻版本,看完月薪最少漲5k!

0tnAy9.png
0tn1QH.png

如果文章對您有一點幫助的話,希望您能點一下贊,您的點贊,是我前進的動力

本文參考連結:

  • Java集合原始碼分析之基礎(六):紅黑樹(RB Tree)
  • 紅黑樹原理原始碼講解(java),全B站講解最細緻版本,看完月薪最少漲5k!
  • 圖解紅黑樹

本文使用 mdnice 排版