BST存在的主要問題是,數在插入的時候會致使樹傾斜,不一樣的插入順序會致使樹的高度不同,而樹的高度直接的影響了樹的查找效率。理想的高度是logN,最壞的狀況是全部的節點都在一條斜線上,這樣的樹的高度爲N。html
基於BST存在的問題,一種新的樹——平衡二叉查找樹(Balanced BST)產生了。平衡樹在插入和刪除的時候,會經過旋轉操做將高度保持在logN。其中兩款具備表明性的平衡樹分別爲AVL樹和紅黑樹。AVL樹因爲實現比較複雜,並且插入和刪除性能差,在實際環境下的應用不如紅黑樹。
紅黑樹(Red-Black Tree,如下簡稱RBTree)的實際應用很是普遍,好比Linux內核中的徹底公平調度器、高精度計時器、ext3文件系統等等,各類語言的函數庫如Java的TreeMap和TreeSet,C++ STL的map、multimap、multiset等。
RBTree也是函數式語言中最經常使用的持久數據結構之一,在計算幾何中也有重要做用。值得一提的是,Java 8中HashMap的實現也由於用RBTree取代鏈表(當鏈表長度8時),性能有所提高。java
RBTree的定義以下(3結點+2路徑,插入時受到威脅就是2路徑):算法
定義中的這些約束確保了紅黑樹的關鍵特性:從根到葉子的最長的可能路徑很少於最短的可能路徑的兩倍長。結果是這個樹大體上是平衡的(平衡的複雜度爲O(lgN),簡略能夠說紅黑樹也是O(lgN)),嚴格的所須要數學證實
。由於操做好比插入、刪除和查找某個值的最壞狀況時間都要求與樹的高度成比例,這個在高度上的理論上限容許紅黑樹在最壞狀況下都是高效的,而不一樣於普通的二叉查找樹。
要知道爲何這些性質確保了這個結果,注意到性質4致使了路徑不能有兩個毗連的紅色節點就足夠了。最短的可能路徑都是黑色節點,最長的可能路徑有交替的紅色和黑色節點。由於根據性質5全部最長的路徑都有相同數目的黑色節點,這就代表了沒有路徑能多於任何其餘路徑的兩倍長。
RBTree在理論上仍是一棵BST樹,可是它在對BST的插入和刪除操做時能經過旋轉操做來保持樹的平衡,即保證樹的高度在[logN,logN+1](理論上,極端的狀況下能夠出現RBTree的高度達到2*logN,但實際上很難遇到)。這樣RBTree的查找時間複雜度始終保持在O(logN)從而接近於理想的BST。RBTree的刪除和插入操做的時間複雜度也是O(logN)。因爲RBTree結構上就是一顆二叉查找樹,因此其查找操做就是BST的查找操做。因此就是RBTree的查找、插入、刪除在最壞狀況下的複雜度都是O(lgN)的。安全
關於紅黑樹的旋轉還好在算法導論上有僞代碼,註釋講解也很通透,沒什麼問題。網上關於紅黑樹的插入和刪除討論不少,但其中各類錯誤都有的,把左傾紅黑樹當成原版紅黑樹也有之,每一個人都有各類不一樣的狀況分析你分三種我分五種這樣,也讓人看了心煩,很難找到一份靠譜或者說權威的。
美團技術團隊知乎上那篇紅黑樹深刻剖析及Java實現裏面一些點總結的不錯,但也搞錯了幾個小地方,致使源碼也是有錯誤的地方,評論裏也有人指出來了。後來以爲這種各類不可以徹底靠譜的源碼與分析看着實在費勁,由於java8當中的TreeMap就是採用的紅黑樹,因此乾脆本身看TreeMap的源碼了,當時以爲源碼本身看不懂再去google一些英文靠譜點的文章吧。但最後經過一步步分析源碼明白了二叉紅黑樹的插入原理(雖然搞到了凌晨三點多...),畫出流程圖並進行對照總結以後發現就是美團技術團隊中所總結的三種,因此說它總結的不錯但它的那個少寫了個黑色結點的狀況包括給出的源碼也是。因此對於插入操做的參考源碼就是TreeMap上的源碼,爲了方便看我把一些保證安全的泛型去掉了,將Entry改爲了Node,具體完整的實現仍是要看TreeMap源碼。
對於刪除應該是更爲複雜的操做了,因此暫時還沒怎麼看,後面可能會補上吧。數據結構
紅黑樹中的結點增長了一個屬性color,來表示結點的顏色,能夠是RED或者BLACk,因此共包含6個屬性:color、key、value、left、right和parent。函數
private static final boolean RED = false; private static final boolean BLACK = true; static final class Node { K key; V value; Node left; Node right; Node parent; boolean color = BLACK; Node(K key, V value, Node parent) { this.key = key; this.value = value; this.parent = parent; }
描述的更準確些,不是紅黑樹的旋轉操做,而是二叉樹樹的旋轉操做,也就是在二叉樹中的一種子樹調整操做, 每一次旋轉並不影響對該二叉樹進行中序遍歷的結果,也就是旋轉先後兩棵樹上的結點直線投影來的序列同樣的,這樣旋轉不會違反結點的大小關係(即左小右大)。樹旋轉一般應用於須要調整樹的局部平衡性的場合。旋轉分爲左旋轉和右旋轉,其實直白點應該說成是把示意圖中展示出的結點多的一方向左旋轉和向右旋轉。在寫旋轉代碼的時候能夠配合示意圖來寫(對於旋轉的兩個根對象,寫完每一行指針指向相應的對象)。圖中所畫出來的結點也就是咱們旋轉的時候會涉及到的結點。
爲了更加針對紅黑樹(結點有父指針),我畫出了下面的這張圖,雙向箭頭表明着左或右指針指向和父指針指向,紅色部分表明進行旋轉時須要改變的連接關係。性能
private void rotateLeft(Node p) { if (p != null) { Node r = p.right; p.right = r.left; if (r.left != null) r.left.parent = p; r.parent = p.parent; if (p.parent == null) root = r; else if (p.parent.left == p) p.parent.left = r; else p.parent.right = r; r.left = p; p.parent = r; } } private void rotateRight(Node p) { if (p != null) { Node l = p.left; p.left = l.right; if (l.right != null) l.right.parent = p; l.parent = p.parent; if (p.parent == null) root = l; else if (p.parent.right == p) p.parent.right = l; else p.parent.left = l; l.right = p; p.parent = l; } }
旋轉的過程如代碼所示,能夠劃分爲下面的四個過程:this
關鍵注意的就是y在旋轉的時候會丟失一個與旋轉方向一致的結點,而後用x來填補這個位置(賦值給與旋轉方向一致的指針)。x則會用與旋轉方向相反的那個指針來改指向這個結點,而後就是這個結點若是存在的話就把它的父指針也改指爲x。程序流程中先處理了那個丟失的結點(y結點自己是逆賊上位,在旋轉方向上有失有得,x在旋轉方向相反方向上有失有得),再處理了父結點,最後處理了x、y之間的關係
google
咱們首先以二叉查找樹的方法增長節點並標記它爲紅色
。(若是設爲黑色,就會致使根到葉子的路徑上有一條路上,多一個額外的黑節點,這個是很難調整的。可是設爲紅色節點後,可能會致使出現兩個連續紅色節點的衝突,那麼能夠經過顏色調換(color flips)和樹旋轉來調整。)下面要進行什麼操做取決於其餘臨近節點的顏色。其次每次插入以及插入修復後將根節點置爲黑色。最後是空節點實現上處理爲顏色爲黑。這兩條保證了性質123始終是成立的。同人類的家族樹中同樣,咱們將使用術語叔節點來指一個節點的父節點的兄弟節點。注意:spa
紅色
節點、重繪黑色節點爲紅色
,或作旋轉時受到威脅。即:每一個紅色節點必須有兩個黑色的子節點。(從每一個葉子到根的全部路徑上不能有兩個連續的紅色節點。)紅色
節點爲黑色,或作旋轉時受到威脅。即:從任一節點到其每一個葉子的全部簡單路徑都包含相同數目的黑色節點。
爲了方便操做,TreeMap紅黑樹中還設置了以下幾個輔助函數
//返回結點的顏色,空結點返回黑色 private static boolean colorOf(Node p) { return (p == null ? BLACK : p.color); } //返回結點的父結點或者null private static Node parentOf(Node p) { return (p == null ? null: p.parent); } //設置結點的顏色 private static void setColor(Node p, boolean c) { if (p != null) p.color = c; } //返回該結點的左節點 private static Node leftOf(Node p) { return (p == null) ? null: p.left; } //返回該結點的右節點 private static Node rightOf(Node p) { return (p == null) ? null: p.right; }
首先講下插入與插入修復,顧名思義插入修復是對插入後進行的一種修復,爲何要進行修復,由於你的插入可能會形成上面提到的性質4和性質5被破壞。下面提到的重繪(不包括每次將結束將根節點設置爲黑色)實際上至關於將結點的顏色進行了反轉
,先說下不會形成性質破壞的插入:
首先咱們要明確一個事實,在咱們插入以前,紅黑樹是嚴格遵照上面5條性質的。性質13是不會被破壞的以上三種狀況,
其中1是根節點,咱們只須要將它在插入後重繪爲黑色便可,更不會破壞性質4和性質5。
對於2,既然可以在根節點進行插入也就是說在插入前空節點到根節點的黑色結點路徑爲0,那麼插入後新結點的兩個子節點(空節點)及其新結點自己到根節點黑色距離也是0(由於插入結點自己是紅色),不會破壞性質5,性質4由於新插入結點的兩個子節點(空節點都是黑色),因此也不會破壞。
對於3,與2同理,不會破壞性質4,對於性質5,任何一個節點到新結點及其兩個子節點的距離是相同的,由於新結點是紅色,此外任何節點到新結點及其子節點的距離確定與該結點到新結點所替換的那個空節點的距離相同,因此不會破壞性質5。
因此以上這三種狀況的插入是被排除在插入後修復的。那麼那些須要修補的插入狀況是什麼呢?首先要知足不是上面那幾種狀況之一,因此即必須有祖父節點(不爲空)而且父結點是紅色的,這是下面說的幾種狀況的前提條件(而且下面的狀況是過濾式的,即if 、else if 、else的關係
)
下面圖中的虛線段代表子節點可能爲父結點的左節點或者父結點的右節點兩種狀況,綠色虛線圓圈包圍着的結點表示在即將的修復操做後會被重繪的。
一張圖中的全部的黑色外圈數字結點
(多是一顆子樹,由於狀況1可能會迭代修復)表示它們是內含有相同的黑色長度的,黑色外圈指的是它的根節點必須是黑色的,否則就不知足性質4。內含有相同的黑色長度就是從它們的根節點到各自子樹的空節點的路過的黑色結點個數相同(這是根據性質5推出的)。當它們其中之一是null即空節點,即內含長度是0,而它們的根節點自己黑色的,因此他們全部黑色外圈數字節點都是null即空節點(這就是插入新結點的第一次插入修復的時候)。
一張圖中的全部紅黑外圈數字節點
(多是一顆子樹,由於狀況1可能會迭代修復)都是掛在叔結點下面的,表示它們內含長度必定是比黑色外圈數字結點少1(根據性質5推出),當黑色外圈數字已是null空節點時,由於內含長度不多是-1(根據性質5推出),這時表示的是叔結點爲null即空節點(根據性質3空節點是黑色的,仍知足叔結點爲黑色)(這就是插入新結點的第一次插入修復的時候)。**紅黑外圈表示它們的根節點多是紅色或者黑色的。
叔結點爲紅色的時候,則須要進行插入修復,如上圖表示的四種狀況所示,修復操做統一爲:將叔、父重繪爲黑色,祖繪爲紅色,這種修復並非一次性的,修復完畢須要繼續進行從頭開始的插入後修復判斷(即從判斷是否須要插入修復開始往下也要能又回到這種狀況)。
叔結點爲黑色而且子父祖在同側斜線上時,修復操做爲:將父繪黑,祖繪紅,而後以祖爲樞紐進行向相反側的方向旋轉,同在右側斜線上的就往左旋轉了,同在左側斜線上的就往右旋轉了。注意咱們這個時候繪黑的是即將成爲這顆子樹新的根節點的的父,繪紅的是再也不是根節點的祖,此種狀況是修復中止的,由於該子樹的根節點仍是黑色沒有發生變化,而字數內部又知足了性質三四五,而且沒有破壞有序性。
當叔結點爲黑色而且子父祖異側的時候,狀況如圖所示,修復操做分爲了兩步:第一步是根據子父所在的那一側的方向,以父爲樞向相反方向旋轉,這樣就實現了將子父祖異側轉換爲了父子祖同側(而且是子父原所在側的相反側),第二步是發現它符合上一種狀況了,那就將如今的父(原來的子)繪黑,祖(原來的祖)繪紅,由於上種狀況是修復中止的因此修復結束
在上面三種修補狀況當中後兩種是不會改變所在子樹的黑色路徑長度的,只有第一種狀況可能改變黑色路徑長度,由於它可能子樹根節點塗紅,若是子樹根節點就是整棵樹的根節點時,在修補結束後會被從新設置成黑色,黑色路徑長度就加1了,但這個時候黑色路徑增長針對的再也不是這個子樹了,而是整顆紅黑樹,因此是不會破壞性質5的。至於性質4看修補後的圖中結點的顏色就知道是不會破壞了。
插入結束以後,這顆子樹的知足了性質4(不能有兩個連續的紅色節點)和性質5(從任一節點到其每一個葉子的全部簡單路徑都包含相同數目的黑色節點),又是一顆新的紅黑樹了。