数据结构-红黑树
本文由@呆代待殆原創,轉載請注明出處:http://www.cnblogs.com/coffeeSS/
?
學習紅黑樹的前置技能:二叉搜索樹http://www.cnblogs.com/coffeeSS/p/5452719.html
?
紅黑樹簡介
紅黑樹是一種自平衡(平衡指所有葉子的深度趨于相等)二叉查找樹,它是在1972年由1972年由魯道夫·貝爾發明,這個數據結構對插入時間、刪除時間和查找時間提供了最好可能的最壞情況擔保(也就是說同樣都在最壞情況下,他的速度比一般的樹的速度要快)無論是查找,插入還是刪除都可以在O(lgn)內完成,所以很適合用在時間敏感的場合中。
?
紅黑樹的性質
紅黑樹就是有顏色的二叉查找樹,并且要滿足以下5個約束:
1,節點不是紅色就是黑色。
2,根節點是黑色。
3,所有葉子節點都是黑色(這里所有葉子節點指的是NIL節點)。
4,每個紅色節點的兩個孩子都是黑色。
5,任意一個節點到其每個葉子的所有簡單路徑(簡單路徑指路徑中不會出現重復節點)都包含相同數目的黑色節點。
?
紅黑樹的每個節點至少包含5個屬性:color、key、left、right與parent。
如果一個節點沒有子節點或者父節點則相應的指針設為null或者指向NIL節點。
根節點是唯一一個parent為NIL的節點。
紅黑樹的所有葉子節點都是NIL節點,但是在很多實例圖中這些NIL節點都會被省略,而且實際實現中,如果真的實現NIL節點,一般只會設置一個NIL節點,讓所有指向葉子節點的節點都指向這個節點,根節點的parent指針也會指向這個節點(當然你簡單的將這些指向NIL節點的指針都設成null也是可以的,但是本文的代碼實現是基于NIL節點的,因為這樣代碼寫起來更自然一點)。
NIL節點的作用是充當哨兵和完善紅黑樹的結構,它的color屬性為black,其他屬性都沒有實際意義,設為null或者不管都可以。
下面看一張來自WIKI百科的紅黑樹的圖例
上面的約束確定了一個紅黑樹的關鍵特性:從根到葉子的最長的可能路徑不多于最短可能路徑的兩倍。 證明這個特性的正確性:因為由約束4可知,不可能存在兩個相鄰的紅色節點,這個結論再配合約束5可知最短的路徑一定是全黑的路徑,設這個路徑上有n個黑節點,那么最長的路徑也一定有n個黑節點,而且最長路徑的節點數減去最短路徑的節點數就是最長路徑中紅色節點的數目,那么我們只有在n個黑色節點中盡可能多的插入紅色節點就行了,在這5個約束下,我們只能讓紅黑節點交替出現,而且因為根節點和葉子節點都是黑色的,所以我們最多只能插入n-1個節點,所以當最短路徑有n個節點時,最長路徑最多只有2n-1個節點,完全滿足上述特性。 本文紅黑樹的結構 本文將實現以下包含以下結構和方法的紅黑樹(C++) 1 enum Color{black,red}; 2 3 struct Node{ 4 Node(){ color = black; } 5 Node(int k) :key(k){ color = red; } 6 Node* parent = nullptr; 7 Node* left = nullptr; 8 Node* right = nullptr; 9 Color color; 10 int key; 11 }; 12 13 class MyRBT{ 14 private: 15 Node* nil=new Node(); 16 Node* root=nil; 17 void fix_color(Node* n);//插入過程中有可能遞歸的部分 18 Node* getUncle(Node* n);//返回n的叔節點 19 void left_rotate(Node* n);//左旋 20 void right_rotate(Node* n);//右旋 21 Node*& parentToN(Node* n);//獲得n的父節點里指向n的那個指針 22 Node* getSibling(Node* n);//返回n的兄弟節點 23 bool isParentLeftChild(Node* n);//判斷n是不是父節點的左孩子 24 void delete_fix_color(Node* n);//刪除過程中有可能遞歸的部分。 25 public: 26 MyRBT(){}; 27 MyRBT(vector<int> v); 28 Node* getRoot(){ return root; } 29 void insertNode(int k); 30 void deleteNode(int k); 31 Node* findByKey(int k); 32 Node* findSuccessor(int k); 33 Node* findPredecessor(int k); 34 void traversal(Node* root, void(*f)(int k) = [](int k)->void{cout << k << " "; });//遍歷輸出所有節點 35 void insertNode(Node* n); 36 void deleteNode(Node* m); 37 Node* findSuccessor(Node* n);//找n的后繼 38 Node* findPredecessor(Node* n);//找n的前驅 39 };?
其中除了插入和刪除操作,其他操作和二叉搜索樹沒什么區別,只是這里的版本都是基于NIL節點實現的,而之前的二叉搜索樹那篇是基于nullptr實現的,本質上沒有任何區別,所以本文重點介紹插入刪除操作。
紅黑樹的操作 紅黑樹的只讀操作和普通的二叉查找樹沒有區別(只讀操作包括查找,遍歷等),但是插入和刪除操作和普通的二叉搜索樹是不一樣的,因為紅黑樹要始終保持上述5個約束不被破壞,如果在插入或者刪除某個節點的時候破壞了紅黑樹的約束,我們可以通過旋轉操作來調整這棵樹讓它重新滿足這5個約束,那么再我們介紹插入和刪除前先讓我們來看一看什么是旋轉操作吧。 旋轉操作 如圖是一張來自《算法導論》的圖示,從上面可以直接看出,旋轉操作分為左旋和右旋,并且他們為互逆的操作。左旋(left_rotate):在x節點上進行左旋操作時假設他的右孩子(y節點)不是NIL節點,并且x節點也不是NIL節點,左旋結束后,如圖,x節點成為y節點的做孩子,y節點取代了x節點的位置,其實仔細看一下就知道,左旋就是原本指向x節點的指針指向了x節點的右孩子,相當于整個以x為根節點的子樹向左轉動了一下而已,是一個相當簡單的操作。 右旋(right_rotate):假設右旋節點的左孩子和這個節點不為NIL節點,其他和上述左旋類似,不再贅述。 再給出左旋,右旋的代碼前,我們先給出這個方法的代碼?Node*& parentToN(Node* n);//獲得n的父節點里指向n的那個指針? 取得n節點的父節點比較容易,但是再從父節點取到n節點需要確定n節點是父節點的左孩子還是右孩子,因為我們經常要讓n節點的父節點指向n的指針指向一個新的節點,所以我們封裝一下這個方法比較好,這個方法返回的是左值,可以直接對其賦值。 1 Node*& MyRBT::parentToN(Node* n){ 2 if (n->parent == nil) return nil; 3 if (n->parent->left == n) 4 return n->parent->left; 5 else return n->parent->right; 6 }
?然后我們給出左旋和右旋操作的代碼,結合上面的圖來理解比較好。
1 void MyRBT::left_rotate(Node* n){ 2 if (n->right == nil)//設n的右孩子為y 3 cout << "left_rotate error" << endl; 4 parentToN(n) = n->right;//修改n的父節點指向y 5 n->right->parent = n->parent;//修改y的parent指針 改y->parent 為 n->parent 6 n->parent = n->right;//修改n的parent指針 改n->parent 為 n->right 7 n->right = n->right->left;//將y的左子樹變成n的右子樹 改n->right 為 y-left 8 n->parent->left = n;//將n改成y的左孩子 ,這個時候取原來n的右孩子不能通過n->right,因為上一句中已經改變了r->right,但是看到上上一句就知道n->parent現在指向n的原先的右孩子 改y->left 為 n 9 } 10 void MyRBT::right_rotate(Node* n){ 11 if (n->left == nil) 12 cout << "right_rotate error" << endl; 13 parentToN(n) = n->left; 14 n->left->parent = n->parent; 15 n->parent = n->left; 16 n->left = n->left->right; 17 n->parent->right = n; 18 }?
插入操作 給紅黑樹插入節點時,插入的節點會標為紅色(因為如果你插入的是黑色節點,你就會破壞約束5,直接插入紅色節點可以避免破壞這個約束,減小了破壞約束的概率),但是插入后,仍然有其他約束可能被破壞,歸納一下有5種情況需要我們來處理 注:插入節點我們用n來指代,其父節點用p來指代,叔節點用u來指代,祖父節點用g來表示。 1,n是根節點(破壞了約束2). 2,n的p節點是黑色。 3,n的p節點和u節點都是紅色。 4,n是g節點的左孩子的右孩子,或者是g節點的右孩子的左孩子。(p是紅的,而u是黑的) 5,n是g節點的左孩子的左孩子,或者是g節點的右孩子的右孩子。(p是紅的,而u是黑的) 對于這五種情況需要依次判斷。 情況一:這種情況很簡單,只要把n染成黑色的就解問題了。 情況二:這種情況下,沒有任何規則被違反,不需要處理。 情況三:這種情況下,我們會違反約束4,所以我們要將p與u染成黑色,并且將g染成紅色,這樣就解決了局部問題,但是這樣也許會導致g節點違反約束,此時的g節點就像是剛插入的新節點一樣需要從情況一開始判斷是否違反約束。 情況四:如果n是g節點的左孩子的右孩子,那么我們對n進行左旋,如果n節點是g節點的右孩子的左孩子,那么我們對p進行右旋,這是情況四會轉換成情況五,然后我們就用情況五去解決剩下的問題(因為此時約束4仍然不滿足) 情況五:如果n是g節點的左孩子的左孩子,那么我們對g節點右旋,如果n節點是g節點的右孩子的右孩子,那么我們對g節點左旋,然后將此時的根節點染成黑色(如果是紅色會違反約束四),將根節點的兩個孩子染成紅色,此時樹就被修復了。 這就是插入新節點需要考慮的問題。那么我們接下來集中解決一些相關問題。 問題一:為什么只有這些情況(注意插入的操作前半部分是和二叉搜索樹一樣的)。 答:首先一個節點插入要么他在根上,那么就是情況一;要么他的深度為2,只有p節點(此時p是跟節點只能是黑的),沒有g節點,這是情況二;如果深度大于二,那么首先考慮父節點那一層,如果都是黑的,那么可以當做情況二來處理,如果p是黑的,u是紅的,也可以當做情況二處理;如果p是紅的u是黑的那么進入情況四與情況五,如果p與u都是紅色的,這是情況三。 問題二:為什么分情況的時候不考慮g上面的其他節點呢? 答:太上面的節點我們已經是規范的了,如果我們能解決g節點一下的不規范,g節點以上是不用管的,但是如果我們改變了g節點的顏色,我們就要對g節點進行上述判斷,這是情況三中的一部分。 問題三:為什么不考慮g節點的顏色呢? 答:g節點的顏色我們不需判斷(g一定是黑的,因為p是紅的),理由如下,涉及到g節點的過程情況是三到五,在情況三中,我們一定會把g節點染成紅色并把它當成剛插入的節點來重新判斷,所以g節點原本是什么顏色不重要;如果判斷流程進入情況四,那么一定會進入情況五,而情況四種沒有針對g的操作,所以g的顏色對過程四也不重要,情況五中,g的孩子節點一定會取代g節點的位置,而且這個新的根節點會被染成黑色,之后g節點也一定會被我們染成紅色,所以g的顏色對情況2五也不重要。 問題四:情況五中,為什么我們要將新的根節點染黑而不通過調整子節點的顏色來規范紅黑樹呢? 答:因為之前的根節點一定是黑色,如新的根節點顏色改變,那么我們不得不對新的節點重新判斷,看它是否也破壞了紅黑樹到的約束,所以將新的根節點涂成黑色,可以將矛盾控制在內部,方便解決。 問題五:有全黑的紅黑樹么? 答:有啊,只有一個根節點的時候,其他情況,你可以自己試試看= = 下面給出插入方法的實現,插入的過程和二叉搜索樹差不多,不再寫注釋,不同的部分在于用NIL節點代替了nullptr,并增加了修正顏色用的方法?void fix_color(Node* n);//插入過程中修正顏色的方法?對這個方法的理解是重點所在。 首先再給出一個輔助方法?Node* getUncle(Node* n);//返回n的叔節點? 1 Node* MyRBT::getUncle(Node* n){ 2 if (n->parent != nil&&n->parent->parent != nil){ 3 if (n->parent->parent->left == n->parent) 4 return n->parent->parent->right; 5 else return n->parent->parent->left; 6 } 7 return nullptr; 8 }?
下面是插入算法本體和fix_color方法 1 void MyRBT::insertNode(int k){ 2 insertNode(new Node(k)); 3 } 4 5 void MyRBT::insertNode(Node* n){ 6 Node* temp = root; 7 if (temp == nil){ 8 root = n; 9 root->parent = nil;//root的父節點指向nil 10 root->left = nil; 11 root->right = nil; 12 fix_color(n); 13 return; 14 } 15 while (true){ 16 if (temp->key > n->key){ 17 if (temp->left != nil) 18 temp = temp->left; 19 else{ 20 temp->left = n; 21 n->parent = temp; 22 n->right = nil; 23 n->left = nil; 24 fix_color(n); 25 return; 26 } 27 } 28 else{ 29 if (temp->right != nil) 30 temp = temp->right; 31 else{ 32 temp->right = n; 33 n->parent = temp; 34 n->right = nil; 35 n->left = nil; 36 fix_color(n); 37 return; 38 } 39 } 40 } 41 } 42 43 //nil節點的存在是為了保證數結構的完整性和規范性 44 45 void MyRBT::fix_color(Node* n){ 46 if (n->parent == nil)//情況一 47 n->color = black; 48 else if (n->parent->color == black){//情況二 49 //無需任何處理 50 } 51 else if (getUncle(n)->color == red){//情況三 52 n->parent->color = black; 53 getUncle(n)->color = black; 54 n->parent->parent->color = red; 55 fix_color(n->parent->parent); 56 } 57 else { 58 if (n == n->parent->parent->left->right){// 頭兩個if是同時進行了情況四和情況五,這樣寫是因為旋轉操作次數不 59 left_rotate(n->parent); //一樣,各個節點的位置關系不一樣,不能把這兩個過程完全分開,看看后面的 60 right_rotate(n->parent); //顏色處理就會明白。 61 n->color = black; 62 n->left->color = red; 63 n->right->color = red; 64 } 65 else if (n == n->parent->parent->right->left){ 66 right_rotate(n->parent); 67 left_rotate(n->parent); 68 n->color = black; 69 n->left->color = red; 70 n->right->color = red; 71 } 72 else if (n == n->parent->parent->left->left){//這兩個if是沒有經過情況四直接進行情況五的時候。 73 right_rotate(n->parent->parent); 74 n->parent->color = black; 75 n->parent->left->color = red; 76 n->parent->right->color = red; 77 } 78 else { 79 left_rotate(n->parent->parent); 80 n->parent->color = black; 81 n->parent->left->color = red; 82 n->parent->right->color = red; 83 } 84 } 85 }?
插入算法的特點: 1,旋轉操作不會超過兩次。 2,插入算法的遞歸次數不會大于此節點深度的二分之一。 3,插入算法的最壞時間復雜度為O(lgn) 以上就是插入的所有問題。 下面讓我們來看看刪除的時候會碰到的問題。 刪除操作 刪除算法比較復雜,我們來慢慢分析,請保持耐心。 首先當被刪除的節點M有兩個非NIL孩子的時候,我們會像二叉搜索樹那樣找M節點的(前驅/后繼)來代替M(代替M時,我們只是改變了M的key值,并沒有改變任何節點的顏色,所以沒有破壞紅黑樹的性質),然后刪除這個(前驅/后繼)(這時紅黑樹的性質可能被破壞),這時,我們刪除的這個(前驅/后繼)節點是只有一個非NIL孩子的,所以刪除有兩個非NIL孩子節點的問題可以當成刪除有一個非NIL孩子節點的問題。 那么接下來我們的討論都會針對最多只有一個非NIL孩子的節點 我們稱被刪除的節點為M,如果M有一個非NIL節點的孩子,那么我們叫這個節點C,如果M只有兩個NIL節點孩子,隨便選一個叫C就好。 情況一:M是紅節點時,此時M一定有兩個NIL節點的孩子(證明:考慮如果有一個非NIL節點的孩子C,那么C一定是黑色的,這時不管C節點后面都有什么節點,經過C節點的路徑的黑色節點的個數一定比到達M節點另一個孩子節點的路徑多,這時違反紅黑樹規定的,M為紅色時,只可能有兩個NIL孩子節點),此時我們只要用C節點替換M節點就行了。 情況二:M是黑色節點,且有一個紅色節點的孩子C,這時情況也很簡單,我們只需要刪掉M并用C節點代替它,再把C節點染成黑色就好了,從顏色上看,我們只是減少了一個紅色節點,不會破壞任何性質,從結構上看,我們成功刪除了M節點,達到了目的,也很好操作。情況三:當M與C都是黑色節點時,情況就比較復雜了(這種情況只可能時M有兩個NIL孩子節點時,假設其中有一個是非NIL孩子節點那么它的子樹中一定有黑色節點,這就會導致不是每條路徑的黑色節點數相同,所以M與C都是黑色時只能是M有兩個NIL孩子節點時)這時我們先用任意一個M的孩子當做C節點來進行接下來的操作。
現在我們來確定在情況三下,若是刪除了M節點會有什么影響,由于我們用C節點代替了M節點,將M節點移除了,所以經過C節點的路徑會比沒有經過C節點的路徑少一個黑色節點,所以只要我們想辦法把經過C節點的路徑的黑色節點數增加一個就可以了,那么現在我們來進行這個工作,第一步,我們先來做一下相關節點的命名區分。 我們定義當C節點替換完了M節點后,我們重新命名這個C節點為N節點(new的意思啦),N的兄弟節點命名為S(也就是原來M的父節點的另一個孩子),N的父節點為P(也就是之前M的父節點)。
下面我們就來討論情況三下,當節點呈現各種狀態的時候我們應該如何面對,在這里我們先假定N是p的左孩子(如果N是P的右孩子,下述相關操作的很多左與右的關系會替換,但是過程和思想是一樣的)。 經過前人的歸納,一共將這個修復的過程劃分成了6個狀態,每一次刪除M節點后我們都要對N進行這個六個狀態的依次檢查,規則如下。 開始:首先判斷是否符合狀態一的描述,符合則進入狀態一,不符合則跳過狀態一,判斷是否滿足狀態二,不滿足就跳過狀態二的描述,就根據狀態二的后續部分進行接下來的操作。
狀態一:
描述:N是根節點。
操作:什么都不用做了,因為樹空了。
???????? 影響:無。
???????? 后續:退出。
?
狀態二:
描述:S節點是紅色。
操作:將P和S的顏色交換,然后對P節點進行左旋(到此樹的結構還是沒有正常之后還要經過4,5 or 6,同樣接下來的操作還是通過N節點來定位)。
影響:
結構:因為進行了左旋所以有很多變化詳細請看圖。
顏色:各個路徑上的黑色節點數沒有變化,但是S(左旋后的位置)的左子樹的紅色節點多了一個,而右子樹的紅色節點少了一個。
后續:判定現在的狀態決定進入狀態三、四、五或者六中的一個。
?
狀態三:
描述:P、S、S的孩子都是黑色。
操作:我們直接把S染成紅色。
影響:
???????? 結構:無變化。
???????? 顏色:P的右子樹路徑上的黑色節點數少了一個,而紅色節點多了一個。
后續:這樣一來經過P節點的路徑就會比不經過P節點的路徑少一個黑色節點(P節點以下的顏色矛盾已經解決),P節點現在的處境和之前的N節點一模一樣,所以我們對P進行顏色的修正,進行遞歸調用。
?
?
狀態四:
描述:S和S的孩子是黑色的,但是P是紅的。
操作:那么我們就將S和P的顏色交。
影響:
結構:無變化
顏色:P的左子樹路徑黑色節點數量加一,P的右子樹路徑黑色節點數量不變
后續:狀態修復退出。
?
?
狀態五:
描述:S是黑色,S的左孩子是紅的,S的右孩子是黑的。
操作:我們對S進行右旋,然后交換S和他的新父節點的顏色。
影響:
結構:由于進行了右旋所以結構變化很大,請直接看圖。
顏色:就黑色節點而言沒有任何一條路徑有改變,但是,紅色節點的位置發生了變化,N節點現在有一個有紅色節點當右孩子的黑色兄弟。
后續:進入狀態六。
?
?
狀態六:
描述:S是黑色的,S的右節點是紅色的。
操作:對P進行左旋,然后交換P和S的顏色,并將S的右孩子變成黑色
影響:
結構:由于進行了左旋所以結構變化很大,請直接看圖。
顏色:S(左旋后)的右子樹路徑的黑色節點的數目不變,左子樹路徑的黑色節點增加了一個。
后續:狀態修復退出。
?
?
這樣來看的話我們一共有8條可能的路徑來修復(假設1-6代表進行狀態1-6對應的操作)
路徑:
1(問題解決)
3(解決局部問題,向上傳遞到狀態1)
2,4(問題解決)
2,5,6(問題解決)
2,6(問題解決)
4(問題解決)
5,6(問題解決)
6(問題解決)
?
然后讓我們來假設N是父節點的右孩子,那么那些操作和情況會發生變化呢?(下面說的不會變化指的是上面的文字描述不會有變化)
狀態一,不會有變化。
狀態二,對P節點的左旋會變成右旋,其他操作不變。
狀態三,不會有變化。
狀態四,不會有變化。
狀態五,S的左孩子是黑色,S的右孩子是紅色,對S進行左旋,其他不變。
狀態六,S的左節點是紅色,對P進行右旋,將S的左孩子染成黑色,其他不變。
?
所以我們在寫判定條件和操作的時候要記得把N是左右孩子的情況都考慮進去,我們綜合一下應該是下面這樣。在M與C都是黑色的情況下
狀態一:
描述:N是根節點。
操作:什么都不用做了,因為樹空了。
狀態二:
描述:S節點是紅色。
操作:將P和S的顏色交換,然后如果N是P的左孩子則對P節點進行左旋。
???????????????????????????????????????? 如果N是P的右孩子則對P節點進行右旋。
狀態三:
描述:P、S、S的孩子都是黑色。
操作:我們直接把S染成紅色。
狀態四:
描述:S和S的孩子是黑色的,但是P是紅的。
操作:那么我們就將S和P的顏色交。
狀態五:
描述:S是黑色,S的左孩子是紅的,S的右孩子是黑的,N是P的左孩子。
???????? 或者
???????? S是黑色,S的左孩子是黑的,S的右孩子是紅的,N是P的右孩子。
操作:N是P的左孩子我們對S進行右旋。
?? 或者
?? N是P的右孩子我們對S進行左旋。
???????? 然后交換S和他的新父節點的顏色。
狀態六:
描述:S是黑色的,S的右節點是紅色的,N是P的左孩子。
???????? 或者
??????? S是黑色的,S的左節點是紅色的,N是P的右孩子。
操作:N是P的左孩子我們對P進行左旋,然后交換P和S的顏色,并將S的右孩子變成黑色。
?? 或者
?? N是P的右孩子我們對P進行右旋,然后交換P和S的顏色,并將S的左孩子變成黑色。
????????
?
現在讓我們來集中解決一些常見問題。
問題一:情況三中S節點不會是NIL節點么?
答:不會,可以用假設法,假設S是NIL節點,那么為了保證每條路徑上的黑色節點數相同,那么M節點必須是NIL節點,如果M是非NIL節點,則一定會導致這個非NIL節點的路徑上的黑色節點數多于經過S節點的路徑,而M在情況三中并不是NIL節點,M的兩個孩子才是。
?
問題二:N節點是NIL節點,刪除算法中把它當做普通節點對待沒有問題么?
答:NIL節點和null值是不同的,NIL節點是一個空的節點,它也具有節點的結構,但是在這里NIL里有意義的數據只有color,其他的數據都是沒有意義的,所以只要算法結束后紅黑樹的結構維持不涉及NIL節點中color以外的屬性就沒有問題。(如果你想用null來代替NIL節點實現算法也是可以的,但是很多細節需要有相應的修改,博主覺得還是用NIL節點比較方便)。
?
現在讓我們來看代碼實現,為了使代碼更簡潔我們再寫兩個輔助函數
?Node* getSibling(Node* n);//返回n的兄弟節點?
?bool isParentLeftChild(Node* n);//判斷n是不是父節點的左孩子?
1 bool MyRBT::isParentLeftChild(Node* n){ 2 if (n->parent == nil){ 3 cout << "isParentLeftChild error" << endl; 4 exit(0); 5 } 6 if (n->parent->left == n) return true; 7 else return false; 8 } 9 10 Node* MyRBT::getSibling(Node* n){ 11 if (n->parent == nil||n==nullptr) return nil; 12 if (n->parent->left == n) 13 return n->parent->right; 14 else return n->parent->left; 15 }?
因為刪除算法涉及到查找前驅和后繼,所以這個代碼也貼在這里
1 Node* MyRBT::findSuccessor(int k){ 2 return findSuccessor(findByKey(k)); 3 } 4 Node* MyRBT::findSuccessor(Node* n){ 5 if (n->right != nil){ 6 n = n->right; 7 while (n->left != nil) 8 n = n->left; 9 return n; 10 } 11 while (n->parent != nil&&n->parent->right == n) 12 n = n->parent; 13 return n->parent; 14 } 15 16 Node* MyRBT::findPredecessor(int k){ 17 return findPredecessor(findByKey(k)); 18 } 19 Node* MyRBT::findPredecessor(Node* n){ 20 if (n->left != nil){ 21 n = n->left; 22 while (n->right != nil) 23 n = n->right; 24 return n; 25 } 26 while (n->parent != nil&&n->parent->left == n) 27 n = n->parent; 28 return n->parent; 29 }?
現在讓我們來看真正的刪除算法吧 1 void MyRBT::deleteNode(int k){ 2 deleteNode(findByKey(k)); 3 } 4 5 void MyRBT::deleteNode(Node* m){ 6 while(m->left != nil&&m->right != nil){//這個循環就是不斷的找前驅(找后繼也是可以的)來代替要被刪除的節點,直達某個前驅最多只有一個非NIL節點的孩子 7 Node* temp = findPredecessor(m); 8 m->key = temp->key; 9 m = temp; 10 } 11 if (m->color == red) //情況一 12 parentToN(m) = nil; 13 else if (m->left->color == red || m->right->color == red){//情況二 14 if (m->left != nil){ 15 m->left->parent = m->parent; 16 parentToN(m) = m->left; 17 m->left->color = black; 18 } 19 else{ 20 m->right->parent = m->parent; 21 parentToN(m) = m->right; 22 m->right->color = black; 23 } 24 } 25 else delete_fix_color(m);//進入最復雜的情況三 26 delete m;//處理完釋放被刪除的節點的空間 27 return; 28 } 29 30 void MyRBT::delete_fix_color(Node* n){ 31 Node* s = getSibling(n); 32 Node* p = n->parent; 33 parentToN(n) = nil; 34 nil->parent = n->parent; 35 n = nil; 36 if (n->parent == nil){//狀態一 37 //什么都不用做 38 } 39 else if (p->color == black&&s->color == black&&s->left->color == black&&s->right->color == black){//狀態三 40 s->color = red; 41 delete_fix_color(p); 42 } 43 else { 44 if (s->color == red){//狀態二 45 s->color = black; 46 p->color = red; 47 if (isParentLeftChild(n)) left_rotate(p); 48 else right_rotate(p); 49 } 50 if (s->color == black&&s->left->color == black&&s->right->color == black&&p->color == red){//狀態四 51 s->color = red; 52 p->color = black; 53 return; 54 } 55 else{//狀態五 56 if (s->color == black&&s->left->color == red&&s->right->color == black&&isParentLeftChild(n)){ 57 right_rotate(s); 58 s->color = red; 59 s->parent->color = black; 60 } 61 else if (s->color == black&&s->left->color == black&&s->right->color == red && (!isParentLeftChild(n))){ 62 left_rotate(s); 63 s->color = red; 64 s->parent->color = black; 65 } 66 //狀態六 67 if (s->color == black&&s->right->color == red&&isParentLeftChild(n)){ 68 left_rotate(p); 69 s->color = p->color; 70 p->color = black; 71 s->right->color = black; 72 } 73 else if (s->color == black&&s->left->color == red && (!isParentLeftChild(n))){ 74 right_rotate(p); 75 s->color = p->color; 76 p->color = black; 77 s->left->color = black; 78 } 79 } 80 } 81 }?
刪除算法的特點: 1,旋轉操作不會超過三次。 2,插入算法的遞歸次數不會大于此節點的深度。 3,插入算法的最壞時間復雜度為O(lgn)刪除操作到此結束。
?
其他操作只讀操作的代碼我也貼在這里,供大家參考(詳細的說明請參考二叉搜索樹那一篇) 構造方法 1 MyRBT::MyRBT(vector<int> v):MyRBT(){ 2 for (auto n : v){ 3 insertNode(n); 4 } 5 }通過key值查找特定節點
1 Node* MyRBT::findByKey(int k){ 2 Node* temp = root; 3 while (temp != nil){ 4 if (k == temp->key) 5 return temp; 6 temp = k < temp->key ? temp->left : temp->right; 7 } 8 cout << "can't find" << endl; 9 return nullptr; 10 }遍歷算法
1 void MyRBT::traversal(Node* root, void(*f)(int k)){ 2 if (root==nil)//當樹為空的時候root為nullptr 3 return; 4 traversal(root->left); 5 cout << root->key << " " << root->color << endl; 6 traversal(root->right); 7 }?
終于寫完了,撒花???,好長啊= =,如果覺得還是不夠清楚可以看看wiki百科上的解釋,鏈接在參考資料里,講的很好,一定要看英文的,中文版的翻譯的不全,省略了很多說明性的東西,反而更難懂了_(:зゝ∠)_。 祝各位學習愉快?(^?^*)~~~~。參考資料:
1,《算法導論 中文版》(英文版第三版)(美)ThomasH.Cormen,CharlesE.Leiserson,RonaldL.Rivest,CliffordStein 著;王剛,鄒恒明,殷建平,王宏志等譯。
2,WIKI百科https://en.wikipedia.org/wiki/Red%E2%80%93black_tree
轉載于:https://www.cnblogs.com/coffeeSS/p/5447929.html
總結
- 上一篇: 数组的循环右移问题(好未来笔试题)
- 下一篇: 用java api 实现查询 Hive