1. 程式人生 > >演算法導論第十三章 紅黑樹

演算法導論第十三章 紅黑樹

這碗雞湯我幹了,大家隨意。“雞湯”轉自:http://www.cnblogs.com/bakari/p/4900895.html

      寫在前面:這一章真的把我害慘了,之前至少嘗試看過3遍,每次看之前都下定決定一定要把它拿下,可是由於內容較多,深度夠深,以致於每次要不是中途有什麼事放棄了就跳過了,要不是花時間太多仍然不能理解而放棄。這次總算挺過來了,前後零零散散的時間加起來差不多也有兩天時間。這次能堅持下來並攻克,我想大概有這麼幾個原因吧:第一是之前下定的決心要寫一個最新版《演算法導論》的讀書筆記,之前幾章都堅持寫了,不能讓這個成為攔路虎,即使再難再花時間都要弄懂;第二是通過前面幾章的動手實踐,發現自己的理解能力、動手能力都進步了,自然這章理解起來也不那麼費力了;第三,如果有,那就是現在懂的東西多了,視野開闊了^-^。但說實話,也是費了不少心血,看了一下自己的打的草稿,超過十頁以上,密密麻麻都是一些紅黑樹,這些努力我覺得都是值得的,但我之所以說“把我害慘了”,甚至有點不甘的是:我好大一部分時間都花在了除錯程式碼上,原因是粗心大意寫錯了一些變數、指標......這一章由於涉及到多個指標的替換,所以切記在寫的時候一定足夠專注,儘量一口氣寫完,不要拖。

一、紅黑樹概覽

  紅黑樹是一種平衡二叉樹,什麼是平衡二叉樹?我的理解是加上”平衡條件“的二叉搜尋樹。其實這樣的理解還不準確,因為二叉搜尋樹只在某些特殊的情況下是不平衡的。比如下圖所示:

  所以,所謂樹形平衡與否,並沒有一個絕對的標準,”平衡“ 的大致意義是:沒有任何一個結點深度過大。二叉搜尋樹在某些特殊情況下,無法維持絕對的平衡,所以,其動態集合操作,最壞的時間複雜度為O(n)。因此就出現一些通過加上某種”平衡條件“來促使二叉搜尋樹達到絕對的平衡(確保整棵樹的深度維持在O(lgn))。紅黑樹的”平衡條件“是:賦予結點不同顏色,並對根結點到任何葉子結點的顏色進行約束。這樣的平衡不算太好,近似平衡,但效能已經比二叉搜尋樹提升了不少。

  紅黑樹不僅是二叉搜尋樹,且必須滿足以下5條平衡規則:

1)每個結點或是紅色,或是是黑色。
2)根結點是黑的。
3)所有的葉結點(NULL)是黑色的。(NULL被視為一個哨兵結點,所有應該指向NULL的指標,都看成指向了NULL結點。)
4)如果一個結點是紅色的,則它的兩個兒子節點都是黑色的。
5)對每個結點,從該結點到其子孫結點的所有路徑上包含相同數目的黑結點。

簡單的記法就是:紅黑 黑 黑 紅黑黑 黑

黑高度的定義: 從某個結點出發(不包括該結點)到達一個葉結點的任意一條路徑上,黑色結點的個數成為該結點x的黑高度。紅黑樹的黑高度定義為其根結點的黑高度。

二、平衡二叉樹歷史概覽

  最好的平衡是形如滿二叉樹這種,所以可以把全是黑色節點的滿二叉樹看做是紅黑樹的一個特列,其效能是最好的。但是是無論如何也不可能找到這樣的平衡條件,有一種樹退而求其次,它的平衡條件是要求任何結點的左右子樹高度相差不超過1,就是AVL樹。AVL樹是最早提出的將搜尋樹平衡化的想法的實踐。此外,還有由J.E.Hopcroft提出的一種”2-3“樹,這種樹是通過操縱結點的度數來維持平衡的。Bayer提出一種”2-3“樹的推廣,B樹。Anderson提出了一種程式碼更簡單的紅黑樹變種,稱為AA樹,AA樹和紅黑樹類似,只是左邊孩子永遠不能為紅色。還有一種treap樹則是由Seidel和Aragon提出的。

  此外,平衡二叉樹還有很多變種,包括帶權的平衡樹、k近鄰樹,以及替罪羊樹,還有一種比較有趣的”伸展樹“,伸展樹不需要明確的平衡條件來維持平衡,替代的是,每次存取時的”伸展操作“在樹內進行,後面會涉及到。另外還有就是跳錶,跳錶是擴充套件了一些額外指標的連結串列。

  但是,紅黑樹是真正的在實際中得到大量應用的複雜資料結構:C++STL中的關聯容器map,set都是紅黑樹的應用(所以標準庫容器的效率太好了,能用標準庫容器儘量使用標準庫容器);Linux核心中的使用者態地址空間管理也使用了紅黑樹。

三、紅黑樹實現

經驗之談:

1)插入刪除和二叉搜尋樹類似,插入的結點必須著紅色(因為如果是黑色,是一定會破壞性質5,難以修復,而如果是紅色,則可能破壞性質2和4,容易修復);

2)插入修復三種情況:發生在插入結點的父結點為紅色的情況下,即破壞了性質4,這個時候考慮插入點的uncle結點進行修復;性質2破壞,直接著黑色;

3)刪除恢復四種情況:發生在刪除結點為黑色的情況下,即破壞了性質5,這個時候考慮刪除點的brother結點進行修復;

4)旋轉操作注意指標的指向,每個都要考慮全了,parent、left、right缺一不可。

如果按照《演算法導論》書的步驟一步步往下看,是一定看得懂的,因為書上的東西是寫的最全的,網友寫的部落格雖然有些也不錯,但都是經過自己過濾過的,且不說語言表達怎樣,肯定沒有書本記錄得詳細。只是有些地方書本上表達得太深奧,可以藉助一些部落格來理解。比如說我在看到刪除修復的四種情況時,書上說的什麼”雙重黑色、紅+黑,x既不是黑色,也不是紅色“,把我搞得稀裡糊塗的,看了之後整個人都不好了,後來看了July的部落格才弄懂了個大概(見後面的參考引文),再回過頭來看就發現原來如此。

關於紅黑樹查詢、刪除等具體的細節就不再做過多的贅述,這裡只記錄下自己學習了之後的一些規律總結及心得。

關於旋轉:

旋轉有些書分為單旋和雙旋,雙旋顧名思義就是單旋兩次,單旋又分為左旋和右旋,操作是對稱的。旋轉操作對於理解樹的指標指向是再好不過了,就像理解連結串列的指標指向再好不過是元素的插入了。這裡要確保一個結點的三個指標:parent、left、right都要更新了。書上沒說具體的方法論,如果讓我們在紙上寫個左旋,估計好多人都要跪,因為指標指來指去,沒有思路完全不行。根據我的經驗,總結這樣的一個規律(僅供參考):

就拿左旋作為例子,如下圖所示:

規律可以總結成3個字:補——>提——>降

注意:圖2由於紙張不夠的原因,程式碼沒寫全,見下面的程式碼部分:

附上左旋的程式碼(C++模板類):

複製程式碼
 1 //左旋
 2 template<typename TKey, typename TValue> 
 3 void RBTree<TKey, TValue>::_LeftRotate( RBTreeNode *x_node )
 4 {
 5     //assert
 6     if ( !(x_node->isValid() && x_node->Right->isValid()) )
 7         throw exception( "左旋操作要求對非哨兵進行操作,並且要求右孩子也不是哨兵" );
 8 
 9     RBTreeNode *y_node = x_node->Right;
10 
11     //以下三步的宗旨是用 y 替換 x,注意將 x 的Parent、Left、Right都換成 y 的
12     // 1) x 和 y 分離 (補)
13     x_node->Right = y_node->Left;
14     if (y_node->Left != m_pNil)
15         y_node->Left->Parent = x_node;
16     
17     // 2) 設定y->Parent (提)
18     y_node->Parent = x_node->Parent;
19     if (x_node->Parent == m_pNil)
20         m_pRoot = y_node;
21     else if (x_node->Parent->Left == x_node)
22         x_node->Parent->Left = y_node;
23     else
24         x_node->Parent->Right = y_node;
25 
26     // 3) 設定y->Left (降)
27     y_node->Left = x_node;
28     x_node->Parent = y_node;
29 }
複製程式碼

關於刪除修復的”雙重黑、紅+黑“:

  如何理解?這個地方,書上沒說明白,在說這個意思之前,我們先來看看紅黑樹的刪除修復究竟是怎麼個回事?

  紅黑樹的刪除務必不能破壞了紅黑樹的5條性質,但這是不可能的,如果刪除的結點破壞了5條中任何一條性質,這個時候就需要採用措施進行修復,我們分析一下:刪除什麼結點會破壞性質,破壞哪條性質?

1)如果刪除的是紅色結點,則無影響;

2)如果刪除的是黑色結點,則不用想,第5條性質破壞了,其中:

  a)如果這個黑色結點是根結點,同時根結點的非空子結點,即將要替換它的結點為紅色,則破壞性質2;

  b)如果這個黑色結點的父結點和非空子結點都為紅色,則破壞性質4。

知道了這點,我們再來看下什麼是”雙重黑、紅+黑“,其實,這個說法主要是一種假設,假設存在著這樣的節點,那麼紅黑樹的性質就滿足了,但實際上這樣的結點是不存在的,所以需要轉換,而轉換的過程就是修復的過程。說白了,這個假設是為了便於程式碼實現,為了方便完成四種修復操作的一個假設性規律。因為刪除修復不像插入修復那麼明顯,有了它就像找到什麼訣竅一樣,刪除的四種修復不用”強制性記憶“就能明白為什麼要這樣做^-^。

紅黑樹的刪除與二叉搜尋樹的刪除基本一樣,不同之處在於需要記錄替換被刪結點到那個結點,然後以它為根進行修復。”雙重黑、紅+黑“就體現在這裡,如下兩圖所示:

  其中,delete結點被其後繼結點 x (這裡兩種情況:一是後繼就是delete的右孩子,二是比delete大的最小結點)替換。需要修復的條件是:刪除結點得是黑色,如果 x 也是黑色,則稱為”雙黑“;如果 x 是紅色,則稱為”紅黑“。好了,知道了這點,在對照著刪除修復的四種情況看,就很容易懂了,其修復的過程就是看 x 的顏色情況和 x的兄弟結點的顏色情況,有雙重黑的,就去掉一重黑,使之平衡,四種情況分別有不同的去重情況,整個過程是很好理解的,具體的細節就不做贅述,想必知道這點,整個刪除修復就很好理解了。

  這一章我覺得難點就在於刪除修復,插入修復是比較容易想到的,然後我認為需要著重注意的地方都記錄下來了,下面貼上自己寫的基於C++模板的程式碼,有點長。

1)red_black_tree.h

View Code

2)red_black_tree.cpp

View Code

參考資料:

《演算法導論》第三版

《STL原始碼剖析》

stay hungry stay foolish ----jobs希望多多燒香!