1. 程式人生 > >三十張圖助你看清紅黑樹的前世今生

三十張圖助你看清紅黑樹的前世今生

> 微信公眾號:小超說 > 這是查詢算法系列的第三篇 : 三十張圖助你看清紅黑樹的前世今生 ![](https://user-gold-cdn.xitu.io/2020/7/5/1731f16ccb8c278c?w=704&h=698&f=png&s=48026) > 在《演算法》(第4版)中,紅黑樹的實現直接採用了左傾紅黑樹 (LLRB) 的方法,左傾紅黑樹可以用更少的程式碼量實現紅黑樹,在本文中我也使用他的方法理解。相比於經典紅黑樹,增加了一個限制**紅節點一定是父節點的左子節點**,但是實現卻容易不少 ## 一、紅黑樹的定義 1.每個節點要麼是黑色要麼是紅色; 2.根節點是黑色; 3.每個葉子節點都是黑色的空節點(NIL),也就是說,葉子節點不儲存資料; 4.任何相鄰的節點都不能同時為紅色,也就是說,紅色節點是被黑色節點隔開的; 5.每個節點,從該節點到達其可達葉子節點的所有路徑,都包含相同數目的黑色節點; 這就是紅黑樹的定義,但你看完肯定會一臉懵,我也是一樣。我會想:紅黑樹的來源是什麼?為什麼要區分紅色和黑色節點呢?這些性質是怎麼來的,或者有什麼作用?不著急,你聽我慢慢道來,我希望,你能夠通過這篇文章對紅黑樹有一個清晰的認識,包括它的來歷,意義以及各種操作。 ## 二、平衡二叉查詢樹 首先,還記得咱們上次文章中介紹的**二叉查詢樹**嗎?儘管它具有不錯的效能,但是仍然無法避免極端的情況。一般的二叉查詢樹的生成和資料插入的順序密切相關,我們來看一組情況: ![](https://user-gold-cdn.xitu.io/2020/7/4/1731802153e8c2da?w=673&h=578&f=png&s=19525) 我們依次插入[B,A,C,G,R,Z],最後得到的樹無疑已經退化到接近連結串列,這時,效能會受到很大地影響。 我們的任務就是**試圖構造一種新的資料結構**來解決這種問題,即構建**平衡二叉查詢樹**: ![](https://user-gold-cdn.xitu.io/2020/7/4/17318346b677390f?w=671&h=449&f=png&s=19478) 平衡二叉查詢樹的嚴格定義是:每個節點的左子樹和右子樹的高度差至多等於1。但我們不必嚴格死扣定義,我們只需要知道,平衡二叉查詢樹中“平衡”的意思,其實就是讓整棵樹左右看起來比較“**對稱**”、比較“**平衡**”,不要出現左子樹很高、右子樹很矮的情況。這樣就能讓整棵樹的高度相對來說低一些,相應的插入、刪除、查詢等操作的效率高一些。 ## 三、2-3樹 我們不著急直接介紹紅黑樹,請跟隨我的思路先學習一下`2-3樹`,循序漸進地走向紅黑樹。 ### 2-3樹的定義 > 一棵2-3樹由以下節點組成: > > - 2-節點,含有一個鍵(及其對應的值)和兩條連結,左連線指向的2-3樹中的鍵都小於該節點,右連結指向的2-3樹中的鍵都大於該節點。 > - 3-節點,含有兩個鍵(及其對應的值)和三條連結,左連結指向的2-3樹中的鍵都小於該節點,中連結指向的2-3樹中的鍵都位於該節點的兩個鍵之間,右連結指向的2-3樹中的鍵都大於該節點。 ![](https://user-gold-cdn.xitu.io/2020/7/4/17318a91398fc78d?w=674&h=547&f=png&s=23782) 如果將一棵`2-3樹`進行中序遍歷,得到的是一個有序的序列,比如我們對下圖進行中序遍歷,會得到`[A,C,F,J,K,M,O,Q,R,S,T]`。 ![](https://user-gold-cdn.xitu.io/2020/7/4/17318978c9b947ab?w=866&h=518&f=png&s=28701) ### 2-3樹的建立 我們首先給出兩條原則【融合】與【拆分】: - 原則1. 加入新節點時,不會往空的位置新增節點,而是新增到最後一個葉子節點上(使2-節點變為3-節點,3-節點變為4-節點) - 原則2. 如果出現4-節點,要將它分解成三個2-節點組成的樹,並且分解後新樹的根節點需要向上和父節點融合(父節點變成3-節點或者4-節點) 接下來我們一起看一下`2-3`樹的生長過程: ![](https://user-gold-cdn.xitu.io/2020/7/4/1731914b21bbe716?w=648&h=591&f=png&s=25899) ![](https://user-gold-cdn.xitu.io/2020/7/4/173191520db9d59c?w=692&h=731&f=png&s=44287) ![](https://user-gold-cdn.xitu.io/2020/7/4/1731915954012c17?w=615&h=734&f=png&s=40838) ![](https://user-gold-cdn.xitu.io/2020/7/4/1731915d732073b4?w=697&h=749&f=png&s=48454) ![](https://user-gold-cdn.xitu.io/2020/7/4/1731916245c5db87?w=1276&h=802&f=png&s=82318) 可以發現,雖然我們的插入順序是`[A->C->F->J->K->M->O->]`,是升序的,但是我們構造的2-3樹卻始終保持平衡。 那麼,如何實現這種高效的**資料結構**呢?上面的一些操作我們描述清楚是一回事,但是實現又是另外一回事。我們選擇**紅黑二叉查詢樹**這種資料結構來實現它,簡稱**紅黑樹**。 ### 紅黑樹與2-3樹的等價 紅黑樹背後的思想是**用標準的二叉查詢樹(完全由2-節點構成)和一些額外的資訊(替換3-節點)來表示2-3樹**。我們將樹中的連結分為兩種型別:一種是紅連結(左傾),一種是黑連結。其中紅連結將兩個2-節點連線起來構成一個3-節點,黑節點則是2-3樹中的普通節點。為了方便,我們把**紅連結指向的節點標記為紅色,其他節點標記為黑色**,這就是紅黑樹的由來,具體見下圖所示: ![](https://user-gold-cdn.xitu.io/2020/7/4/1731947d5d880f19?w=919&h=651&f=png&s=45698) 我們再重新審視一下紅黑樹定義要求的各種性質: (1)每個節點要麼是黑色要麼是紅色;(這個就不多說了,很自然) (2)根節點是黑色; 根節點只有兩種情況,第一種可能是`2-節點`,此時對應著黑色;第二種可能是`3-節點`,此時根節點也是黑色的,你可以結合上圖理解。 (3)每個葉子節點都是黑色的空節點(NIL),也就是說,葉子節點不儲存資料; 這一條主要是為了簡化紅黑樹的實現而設定的,嚴格來說,我們之前畫的還不完整。 ![](https://user-gold-cdn.xitu.io/2020/7/4/1731990694d738c6?w=562&h=454&f=png&s=21957) (4)任何相鄰的節點都不能同時為紅色,也就是說,紅色節點是被黑色節點隔開的; 我們假設存在同時為紅色的兩個相鄰節點,那麼會怎麼樣呢?見下圖: ![](https://user-gold-cdn.xitu.io/2020/7/4/173199342b67ca70?w=1095&h=614&f=png&s=53946) (5)每個節點,從該節點到達其可達葉子節點的所有路徑,都包含相同數目的黑色節點; 這條要求保證了紅黑樹的效能。我們隨意選一條從根節點到葉子節點的路徑,由於性質4,任何相鄰的節點不能同時為紅色,所以每存在一個紅節點,至少對應了一個黑節點,即**紅色節點個數<=黑色節點個數**,我們再結合`2-3樹`,每個黑色節點對應著一個2-節點或一個3-節點;根據`2-3樹`的性質,**其節點<=log(N)**,可以推出**黑色節點<=log(N)**,那麼**加上紅色節點不會大於2log(N)**。 總結如下就是: > 紅色節點個數<=黑色節點個數=====>每一個黑色節點對應2-3樹的每一個節點====>2-3樹節點<=log(N)====>黑色節點<=log(N)====>紅黑樹節點<=2log(N) > 我們將2-3樹染色後稱為紅黑樹,然後給出了**一些顏色的規則**,這些規則能夠幫助我們快速的構建使用紅黑樹。這使得我們以後只需要將注意力集中在**顏色**上面,只需要去**維護上述規定的性質**即可。我想,這大概就是紅黑樹存在的意義! 紅黑樹的節點表示程式碼如下: ``` private static final boolean RED = true; private static final boolean BLACK = false; private class Node{ int key;//鍵 String value;//值 Node left,right;//左右子樹 boolean color; } //建構函式 Node(int key,String value,boolean color){ this.key=key; this.value=value; this.color=color; } //判斷節點x的顏色 private boolean isRed(Node x){ if(x==null) return false; return x.color==RED; } ``` ## 四、紅黑樹的建立 首先,我們回顧一下2-3樹的建立過程, - 原則1. 加入新節點時,不會往空的位置新增節點,而是新增到最後一個葉子節點上(使2-節點變為3-節點,3-節點變為4-節點) - 原則2. 如果出現4-節點,要將它分解成三個2-節點組成的樹,並且分解後新樹的根節點需要向上和父節點融合(父節點變成3-節點或者4-節點) 原則1我們可以對應為:每次插入新節點的時候都插入紅色節點+根節點永遠是黑色。這是因為紅色節點被紅色連結所指向,而紅色連結是2-3節點的**內部連結**,或者也可以理解為**始終插入的位置都是同級節點**。另一方面,初始化樹的時候,第一個節點應該是黑色的,所以增加這一條規則。 原則2我們可以對應為三種簡單的操作:**左旋轉,右旋轉和改變顏色** 所以,綜合以上對應關係,我們可以**直接建立紅黑樹而不必考慮2-3樹的種種性質**。 - 1、保持根節點是黑色 - 2、左旋轉 - 3、右旋轉 - 4、改變顏色 #### 1.左旋轉[圍繞某個節點左旋轉] 在我們定義的紅黑樹中,**紅色連結永遠是左連結**,換句話說,**紅色節點永遠是父節點的左子節點**,所以,在在操作的過程中,為了維持這種性質,我們實現了左旋轉這個操作。 ![](https://user-gold-cdn.xitu.io/2020/7/5/1731cc021737ec5e?w=329&h=241&f=gif&s=259065) 功能:右連結變左連結 具體過程見圖片: ![](https://user-gold-cdn.xitu.io/2020/7/4/1731a45654e09271?w=948&h=661&f=png&s=55753) 我們只是將用兩個鍵中較小的那個作為根節點改變為將較大者作為根節點,程式碼的實現還是比較容易的,結合上圖理解更輕鬆。 ``` Node rotateLeft(Node h){ Node x=h.right; h.right=x.left; x.left=h; h.color=RED; return x; } ``` #### 2.右旋轉[圍繞某個節點右旋轉] 右旋轉其實和左旋轉完全類似,它的功能是:左連結變右連結,程式碼只需要將`left`和`right`互換即可。 ![](https://user-gold-cdn.xitu.io/2020/7/5/1731cc068db27757?w=301&h=258&f=gif&s=223627) ![](https://user-gold-cdn.xitu.io/2020/7/5/1731c63cefa09893?w=1024&h=683&f=png&s=61147) 程式碼如下: ``` Node rotateRight(Node h){ Node x=h.left; h.left=x.right; x.right=h; x.color=RED; return x; } ``` #### 3.改變顏色 ![](https://user-gold-cdn.xitu.io/2020/7/5/1731e630483cb191?w=714&h=769&f=png&s=41857) ``` void flipColors(Node h){ h.color=RED; h.left.color=BLACK; h.right.color=BLACK; } ``` > 如果你已經看到這裡了,我先給你樹個大拇指