二分搜索樹是為了快速查找而生,它是一顆二叉樹,每一個節點只有一個元素(值或鍵值對),左子樹所有節點的值均小於父節點的值,右子樹所有的值均大於父節點的值,左右子樹也是一顆二分搜索樹,而且沒有鍵值相等的節點。它的查找、插入和刪除的時間復雜度都與樹高成比例,期望值是O(log n)。
但是插入數組如[],二分搜索樹的缺點就暴露出來了,二分搜索樹退化成線性表,查找的時間復雜度達到最壞時間復雜度O(n)。
動畫:二分搜索樹退化成線性表
那有沒有插入和刪除操作都能保持樹的完美平衡性(任何一個節點到其葉子節點的路徑長度都是相等的)?
有,B樹。B樹是一種自平衡的樹,根節點到其葉子節點的路徑高度都是一樣的,能夠保持數據有序(通過中序遍歷能得到有序數據)。B樹一個節點可以擁有2個以上的子樹,如2-3樹、2-3-4樹甚至2-3-4-5-6-7-8樹,它們滿足二分搜索樹的性質,但它們不屬於二叉樹,也不屬於二分搜索樹。
2-3-4樹的完美平衡,每條從根節點到葉子節點的路徑的高度都是一樣的
2-3-4樹有以下節點組成:
2-節點,含有一個元素(值或鍵值對)和兩個子樹(左右子樹),左子樹所有的值均小於父節點的值,右子樹所有的值均大於父節點的值;
3-節點,含有兩個元素和三個子樹,左子樹所有的值均小於父節點最小元素的值,中間子樹所有的值均位於父節點兩個元素之間,右子樹所有的值均大於父節點最大元素的值;
4-節點,含有三個元素和四個子樹,節點之間的比較也滿足二分搜索樹的性質。
2-3-4樹查找算法
2-3-4樹的查找類似二分搜索樹的查找。
2-3-4樹插入算法
2-3-4樹的插入算法是消除當前節點是4-節點,將4-節點分解成多個2-節點,中間的2-節點與父節點合並成3-節點或4-節點。
沿着鏈接向下進行變換分解4-節點分為兩種情況:
1)4-節點作為根節點,分解成3個2-節點,中間的2-節點作為根節點;
2)當前節點為4-節點,分解成3個2-節點,中間的2-節點與父節點合並成3-節點或4-節點;
圖:沿着鏈接向下進行變換,分解4-節點
在沿着左右鏈接向下進行變換的同時,也會進行命中查找。如果元素是鍵值對的話,查找命中將舊的值賦值為新的值;如果元素是一個值的話,查找命中將忽略之,因為二分搜索樹需要滿足沒有相等的元素;如果需要支持重復的元素,則在元素對象添加count屬性,默認為1。
如果查找未命中,則將待插入元素插入在葉子節點上。樹底下插入一個元素只有兩種情況:向2-節點中插入和向3-節點中插入。
圖:樹底下插入一個元素
2-3-4樹刪除算法
2-3-4樹的刪除算法是消除當前節點是2-節點,向兄弟節點或父節點借一個元素過來。
刪除最小元素
從根節點的左孩子開始,沿着左鏈接向下進行變換可以分為三種情況:
1)當前節點不是2-節點,跳過;
2)當前節點是2-節點,兄弟節點是2-節點,將當前節點、父節點最小元素和兄弟節點合並為4-節點,當前節點變換成4-節點;
3)當前節點是2-節點,兄弟節點不是2-節點,將兄弟節點的最小元素移到父節點,父節點的最小元素移到當前節點,當前節點變換成3-節點。
圖:沿着左鏈接向下進行變換
刪除最大元素
從根節點的右孩子開始,沿着右鏈接向下進行變換也同樣分為三種情況:
1)當前節點不是2-節點,跳過;
2)當前節點是2-節點,兄弟節點是2-節點,將當前節點、父節點的最大元素和兄弟節點合並為4-節點,當前節點變換成4-節點;
3)當前節點是2-節點,兄弟節點不是2-節點,將兄弟節點的最大元素移到父節點,父節點的最大元素移到當前節點,當前節點變換成3-節點。
圖:沿着右鏈接向下進行變換
刪除任意元素
學習過刪除最小元素和刪除最大元素算法之后,刪除任意元素的算法自然就簡單了。刪除任意元素算法需要先進行命中查找,若查找命中,則將右子樹的最小值替換掉待刪除元素,然后將右子樹進行刪除最小元素的算法。
2-3-4樹雖滿足二分搜索樹的性質,但不是一顆二分搜索樹。如果期望它是一顆二分搜索樹,就需要將3-節點和4-節點替換為多個2-節點,還需要注明元素之間的關系(用紅鏈接表示)。
替換3-節點和4-節點
圖:替換3-節點
圖:替換4-節點
但是存在一個問題,2-3-4樹因為3-節點的不同表示會有很多種不同的紅黑樹,3-節點既可以左傾,也可以右傾。所以為了保證樹的唯一性,3-節點只考慮左傾,當然你也可以只考慮右傾。
這樣對於任何一顆2-3-4樹,只考慮左傾的情況下,都能得到唯一的一顆對應的紅黑樹,這種樹也叫左傾紅黑樹,相對比較減少了復雜性,設計更容易被實現。
紅黑樹查找算法
紅黑樹的查找算法和二分搜索樹一樣。
關於鏈接的顏色變換只跟顏色轉換有關,而旋轉不會改變鏈接的顏色變換,只在被紅鏈接指向的節點變成紅色,被黑鏈接指向的節點變成黑色。
旋轉
旋轉是將不滿足紅黑樹性質的3-節點和4-節點進行旋轉,如果3-節點出現右鏈接,則將右鏈接通過左旋轉變成左鏈接;如果4-節點出現一個紅節點連着兩條紅鏈接,則將4-節點配平。
左旋轉
圖:左旋轉
右旋轉
圖:右旋轉
圖:3-節點和4-節點的旋轉
Code:右旋轉和左旋轉
顏色轉換
顏色轉換只應用於4-節點。
圖:顏色轉換
Code:顏色轉換
紅黑樹插入算法
回顧之前的2-3-4樹的插入算法,它有兩個過程:沿着鏈接向下進行分解4-節點和樹底下插入一個元素。
紅黑樹的插入算法和2-3-4樹的插入算法類似,它不僅包含前面兩個過程,還增加了向上進行變換的過程,此過程是將3-節點左傾和4-節點配平。
紅黑樹插入算法會先從根節點開始,沿着左右鏈接向下進行變換,目的是為了分解4-節點。如果該節點的左右孩子都是紅節點,則通過flipColors方法進行顏色轉換,接着進行下一個子節點;如果不是,則直接進行下一個子節點。
到達樹底的時候,則意味着可以開始插入新的元素。
如果紅黑樹目前是一顆空樹,插入紅色的元素作為第一個節點,然后該節點變成黑色。如果不是一顆空樹,插入元素分為三種情況:向2-節點插入新元素、向3-節點插入新元素和向4-節點插入新元素。
向2-節點插入新元素
向2-節點插入新元素很簡單,如果新元素的值小於父節點,直接插入紅色的節點即可;如果新元素的值大於父節點,則產生一個紅色右鏈接,插完之后則將3-節點進行左旋轉,將右鏈接變成左鏈接,被紅鏈接指向的節點變成紅色,被黑鏈接指向的節點變成黑色。
圖:向2-節點插入新元素
向3-節點插入新元素
因為前面的3-節點進行過旋轉,此時的3-節點肯定滿足左傾紅黑樹的性質。向3-節點插入新元素分為三種情況:
1)新元素的值位於3-節點中的兩元素之間;
2)新元素的值小於3-節點中的最小元素;
3)新元素的值大於3-節點中的最大元素。
圖:向3-節點插入新元素
##### 向4-節點插入新元素
向4-節點插入新元素之前需要先進行顏色轉換,才可以進行插入新的元素。
圖:向4-節點插入新元素
插完新元素之后需要滿足紅黑樹的性質,則在沿着父節點的鏈接向上進行變換,具體做法和向3-節點插入新元素的做法類似,通過左旋轉將3-節點左傾和左右旋轉將4-節點配平,沒有顏色轉換。
動畫:2-3-4樹與紅黑樹的插入
Code:紅黑樹插入算法
紅黑樹刪除算法
紅黑樹刪除算法也需要進行旋轉和顏色轉換操作,在插入算法中為了待插入元素所在的節點不是4-節點,所以在沿着左右鏈接向下進行變換時將4-節點分解成3個2-節點,中間的2-節點與父節點合並;而在刪除算法中為了待刪除元素所在的節點不是2-節點,所以在沿着左右鏈接向下進行變換時將2-節點向其它不是2-節點的節點(兄弟節點或父節點)借一個元素過來,合並成3-節點。
所以,只要是2-節點的節點,如果兄弟節點不是2-節點,就將兄弟節點的與父節點鄰近的元素移到父節點,而父節點將與當前節點鄰近的元素移到當前節點;如果兄弟節點是2-節點,則將父節點的與當前節點鄰近的元素移到當前節點。(是不是很繞?待會在后面刪除最值算法中詳細給出)
然后刪除完一個元素之后需要進行修復調整,將這個樹滿足紅黑樹的性質。如果右鏈接是紅色,將右鏈接通過左旋轉變成左鏈接;如果有連續的左鏈接,通過右旋轉配平,然后進行顏色轉換。
Code:向上變換(修復調整)
刪除最小元素
刪除最小元素算法和二分搜索樹一樣,一直遞歸它的左孩子,直到它的左孩子為空才進行刪除這個最小元素。但是紅黑樹在遞歸的同時如何旋轉和顏色轉換是個問題。
刪除最小元素算法一直沿着左鏈接向下進行轉換,對照2-3-4樹,我們可以給出三種情況,從根節點開始:
1)當前節點(父節點位置)的左子節點不是2-節點,直接進行下一個節點(左子節點);
2)當前節點的左子節點和右子節點都是2-節點,則將左子節點、當前節點的最小元素和右子節點合並成4-節點,然后進行下一個節點;
3)當前節點的左子節點是2-節點,右子節點不是2-節點,則將右子節點的最小元素移到當前節點的位置,當前節點的最小元素移到左子節點,然后進行下一個節點。
圖:沿着左鏈接向下進行轉換
Code:沿着左鏈接向下變換
直到某元素左孩子為空的時候,此時的元素是這個樹的最小元素。因為通過前面的轉換,最小元素肯定被一個紅鏈接指向,刪除這個元素之后通過balance方法修復調整為紅黑樹。
Code:刪除最小元素算法
刪除最大元素
刪除最大元素算法和刪除最小元素算法類似的,也分為三種情況:
1)當前節點(父節點位置)的右子節點不是2-節點,直接進行下一個節點(右子節點);
2)當前節點的右子節點和左子節點都是2-節點,則將右子節點、當前節點的最大元素和左子節點合並成4-節點,然后進行下一個節點;
3)當前節點的右子節點是2-節點,左子節點不是2-節點,則將左子節點的最大元素移到當前節點的位置,當前節點的最大元素移到左子節點,然后進行下一個節點。
圖:沿着右鏈接向下進行變換
Code:沿着右鏈接向下變換
Code:刪除最大元素算法
刪除任意元素
學習過前面的刪除最小元素算法和刪除最大元素算法,刪除任意元素會變得很簡單。刪除最小元素算法會一直沿着左鏈接向下進行變換,刪除最大元素算法會一直沿着右鏈接向下進行變換,而刪除任意元素算法需要同時存在着左右鏈接向下進行變換。
刪除任意元素算法需要先進行命中查找,在命中查找的過程中會進行沿着左右鏈接向下變換,如果查找命中則將右子樹的最小元素替換掉待刪除元素,然后進行右子樹的刪除最小元素算法;如果查找未命中,則直接返回balance函數,向上將3-節點左傾或將4-節點配平。
Code:刪除任意元素算法
動畫:2-3-4樹與紅黑樹的刪除
學習完上面的算法之后,可以總結下紅黑樹的性質:
1)每個節點或是紅色的,或是黑色的;
2)根節點是黑色的;
3)每個葉子節點(NIL)是黑色的;
4)如果一個節點是紅色的,則它的兩個子節點都是黑色的(NIL節點也是黑色的);
5)對每個結點,從該節點到其所有后代葉子節點的簡單路徑上,均包含相同數目的黑色節點(黑鏈接平衡)。
喜歡本文的朋友,歡迎關注公眾號「算法無遺策」,收看更多精彩內容