前面我們提到了二叉查找樹,支持快速的查找、插入和刪除操作。中序遍歷二叉查找樹,可以輸出有序的數據序列,非常高效。
但是,二叉查找樹存在一個問題,一般情況下二叉查找樹的搜索、插入、刪除的復雜度等于樹高,時間復雜度為 ,不過在頻繁的插入、刪除過程中,可能會出現樹的高度遠大于
的情況,導致各種操作效率急劇下降,最壞的情況下,二叉樹會退化為鏈表,時間復雜度為
。
由于這個原因,所以出現了很多改進版的平衡二叉樹查找樹,比如 AVL 樹,紅黑樹等。
工程中,紅黑樹用的最多,這也是我們為什么以紅黑樹為代表來介紹。
平衡二叉查找樹
平衡二叉查找樹是改進版的二叉查找樹。一般的二叉查找樹的查詢復雜度取決于目標節點到樹根節點的距離,即深度。因此當目標節點的深度普遍較大的時候,查詢的平均復雜度會上升。為了實現更高效率的查詢,誕生了平衡二叉查找樹。
那么平衡二叉查找樹是怎樣提高查詢效率的呢,平衡二叉查找樹規定,樹中任意節點對應的兩棵子樹的最大高度差為1,因此它也被稱為高度平衡樹。查找、插入和刪除在平均和最壞情況下的時間復雜度都是 。例如上面提到的 AVL 樹,它就是嚴格符合上面的這個定義。
但也有一些平衡二叉樹并沒有嚴格符合上面的定義(樹中任意節點對應的兩棵子樹的最大高度差為1),比如我們今天要介紹的紅黑樹,它從根節點到各個葉子節點的最長路徑,有可能會比最短路徑大一倍。
平衡二叉查找樹這種二叉樹之所以出現,是為了解決普通二叉查找樹在頻繁的插入,刪除操作時,時間復雜度退化的問題。所以最終能夠解決這個問題就好,并不一定要嚴格符合平衡二叉查找樹的定義。所謂“平衡”,簡單來說就是讓整棵樹左右子樹比較“平衡”,不要出現相差很大的情況。這樣樹的高度就能相對小一些,對應的各個操作的效率就會高一些。
紅黑樹
顧名思義,之所以叫紅黑樹,是因為它的每個節點都帶有一個顏色屬性,顏色為紅色或黑色。另外還有如下額外的要求:
- 根節點是黑色
- 節點是紅色或黑色
- 所有葉子節點都是黑色,葉子節點為 NIL 節點,不儲存數據
- 每個紅色節點必須有兩個黑色的子節點。(從每個葉子到根的所有路徑上不能有兩個連續的紅色節點。)
- 從任一節點到其每個葉子的所有簡單路徑都包含相同數目的黑色節點。
下圖是一個紅黑樹的示例:
這些約束確保了紅黑樹的關鍵特性:從根到葉子的最長的可能路徑不多于最短的可能路徑的兩倍長。結果是這個樹大致上是平衡的。因為操作比如插入、刪除和查找某個值的最壞情況時間都要求與樹的高度成比例,這個在高度上的理論上限允許紅黑樹在最壞情況下都是高效的。
紅黑樹相對于AVL樹來說,犧牲了部分平衡性以換取插入/刪除操作時少量的旋轉操作,整體來說性能要優于AVL樹。
操作
因為每一個紅黑樹也是一個特化的二叉查找樹,因此紅黑樹上的只讀操作與普通二叉查找樹上的只讀操作相同。但是,在紅黑樹上進行插入操作和刪除操作會導致不再符合紅黑樹的性質。所以我們會在執行插入和刪除的時候對節點顏色進行修改和樹旋轉,用來保證紅黑樹的性質。
顏色修改很簡單,就是在紅黑兩種顏色之間變化,那么樹旋轉是什么呢?樹旋轉是對二叉樹的一種操作,不影響元素的順序,但會改變樹的結構,將一個節點上移、一個節點下移。從旋轉上的不同又分為左旋轉和右旋轉。我們通過圖來看一下:
理解樹旋轉過程的關鍵,在于理解其中不變的約束。旋轉操作不會導致葉節點順序的改變(可以理解為旋轉操作前后,樹的中序遍歷結果是一致的),旋轉過程中也始終受二叉搜索樹的主要性質約束:右子節點比父節點大、左子節點比父節點小。
插入操作
我們來看一下插入數據的過程。
我們首先以二叉查找樹的方法增加節點并標記它為紅色。(如果設為黑色,就會導致根到葉子的路徑上有一條路上,多一個額外的黑節點,這個是很難調整的。但是設為紅色節點后,可能會導致出現兩個連續紅色節點的沖突,那么可以通過顏色調換(color flips)和樹旋轉來調整。)
其中有兩種特殊的情況:
- 如果插入節點的父節點是黑色的,那我們什么都不用做,它仍然滿足紅黑樹的定義。
- 如果插入的節點是根節點,那我們直接改變它的顏色,把它變成黑色就可以了。
除此之外,其他的插入操作都會破壞紅黑樹的性質,我們都需要通過改變顏色和樹旋轉進行調整。
紅黑樹的平衡調整過程是一個迭代的過程。我們把正在處理的節點叫做關注節點。關注節點會隨著不停地迭代處理,而不斷發生變化。最開始的關注節點就是新插入的節點。
新的節點插入之后,如果紅黑樹的性質被破壞,一般會有三種情況,我們只需要根據每種情況,做出對應的調整,使其繼續符合紅黑樹的性質即可。我們來具體看一下三種情況的調整過程。在此之前我們,我們將使用術語叔父節點來指一個節點的父節點的兄弟節點,父節點的父節點叫做祖父節點。
情況一:關注節點 a,它的叔父節點 d 是紅色
- 將 a 的父節點b,叔父節點 d 的顏色都設置為黑色;
- 將 a 的祖父節點 c 的顏色設置為紅色;
- 關注節點由 a 變成 c;
- 跳轉到情況二或者情況三。
情況二:關注節點是 a,它的叔父節點 d 是黑色,關注節點 a 是其父節點 b 的右子節點
- 關注節點變成節點 a 的父節點 b;
- 圍繞新的關注節點 b 左旋;
- 跳到情況三。
情況三:關注節點是 a,它的叔父節點 d 是黑色,關注節點 a 是其父節點 b 的左子節點
- 圍繞關注節點 a 的祖父節點 c 右旋;
- 將關注節點 a 的父節點 b、兄弟節點 c 的顏色互換。
- 調整結束。
刪除操作
紅黑樹的插入操作還不是很困難,但刪除操作就困難多了。
刪除操作的平衡調整分為兩步,第一步是針對刪除節點初步調整。初步調整只是保證整棵紅黑樹在一個節點刪除之后,仍然滿足最后一條定義的要求,也就是說,每個節點,從該節點到達其可達葉子節點的所有路徑,都包含相同數目的黑色節點;第二步是針對關注節點進行二次調整,讓它滿足紅黑樹的第四條定義, 每個紅色節點必須有兩個黑色的子節點。(從每個葉子到根的所有路徑上不能有兩個連續的紅色節點。)
1. 針對刪除節點初步調整
這里需要注意一下,紅黑樹的定義中“只包含紅色節點和黑色節點”,經過初步調整之后,為了保證滿足紅黑樹定義的最后一條要求,有些節點會被標記成兩種顏色,“紅 - 黑”或者“黑 - 黑”。如果一個節點被標記為了“黑 - 黑”,那在計算黑色節點個數的時候,要算成兩個黑色節點。
情況一:如果要刪除的節點是 a,它只有一個子節點 b
- 刪除節點 a,并且把節點 b 替換到節點 a 的位置,這一部分操作跟普通的二叉查找樹的刪除操作一樣;
- 節點 a 只能是黑色,節點 b 也只能是紅色,其他情況均不符合紅黑樹的定義。這種情況下,我們把節點 b 改為黑色;
- 調整結束,不需要進行二次調整。
情況二:如果要刪除的節點 a 有兩個非空子節點,并且它的后繼節點就是節點 a 的右子節點 c。
- 如果節點 a 的后繼節點就是右子節點 c,那右子節點 c 肯定沒有左子樹。我們把節點 a 刪除,并且將節點 c 替換到節點 a 的位置。這一部分操作跟普通的二叉查找樹的刪除操作無異;
- 然后把節點 c 的顏色設置為跟節點 a 相同的顏色;
- 如果節點 c 是黑色,為了不違反紅黑樹的最后一條定義,我們給節點 c 的右子節點 d 多加一個黑色,這個時候節點 d 就成了“紅 - 黑”或者“黑 - 黑”;
- 這個時候,關注節點變成了節點 d,第二步的調整操作就會針對關注節點來做。
情況三:如果要刪除的是節點 a,它有兩個非空子節點,并且節點 a 的后繼節點不是右子節點
- 找到后繼節點 d,并將它刪除,刪除后繼節點 d 的過程參照 CASE 1;
- 將節點 a 替換成后繼節點 d;把節點 d 的顏色設置為跟節點 a 相同的顏色;
- 如果節點 d 是黑色,為了不違反紅黑樹的最后一條定義,我們給節點 d 的右子節點 c 多加一個黑色,這個時候節點 c 就成了“紅 - 黑”或者“黑 - 黑”;
- 這個時候,關注節點變成了節點 c,第二步的調整操作就會針對關注節點來做。
2. 針對關注節點進行二次調整
經過初步調整之后,關注節點變成了“紅 - 黑”或者“黑 - 黑”節點。針對這個關注節點,我們再分四種情況來進行二次調整。二次調整是為了讓紅黑樹中不存在相鄰的紅色節點。
情況一:如果關注節點是 a,它的兄弟節點 c 是紅色的
- 圍繞關注節點 a 的父節點 b 左旋;
- 關注節點 a 的父節點 b 和祖父節點 c 交換顏色;
- 關注節點不變;
- 繼續從四種情況中選擇適合的規則來調整。
情況二:如果關注節點是 a,它的兄弟節點 c 是黑色的,并且節點 c 的左右子節點 d、e 都是黑色的
- 將關注節點 a 的兄弟節點 c 的顏色變成紅色;
- 從關注節點 a 中去掉一個黑色,這個時候節點 a 就是單純的紅色或者黑色;
- 給關注節點 a 的父節點 b 添加一個黑色,這個時候節點 b 就變成了“紅 - 黑”或者“黑 - 黑”;
- 關注節點從 a 變成其父節點 b;
- 繼續從四種情況中選擇符合的規則來調整。
情況三:如果關注節點是 a,它的兄弟節點 c 是黑色,c 的左子節點 d 是紅色,c 的右子節點 e 是黑色
- 圍繞關注節點 a 的兄弟節點 c 右旋;節點 c 和節點 d 交換顏色;
- 關注節點不變;
- 跳轉到情況四,繼續調整。
情況四:如果關注節點 a 的兄弟節點 c 是黑色的,并且 c 的右子節點是紅色的
- 圍繞關注節點 a 的父節點 b 左旋;
- 將關注節點 a 的兄弟節點 c 的顏色,跟關注節點 a 的父節點 b 設置成相同的顏色;
- 將關注節點 a 的父節點 b 的顏色設置為黑色;
- 從關注節點 a 中去掉一個黑色,節點 a 就變成了單純的紅色或者黑色;
- 將關注節點 a 的叔父節點 e 設置為黑色;
- 調整結束。
總結
紅黑樹的插入和刪除操作每一步都伴隨著調整操作,目的就是為了讓其繼續滿足紅黑樹的定義。從而保證整體的性能。