提及紅黑樹就頭痛,在大學時就沒搞懂,看的暈暈乎乎,理解不了。直到前幾天在極客時間的《數據結構與算法之美》
專欄中的《26 | 紅黑樹(下):掌握這些技巧,你也能夠實現一個紅黑樹 》,再次看到講解紅黑樹插入刪除如何保持平衡,很惋惜,仍是沒看明白。但在留言區看到小夥伴推薦的紅黑樹是2-3樹的變形,以2-3樹的角度去理解紅黑樹就容易多了
。因而,就跑去看了2-3樹
相關的文章,發現理解起來是要簡單些。算法
通常咱們接觸最多的是二叉樹,也就是一個父節點最多有兩個子節點。2-3樹的意思就是說,一個父節點能夠有兩個子節點,也能夠有三個子節點,而且其也知足相似二叉搜索樹
的定義(父節點的值大於左子樹,但小於右子樹),全部葉子節點都在同一層。bash
2節點:父節點存儲一個值,最多有左右兩個子樹。假設父節點爲p,子節點爲l(左節點)、r(有節點),且知足:數據結構
l < p < r
複製代碼
如圖所示: spa
3節點:父節點存儲兩個值,最多有左中右三個子樹。假設父節點分別爲p1,p2,子節點分別爲l(左節點)、m(中間節點)、r(右節點),且知足:3d
l < p1
p1 < m < p2
r > p2
複製代碼
如圖所示: code
跟BST
的查找相似:cdn
從根節點開始比較,若相等,則結束。若是小於根節點,則說明它應該在左邊,選定左節點進行比較;若是大於根節點,則說明在右邊,選定右節點進行比較,如不相等,則繼續循環。如到最後訪問到空節點,則說明沒找到。blog
只不過對於3節點的狀況,就須要判斷左中右子樹,原理同樣。get
下面以這顆樹舉例說明,要查找的值存在和不存在的狀況。it
查找5。
查找24。
在將插入以前,先介紹一下節點分裂與合併。
2-3樹只能存在2節點和3節點,因爲插入的時候會引入4節點,因此咱們須要將其分裂。
好比單個4節點,只需將中間節點往上提,左邊值做爲其左子樹,右邊值做爲其右子樹便可。
好比有父節點的4節點,節點分裂後,需與父節點進行合併。若合併後父節點仍是4節點,則繼續分裂,直至知足定義爲止。下圖中6與3合併後,知足條件,無需再進行操做。
插入一個節點後,也要知足2-3樹的定義。咱們須要找到一個適合的位置來插入新的值,可是和二叉樹不一樣的是,它不會生成新的葉子節點來存儲,而是找到合適的葉子節點來進行合併。
可是注意,插入的原則是儘可能保持樹的高度
,也就是儘可能不要增長
樹的高度。由於樹的高度越小,查找效率會更高。
下面分幾種狀況來講明不一樣的處理狀況。其關鍵字是往上分裂
,從下往上生長。
生成新節點,則其爲根節點。
若是不能直接放到空的子節點,則放到父節點中,此時成爲3節點,仍然知足定義。好比咱們在這棵樹中插入12。
首先找到待插入節點9。
節點9爲2節點,可直接插入。
這種狀況下,稍微會複雜一些,由於涉及到分裂,且跟待插入節點的父節點
有關。假定待插入節
點爲p
,待插入節點的父節點
爲pp
。
將節點強插到p中,此時p中會有三個值,咱們暫且稱之爲4節點
。4節點是不知足2-3樹的定義的,所以須要將4節點中的某個節點往上抽離,與pp
進行合併。這時須要考慮pp的類型了。
若pp
爲二節點
將分裂的節點放到pp
中,則pp
成爲3節點
,知足定義。好比咱們在這棵樹中插入13。
若pp
爲三節點
將分裂的節點放到pp
中,則pp
成爲了4節點
,不知足定義,那麼4-節點須要提出一個值,並向上合併,這時須要從新設置新舊節點的關係。往上合併的過程就是繼續套用這幾種狀況。好的狀況是往上的過程當中遇到了2節點,且平衡,則結束;壞的狀況是一直到根節點,而且根節點是3樹,那麼只好繼續往上分裂出新的根節點,而後處理新節點與其餘節點的關係,此時樹高增長了1。
好比在這顆樹中插入18。
2)向上分裂,將18插入父節點,變成4節點,需繼續分裂
3)根節點成爲3節點,插入結束
以上的插入,樹的高度都沒有變化。下面說一種樹的高度會+1的狀況。
好比在這棵樹中插入32。
刪除的狀況會複雜一些,下面分幾種狀況來講。
直接刪除便可。以下圖12可直接刪除。
刪除後,3節點成2節點。
這裏須要區分臨近兄弟節點的類型。先將節點刪除。
好比在這顆樹中刪除7。
兄弟節點爲2節點,這時須要判斷父節點類型
a. 父節點爲3節點 此時兄弟節點不夠借,父節點降元,從3節點變成2節點,與兄弟節點合併。
好比從這棵樹中刪除36。
30與18合併,3節點變成2節點,刪除完成
b. 父節點爲2節點 將父節點和兄弟節點合併,造成新的節點,這是把新節點當作當前節點,不斷套用上述幾種狀況進行調整,直至平衡。這種狀況下,若根節點是2節點,樹的高度會減1。
例1 從這顆樹中刪除12
[8,9]
)的父節點和兄弟節點都爲2節點,還需進行合併(即節點2
,5
合併)合併完成以下圖。
例2 從這棵樹中刪除30
節點30的父節點和兄弟節點爲2節點,進行合併,此時[22,25]爲新節點。
把[22,25]看做當前節點,因爲其兄弟節點爲2節點,父節點爲3節點,套用2.a
中的狀況,父節點降元,調整完成。
將該節點與其前驅
或後繼
節點交換,而後刪除交換後的葉子節點,此時轉換成上一種狀況的處理。
使用中序遍歷的順序,前驅就是指其前一個節點,後繼是指其後面的一個節點。最直接的定位以下:
好比下圖,5的前驅是3,後繼是7。
那爲何要用前驅/後繼節點交換呢?
由於,用前驅/後繼節點交換後,才能保持大小順序。後繼節點是右子樹中最小的節點,與父節點交換後,排除待刪除的葉節點,仍保持左子樹<新父節點<右子樹的關係。同理,前驅節點是左子樹中最大的節點,交換後,仍能保持。
這裏咱們使用後繼節點
來進行替換。
從這顆樹中刪除節點5。
因爲5的後繼節點是7,先將值進行交換。這時候目的就是刪除葉子節點5
,因而能夠轉換成其父節點和兄弟節點都爲2節點
的狀況進行調整。
紅黑樹也是一種二叉平衡樹,它知足以下幾個特性(根據算法中的定義):
這個定義可能跟咱們日常看到的不太同樣,因爲是以2-3樹來理解紅黑樹,定義紅鏈在左邊,這樣才能跟2-3樹徹底對應上。
AVL
是一種極度平衡的二叉樹,那爲何不用AVL呢?由於AVL
插入刪除要保持平衡,相比紅黑樹要慢一些,須要左旋右旋等等。但實際上它的旋轉也只是幾個場景的套用,哪些場景須要怎麼旋轉,理解就好了。
而紅黑樹是近似平衡的(黑平衡),也就是說它不像AVL
那樣絕對的平衡,因此添加/刪除節點後的平衡操做沒那麼多。
因此對於插入和刪除操做較多的場景,用紅黑樹效率會高一些。
主要思想:3節點分裂成2節點。
將3節點的第一個元素,做爲第二個元素的左節點,並用紅色的線鏈接,此時紅色線鏈接的節點就至關於紅色。
將2-3樹按照以上思想轉換後,就獲得了一顆紅黑樹。用這種方式理解是否是簡單多了呢?
同時也有幾個問題值得咱們思考:
爲何紅鏈規定在左邊呢?
我以爲是前人的一個約定,爲了保持統一,簡化處理,都放在左邊。那都放右邊是否是也能夠呢?
沒有任何一個節點同時與兩個紅連接相連
由於一個紅鏈表示一個3節點,若是有2個紅鏈相連,則表示爲4節點,不符合2-3樹定義。
根節點爲黑色
只有3節點的左鏈才爲紅色。根節點沒有父節點,不可能爲紅色。
根節點到葉子節點通過的黑色節點數目相同
由於2-3樹是完美平衡的。紅黑樹中通過的黑節點數=其層數。
散列表的衝突處理
map的實現,底層通常會採用紅黑樹,在節點多的時候效率高。 在節點少的時候,可用鏈表方式。