某些教程不區分普通紅黑樹和左傾紅黑樹的區別,直接將左傾紅黑樹拿來教學,而且稱其爲紅黑樹,由於左傾紅黑樹與普通的紅黑樹相比,實現起來較爲簡單,容易教學。在這裏,咱們區分開左傾紅黑樹和普通紅黑樹。node
紅黑樹是一種近似平衡的二叉查找樹,從2-3
樹或2-3-4
樹衍生而來。經過對二叉樹節點進行染色,染色爲紅或黑節點,來模仿2-3
樹或2-3-4
樹的3節點和4節點,從而讓樹的高度減少。2-3-4
樹對照實現的紅黑樹是普通的紅黑樹,而2-3
樹對照實現的紅黑樹是一種變種,稱爲左傾紅黑樹,其更容易實現。算法
使用平衡樹數據結構,能夠提升查找元素的速度,咱們在本章介紹2-3
樹,再用二叉樹形式來實現2-3
樹,也就是左傾紅黑樹。segmentfault
2-3
樹是一棵嚴格自平衡的多路查找樹,由1986年圖靈獎得主,美國理論計算機科學家John Edward Hopcroft
在1970年發明,又稱3階的B樹
(注:B
爲Balance
平衡的意思)數組
它不是一棵二叉樹,是一棵三叉樹。具備如下特徵:數據結構
由於2-3
樹的第二個特徵,它是一棵完美平衡的樹,很是完美,除了葉子節點,其餘的節點都沒有空兒子,因此樹的高度很是的小。併發
如圖:數據結構和算法
若是一個內部節點擁有一個數據元素、兩個子節點,則此節點爲2節點。若是一個內部節點擁有兩個數據元素、三個子節點,則此節點爲3節點。函數
能夠說,全部平衡樹的核心都在於插入和刪除邏輯,咱們主要分析這兩個操做。性能
在插入元素時,須要先找到插入的位置,使用二分查找從上自下查找樹節點。學習
找到插入位置時,將元素插入該位置,而後進行調整,使得知足2-3
樹的特徵。主要有三種狀況:
如圖(來自維基百科):
核心在於插入3節點後,該節點變爲臨時4節點,而後進行分裂恢復樹的特徵。最壞狀況爲插入節點後,每一次分裂後都致使上一層變爲臨時4節點,直到樹根節點,這樣須要不斷向上分裂。
臨時4節點的分裂,細分有六種狀況,如圖:
與其餘二叉查找樹由上而下生長不一樣,2-3
樹是從下至上的生長。
2-3
樹的實現將會放在B樹
章節,咱們將會在此章節實現其二叉樹形式的左傾紅黑樹結構。
刪除操做就複雜得多了,請耐心閱讀理解。
2-3
樹的特徵註定它是一棵很是完美平衡的三叉樹,其全部子樹也都是完美平衡,因此2-3
樹的某節點的兒子,要麼都是空兒子,要麼都不是空兒子。好比2-3
樹的某個節點A
有兩個兒子B
和C
,兒子B
和C
要麼都沒有孩子,要麼孩子都是滿的,否則2-3
樹全部葉子節點到根節點的長度一致這個特徵就被破壞了。
基於上面的現實,咱們來分析刪除的不一樣狀況,刪除中間節點和葉子節點。
狀況1:刪除中間節點
刪除的是非葉子節點,該節點必定是有兩棵或者三棵子樹的,那麼從子樹中找到其最小後繼節點,該節點是葉子節點,用該節點替換被刪除的非葉子節點,而後再刪除這個葉子節點,進入狀況2。
如何找到最小後繼節點,當有兩棵子樹時,那麼從右子樹一直往左下方找,若是有三棵子樹,被刪除節點在左邊,那麼從中子樹一直往左下方找,不然從右子樹一直往左下方找。
狀況2:刪除葉子節點
刪除的是葉子節點,這時若是葉子節點是3節點,那麼直接變爲2節點便可,不影響平衡。可是,若是葉子節點是2節點,那麼刪除後,其父節點將會缺失一個兒子,破壞了滿孩子的2-3
樹特徵,須要進行調整後才能刪除。
針對狀況2,刪除一個2節點的葉子節點,會致使父節點缺失一個兒子,破壞了2-3
樹的特徵,咱們能夠進行調整變換,主要有兩種調整:
看圖說話:
若是被刪除的葉子節點有兄弟是3節點,那麼從兄弟那裏借一個值填補被刪除的葉子節點,而後兄弟和父親從新分佈調整位置。下面是從新分佈的具體例子:
能夠看到,刪除100
,從兄弟那裏借來一個值80
,而後從新調整父親,兄弟們的位置。
若是兄弟們都是2節點呢,那麼就合併節點:將父親和兄弟節點合併,若是父親是2節點,那麼父親就留空了,不然父親就從3節點變成2節點,下面是合併的兩個具體例子:
能夠看到,刪除80
,而兄弟節點60
和父親節點90
都是個2節點,因此父親下來和兄弟合併,而後父親變爲空節點。
能夠看到,刪除70
,而兄弟節點都爲2節點,父親節點爲3節點,那麼父親下來和其中一個兄弟合併,而後父親從3節點變爲2節點。
可是,若是合併後,父親節點變空了,也就是說有中間節點留空要怎麼辦,那麼能夠繼續遞歸處理,如圖:
中間節點是空的,那麼能夠繼續從兄弟那裏借節點或者和父親合併,直到根節點,若是到達了根節點呢,如圖:
遞歸到了根節點後,若是存在空的根節點,咱們能夠直接把該空節點刪除便可,這時樹的高度減小一層。
2-3
樹的實現將會放在B樹
章節,咱們將會實現其二叉樹形式的左傾紅黑樹結構。
左傾紅黑樹能夠由2-3
樹的二叉樹形式來實現。
其定義爲:
因爲紅連接都在左邊,因此這種紅黑樹又稱左傾紅黑樹。左傾紅黑樹與2-3
樹一一對應,只要將左連接畫平,如圖:
首先,咱們要定義樹的結構LLRBTree
,以及表示左傾紅黑樹的節點LLRBTNode
:
// 定義顏色 const ( RED = true BLACK = false ) // 左傾紅黑樹 type LLRBTree struct { Root *LLRBTNode // 樹根節點 } // 左傾紅黑樹節點 type LLRBTNode struct { Value int64 // 值 Times int64 // 值出現的次數 Left *LLRBTNode // 左子樹 Right *LLRBTNode // 右子樹 Color bool // 父親指向該節點的連接顏色 } // 新建一棵空樹 func NewLLRBTree() *LLRBTree { return &LLRBTree{} } // 節點的顏色 func IsRed(node *LLRBTNode) bool { if node == nil { return false } return node.Color == RED }
在節點LLRBTNode
中,咱們存儲的元素字段爲Value
,因爲可能有重複的元素插入,因此多了一個Times
字段,表示該元素出現幾回。
固然,紅黑樹中的紅黑顏色使用Color
定義,表示父親指向該節點的連接顏色。爲了方便,咱們還構造了一個輔助函數IsRed()
。
在元素添加和實現的過程當中,須要作調整操做,有兩種旋轉操做,對某節點的右連接進行左旋轉,或者左連接進行右旋轉。
如圖是對節點h
的右連接進行左旋轉:
代碼實現以下:
// 左旋轉 func RotateLeft(h *LLRBTNode) *LLRBTNode { if h == nil { return nil } // 看圖理解 x := h.Right h.Right = x.Left x.Left = h x.Color = h.Color h.Color = RED return x }
如圖是對節點h
的左連接進行右旋轉:
代碼實現以下:
// 右旋轉 func RotateRight(h *LLRBTNode) *LLRBTNode { if h == nil { return nil } // 看圖理解 x := h.Left h.Left = x.Right x.Right = h x.Color = h.Color h.Color = RED return x }
因爲左傾紅黑樹不容許一個節點有兩個紅連接,因此須要作顏色轉換,如圖:
代碼以下:
// 顏色轉換 func ColorChange(h *LLRBTNode) { if h == nil { return } h.Color = !h.Color h.Left.Color = !h.Left.Color h.Right.Color = !h.Right.Color }
旋轉和顏色轉換做爲局部調整,並不影響全局。
每次添加元素節點時,都將該節點Color
字段,也就是父親指向它的連接設置爲RED
紅色。
接着判斷其父親是否有兩個紅連接(如連續的兩個左紅連接或者左右紅色連接),或者有右紅色連接,進行顏色變換或旋轉操做。
主要有如下這幾種狀況。
插入元素到2節點,直接讓節點變爲3節點,不過當右插入時須要左旋使得紅色連接在左邊,如圖:
插入元素到3節點,須要作旋轉和顏色轉換操做,如圖:
也就是說,在一個已是紅色左連接的節點,插入一個新節點的狀態變化以下:
根據上述的演示圖以及旋轉,顏色轉換等操做,添加元素的代碼爲:
// 左傾紅黑樹添加元素 func (tree *LLRBTree) Add(value int64) { // 跟節點開始添加元素,由於可能調整,因此須要將返回的節點賦值回根節點 tree.Root = tree.Root.Add(value) // 根節點的連接永遠都是黑色的 tree.Root.Color = BLACK } // 往節點添加元素 func (node *LLRBTNode) Add(value int64) *LLRBTNode { // 插入的節點爲空,將其連接顏色設置爲紅色,並返回 if node == nil { return &LLRBTNode{ Value: value, Color: RED, } } // 插入的元素重複 if value == node.Value { node.Times = node.Times + 1 } else if value > node.Value { // 插入的元素比節點值大,往右子樹插入 node.Right = node.Right.Add(value) } else { // 插入的元素比節點值小,往左子樹插入 node.Left = node.Left.Add(value) } // 輔助變量 nowNode := node // 右連接爲紅色,那麼進行左旋,確保樹是左傾的 // 這裏作完操做後就能夠結束了,由於插入操做,新插入的右紅連接左旋後,nowNode節點不會出現連續兩個紅左連接,由於它只有一個左紅連接 if IsRed(nowNode.Right) && !IsRed(nowNode.Left) { nowNode = RotateLeft(nowNode) } else { // 連續兩個左連接爲紅色,那麼進行右旋 if IsRed(nowNode.Left) && IsRed(nowNode.Left.Left) { nowNode = RotateRight(nowNode) } // 旋轉後,可能左右連接都爲紅色,須要變色 if IsRed(nowNode.Left) && IsRed(nowNode.Right) { ColorChange(nowNode) } } return nowNode }
可參考論文:Left-leaning Red-Black Trees。
左傾紅黑樹的最壞樹高度爲2log(n)
,其中n
爲樹的節點數量。爲何呢,咱們先把左傾紅黑樹看成2-3
樹,也就是說最壞狀況下沿着2-3
樹左邊的節點都是3節點,其餘節點都是2節點,這時樹高近似log(n)
,再從2-3
樹轉成左傾紅黑樹,當3節點不畫平時,能夠知道樹高變成原來2-3
樹樹高的兩倍。雖然如此,構造一棵最壞的左傾紅黑樹很難。
AVL
樹的最壞樹高度爲1.44log(n)
。因爲左傾紅黑樹是近似平衡的二叉樹,沒有AVL
樹的嚴格平衡,樹的高度會更高一點,所以查找操做效率比AVL
樹低,但時間複雜度只在於常數項的差異,去掉常數項,時間複雜度仍然是log(n)
。
咱們的代碼實現中,左傾紅黑樹的插入,須要逐層判斷是否須要旋轉和變色,複雜度爲log(n)
,當旋轉變色後致使上層存在連續的紅左連接或者紅色左右連接,那麼須要繼續旋轉和變色,可能有屢次這種調整操做,如圖在箭頭處添加新節點,出現了右紅連接,要一直向上變色到根節點(實際上穿投到根節點的狀況極少發生):
咱們能夠優化代碼,使得在某一層旋轉變色後,若是其父層沒有連續的左紅連接或者不須要變色,那麼能夠直接退出,不須要逐層判斷是否須要旋轉和變色。
對於AVL
樹來講,插入最多旋轉兩次,但其須要逐層更新樹高度,複雜度也是爲log(n)
。
按照插入效率來講,不少教程都說左傾紅黑樹會比AVL
樹好一點,由於其不要求嚴格的平衡,會插入得更快點,但根據咱們實際上的遞歸代碼,二者都須要逐層向上判斷是否須要調整,只不過AVL
樹多了更新樹高度的操做,此操做影響了一點點效率,但我以爲兩種樹的插入效率都差很少。
在此,咱們再也不糾結兩種平衡樹哪一種更好,由於代碼實現中,兩種平衡樹都須要自底向上的遞歸操做,效率差異不大。。
刪除操做就複雜得多了。對照一下2-3
樹。
在這裏,爲了使得刪除葉子節點時能夠直接刪除,葉子節點必須變爲紅節點。(在2-3
樹中,也就是2節點要變成3節點,咱們知道要不和父親合併再遞歸向上,要不向兄弟借值而後從新分佈)
咱們創造兩種操做,若是刪除的節點在左子樹中,可能須要進行紅色左移,若是刪除的節點在右子樹中,可能須要進行紅色右移。
咱們介紹紅色左移的步驟:
要在樹h
的的左子樹中刪除元素,這時樹h
根節點是紅節點,其兒子b,d
節點都爲黑色節點,且兩個黑色節點都是2節點,都沒有左紅孩子,那麼直接對h
樹根節點變色便可(至關於2-3
樹:把父親的一個值拉下來合併),如圖:
若是存在右兒子d
是3節點,有左紅孩子e
,那麼須要先對h
樹根節點變色後,對右兒子d
右旋,再對h
樹根節點左旋,最後再一次對h
樹根節點變色(至關於2-3
樹:向3節點兄弟借值,而後從新分佈),如圖:
紅色左移能夠總結爲下圖(被刪除的節點在左子樹,且進入的樹根h必定爲紅節點):
代碼以下:
// 紅色左移 // 節點 h 是紅節點,其左兒子和左兒子的左兒子都爲黑節點,左移後使得其左兒子或左兒子的左兒子有一個是紅色節點 func MoveRedLeft(h *LLRBTNode) *LLRBTNode { // 應該確保 isRed(h) && !isRed(h.left) && !isRed(h.left.left) ColorChange(h) // 右兒子有左紅連接 if IsRed(h.Right.Left) { // 對右兒子右旋 h.Right = RotateRight(h.Right) // 再左旋 h = RotateLeft(h) ColorChange(h) } return h }
爲何要紅色左移,是要保證調整後,子樹根節點h
的左兒子或左兒子的左兒子有一個是紅色節點,這樣從h
的左子樹遞歸刪除元素才能夠繼續下去。
紅色右移的步驟相似,如圖(被刪除的節點在右子樹,且進入的樹根h必定爲紅節點):
代碼以下:
// 紅色右移 // 節點 h 是紅節點,其右兒子和右兒子的左兒子都爲黑節點,右移後使得其右兒子或右兒子的右兒子有一個是紅色節點 func MoveRedRight(h *LLRBTNode) *LLRBTNode { // 應該確保 isRed(h) && !isRed(h.right) && !isRed(h.right.left); ColorChange(h) // 左兒子有左紅連接 if IsRed(h.Left.Left) { // 右旋 h = RotateRight(h) // 變色 ColorChange(h) } return h }
爲何要紅色右移,一樣是爲了保證樹根節點h
的右兒子或右兒子的右兒子有一個是紅色節點,往右子樹遞歸刪除元素能夠繼續下去。
介紹完兩種操做後,咱們要明確一下究竟是如何刪除元素的。
咱們知道2-3
樹的刪除是從葉子節點開始,自底向上的向兄弟節點借值,或和父親合併,而後一直遞歸到根節點。左傾紅黑樹參考了這種作法,但更巧妙,左傾紅黑樹要保證一路上每次遞歸進入刪除操做的子樹樹根必定是一個3節點,因此須要適當的紅色左移或右移(相似於2-3
樹借值和合並),這樣一直遞歸到葉子節點,葉子節點也會是一個3節點,而後就能夠直接刪除葉子節點,最後再自底向上的恢復左傾紅黑樹的特徵。
下面是左傾紅黑樹從樹h
刪除元素的示例圖,往樹h
左子樹和右子樹刪除元素分別有四種狀況,後兩種狀況須要使用到紅色左移或右移,狀態演變以後,樹h
才能夠從左或右子樹進入下一次遞歸:
能夠對照着大圖,繼續閱讀下面的左傾紅黑樹刪除元素代碼:
// 左傾紅黑樹刪除元素 func (tree *LLRBTree) Delete(value int64) { // 當找不到值時直接返回 if tree.Find(value) == nil { return } if !IsRed(tree.Root.Left) && !IsRed(tree.Root.Right) { // 左右子樹都是黑節點,那麼先將根節點變爲紅節點,方便後面的紅色左移或右移 tree.Root.Color = RED } tree.Root = tree.Root.Delete(value) // 最後,若是根節點非空,永遠都要爲黑節點,賦值黑色 if tree.Root != nil { tree.Root.Color = BLACK } }
首先tree.Find(value)
找到能夠刪除的值時才能進行刪除。
當根節點的左右子樹都爲黑節點時,那麼先將根節點變爲紅節點,方便後面的紅色左移或右移。
刪除完節點:tree.Root = tree.Root.Delete(value)
後,須要將根節點染回黑色,由於左傾紅黑樹的特徵之一是根節點永遠都是黑色。
核心的從子樹中刪除元素代碼以下:
// 對該節點所在的子樹刪除元素 func (node *LLRBTNode) Delete(value int64) *LLRBTNode { // 輔助變量 nowNode := node // 刪除的元素比子樹根節點小,須要從左子樹刪除 if value < nowNode.Value { // 由於從左子樹刪除,因此要判斷是否須要紅色左移 if !IsRed(nowNode.Left) && !IsRed(nowNode.Left.Left) { // 左兒子和左兒子的左兒子都不是紅色節點,那麼無法遞歸下去,先紅色左移 nowNode = MoveRedLeft(nowNode) } // 如今能夠從左子樹中刪除了 nowNode.Left = nowNode.Left.Delete(value) } else { // 刪除的元素等於或大於樹根節點 // 左節點爲紅色,那麼須要右旋,方便後面能夠紅色右移 if IsRed(nowNode.Left) { nowNode = RotateRight(nowNode) } // 值相等,且沒有右孩子節點,那麼該節點必定是要被刪除的葉子節點,直接刪除 // 爲何呢,反證,它沒有右兒子,但有左兒子,由於左傾紅黑樹的特徵,那麼左兒子必定是紅色,可是前面的語句已經把紅色左兒子右旋到右邊,不該該出現右兒子爲空。 if value == nowNode.Value && nowNode.Right == nil { return nil } // 由於從右子樹刪除,因此要判斷是否須要紅色右移 if !IsRed(nowNode.Right) && !IsRed(nowNode.Right.Left) { // 右兒子和右兒子的左兒子都不是紅色節點,那麼無法遞歸下去,先紅色右移 nowNode = MoveRedRight(nowNode) } // 刪除的節點找到了,它是中間節點,須要用最小後驅節點來替換它,而後刪除最小後驅節點 if value == nowNode.Value { minNode := nowNode.Right.FindMinValue() nowNode.Value = minNode.Value nowNode.Times = minNode.Times // 刪除其最小後驅節點 nowNode.Right = nowNode.Right.DeleteMin() } else { // 刪除的元素比子樹根節點大,須要從右子樹刪除 nowNode.Right = nowNode.Right.Delete(value) } } // 最後,刪除葉子節點後,須要恢復左傾紅黑樹特徵 return nowNode.FixUp() }
這段核心代碼十分複雜,會用到紅色左移和右移,當刪除的元素小於根節點時,咱們明白要在左子樹中刪除,如:
// 刪除的元素比子樹根節點小,須要從左子樹刪除 if value < nowNode.Value { // 由於從左子樹刪除,因此要判斷是否須要紅色左移 if !IsRed(nowNode.Left) && !IsRed(nowNode.Left.Left) { // 左兒子和左兒子的左兒子都不是紅色節點,那麼無法遞歸下去,先紅色左移 nowNode = MoveRedLeft(nowNode) } // 如今能夠從左子樹中刪除了 nowNode.Left = nowNode.Left.Delete(value) }
遞歸刪除左子樹前:nowNode.Left = nowNode.Left.Delete(value)
,要確保刪除的左子樹根節點是紅色節點,或左子樹根節點的左兒子是紅色節點,纔可以繼續遞歸下去,因此使用了!IsRed(nowNode.Left) && !IsRed(nowNode.Left.Left)
來判斷是否須要紅色左移。
若是刪除的值不小於根節點,那麼進入如下邏輯(可仔細閱讀註釋):
// 刪除的元素等於或大於樹根節點 // 左節點爲紅色,那麼須要右旋,方便後面能夠紅色右移 if IsRed(nowNode.Left) { nowNode = RotateRight(nowNode) } // 值相等,且沒有右孩子節點,那麼該節點必定是要被刪除的葉子節點,直接刪除 // 爲何呢,反證,它沒有右兒子,但有左兒子,由於左傾紅黑樹的特徵,那麼左兒子必定是紅色,可是前面的語句已經把紅色左兒子右旋到右邊,不該該出現右兒子爲空。 if value == nowNode.Value && nowNode.Right == nil { return nil } // 由於從右子樹刪除,因此要判斷是否須要紅色右移 if !IsRed(nowNode.Right) && !IsRed(nowNode.Right.Left) { // 右兒子和右兒子的左兒子都不是紅色節點,那麼無法遞歸下去,先紅色右移 nowNode = MoveRedRight(nowNode) } // 刪除的節點找到了,它是中間節點,須要用最小後驅節點來替換它,而後刪除最小後驅節點 if value == nowNode.Value { minNode := nowNode.Right.FindMinValue() nowNode.Value = minNode.Value nowNode.Times = minNode.Times // 刪除其最小後驅節點 nowNode.Right = nowNode.Right.DeleteMin() } else { // 刪除的元素比子樹根節點大,須要從右子樹刪除 nowNode.Right = nowNode.Right.Delete(value) }
首先,須要先判斷該節點的左子樹根節點是否爲紅色節點IsRed(nowNode.Left)
,若是是的話須要右旋:nowNode = RotateRight(nowNode)
,將紅節點右旋是爲了後面能夠遞歸進入右子樹。
而後,判斷刪除的值是否等於當前根節點的值,且其沒有右節點:value == nowNode.Value && nowNode.Right == nil
,若是是,那麼該節點就是要被刪除的葉子節點,直接刪除便可。
接着,判斷是否須要紅色右移:!IsRed(nowNode.Right) && !IsRed(nowNode.Right.Left)
,若是該節點右兒子和右兒子的左兒子都不是紅色節點,那麼無法遞歸進入右子樹,須要紅色右移,必須確保其右子樹或右子樹的左兒子有一個是紅色節點。
再接着,須要判斷是否找到了要刪除的節點:value == nowNode.Value
,找到時表示要刪除的節點處於內部節點,須要用最小後驅節點來替換它,而後刪除最小後驅節點。
找到最小後驅節點:minNode := nowNode.Right.FindMinValue()
後,將最小後驅節點與要刪除的內部節點替換,而後刪除最小後驅節點:nowNode.Right = nowNode.Right.DeleteMin()
,刪除最小節點代碼以下:
// 對該節點所在的子樹刪除最小元素 func (node *LLRBTNode) DeleteMin() *LLRBTNode { // 輔助變量 nowNode := node // 沒有左子樹,那麼刪除它本身 if nowNode.Left == nil { return nil } // 判斷是否須要紅色左移,由於最小元素在左子樹中 if !IsRed(nowNode.Left) && !IsRed(nowNode.Left.Left) { nowNode = MoveRedLeft(nowNode) } // 遞歸從左子樹刪除 nowNode.Left = nowNode.Left.DeleteMin() // 修復左傾紅黑樹特徵 return nowNode.FixUp() }
由於最小節點在最左的葉子節點,因此只須要適當的紅色左移,而後一直左子樹遞歸便可。遞歸完後須要修復左傾紅黑樹特徵nowNode.FixUp()
,代碼以下:
// 修復左傾紅黑樹特徵 func (node *LLRBTNode) FixUp() *LLRBTNode { // 輔助變量 nowNode := node // 紅連接在右邊,左旋恢復,讓紅連接只出如今左邊 if IsRed(nowNode.Right) { nowNode = RotateLeft(nowNode) } // 連續兩個左連接爲紅色,那麼進行右旋 if IsRed(nowNode.Left) && IsRed(nowNode.Left.Left) { nowNode = RotateRight(nowNode) } // 旋轉後,可能左右連接都爲紅色,須要變色 if IsRed(nowNode.Left) && IsRed(nowNode.Right) { ColorChange(nowNode) } return nowNode }
若是不是刪除內部節點,依然是從右子樹繼續遞歸:
// 刪除的元素比子樹根節點大,須要從右子樹刪除 nowNode.Right = nowNode.Right.Delete(value)
固然,遞歸完成後還要進行一次FixUp()
,恢復左傾紅黑樹的特徵。
刪除操做很難理解,能夠多多思考,紅色左移和右移不斷地遞歸都是爲了確保刪除葉子節點時,其是一個3節點。
PS:若是不理解自頂向下的紅色左移和右移遞歸思路,能夠更換另一種方法,使用原先2-3樹
刪除元素操做步驟來實現,一開始從葉子節點刪除,而後自底向上的向兄弟借值或與父親合併,這是更容易理解的,咱們不在這裏進行展現了,能夠借鑑普通紅黑樹章節的刪除實現(它使用了自底向上的調整)。
完整代碼見最下面。
左傾紅黑樹刪除元素須要自頂向下的遞歸,可能不斷地紅色左移和右移,也就是有不少的旋轉,當刪除葉子節點後,還須要逐層恢復左傾紅黑樹的特徵。時間複雜度仍然是和樹高有關:log(n)
。
查找最小值,最大值,或者某個值,代碼以下:
// 找出最小值的節點 func (tree *LLRBTree) FindMinValue() *LLRBTNode { if tree.Root == nil { // 若是是空樹,返回空 return nil } return tree.Root.FindMinValue() } func (node *LLRBTNode) FindMinValue() *LLRBTNode { // 左子樹爲空,表面已是最左的節點了,該值就是最小值 if node.Left == nil { return node } // 一直左子樹遞歸 return node.Left.FindMinValue() } // 找出最大值的節點 func (tree *LLRBTree) FindMaxValue() *LLRBTNode { if tree.Root == nil { // 若是是空樹,返回空 return nil } return tree.Root.FindMaxValue() } func (node *LLRBTNode) FindMaxValue() *LLRBTNode { // 右子樹爲空,表面已是最右的節點了,該值就是最大值 if node.Right == nil { return node } // 一直右子樹遞歸 return node.Right.FindMaxValue() } // 查找指定節點 func (tree *LLRBTree) Find(value int64) *LLRBTNode { if tree.Root == nil { // 若是是空樹,返回空 return nil } return tree.Root.Find(value) } func (node *LLRBTNode) Find(value int64) *LLRBTNode { if value == node.Value { // 若是該節點剛剛等於該值,那麼返回該節點 return node } else if value < node.Value { // 若是查找的值小於節點值,從節點的左子樹開始找 if node.Left == nil { // 左子樹爲空,表示找不到該值了,返回nil return nil } return node.Left.Find(value) } else { // 若是查找的值大於節點值,從節點的右子樹開始找 if node.Right == nil { // 右子樹爲空,表示找不到該值了,返回nil return nil } return node.Right.Find(value) } } // 中序遍歷 func (tree *LLRBTree) MidOrder() { tree.Root.MidOrder() } func (node *LLRBTNode) MidOrder() { if node == nil { return } // 先打印左子樹 node.Left.MidOrder() // 按照次數打印根節點 for i := 0; i <= int(node.Times); i++ { fmt.Println(node.Value) } // 打印右子樹 node.Right.MidOrder() }
查找操做邏輯與通用的二叉查找樹同樣,並沒有區別。
如何確保咱們的代碼實現的就是一棵左傾紅黑樹呢,能夠進行驗證:
// 驗證是否是棵左傾紅黑樹 func (tree *LLRBTree) IsLLRBTree() bool { if tree == nil || tree.Root == nil { return true } // 判斷樹是不是一棵二分查找樹 if !tree.Root.IsBST() { return false } // 判斷樹是否遵循2-3樹,也就是紅連接只能在左邊,不能連續有兩個紅連接 if !tree.Root.Is23() { return false } // 判斷樹是否平衡,也就是任意一個節點到葉子節點,通過的黑色連接數量相同 // 先計算根節點到最左邊葉子節點的黑連接數量 blackNum := 0 x := tree.Root for x != nil { if !IsRed(x) { // 是黑色連接 blackNum = blackNum + 1 } x = x.Left } if !tree.Root.IsBalanced(blackNum) { return false } return true } // 節點所在的子樹是不是一棵二分查找樹 func (node *LLRBTNode) IsBST() bool { if node == nil { return true } // 左子樹非空,那麼根節點必須大於左兒子節點 if node.Left != nil { if node.Value > node.Left.Value { } else { fmt.Printf("father:%#v,lchild:%#v,rchild:%#v\n", node, node.Left, node.Right) return false } } // 右子樹非空,那麼根節點必須小於右兒子節點 if node.Right != nil { if node.Value < node.Right.Value { } else { fmt.Printf("father:%#v,lchild:%#v,rchild:%#v\n", node, node.Left, node.Right) return false } } // 左子樹也要判斷是不是平衡查找樹 if !node.Left.IsBST() { return false } // 右子樹也要判斷是不是平衡查找樹 if !node.Right.IsBST() { return false } return true } // 節點所在的子樹是否遵循2-3樹 func (node *LLRBTNode) Is23() bool { if node == nil { return true } // 不容許右傾紅連接 if IsRed(node.Right) { fmt.Printf("father:%#v,rchild:%#v\n", node, node.Right) return false } // 不容許連續兩個左紅連接 if IsRed(node) && IsRed(node.Left) { fmt.Printf("father:%#v,lchild:%#v\n", node, node.Left) return false } // 左子樹也要判斷是否遵循2-3樹 if !node.Left.Is23() { return false } // 右子樹也要判斷是不是遵循2-3樹 if !node.Right.Is23() { return false } return true } // 節點所在的子樹是否平衡,是否有 blackNum 個黑連接 func (node *LLRBTNode) IsBalanced(blackNum int) bool { if node == nil { return blackNum == 0 } if !IsRed(node) { blackNum = blackNum - 1 } if !node.Left.IsBalanced(blackNum) { fmt.Println("node.Left to leaf black link is not ", blackNum) return false } if !node.Right.IsBalanced(blackNum) { fmt.Println("node.Right to leaf black link is not ", blackNum) return false } return true }
運行請看完整代碼。
package main import "fmt" // 左傾紅黑樹實現 // Left-leaning red-black tree // 定義顏色 const ( RED = true BLACK = false ) // 左傾紅黑樹 type LLRBTree struct { Root *LLRBTNode // 樹根節點 } // 新建一棵空樹 func NewLLRBTree() *LLRBTree { return &LLRBTree{} } // 左傾紅黑樹節點 type LLRBTNode struct { Value int64 // 值 Times int64 // 值出現的次數 Left *LLRBTNode // 左子樹 Right *LLRBTNode // 右子樹 Color bool // 父親指向該節點的連接顏色 } // 節點的顏色 func IsRed(node *LLRBTNode) bool { if node == nil { return false } return node.Color == RED } // 左旋轉 func RotateLeft(h *LLRBTNode) *LLRBTNode { if h == nil { return nil } // 看圖理解 x := h.Right h.Right = x.Left x.Left = h x.Color = h.Color h.Color = RED return x } // 右旋轉 func RotateRight(h *LLRBTNode) *LLRBTNode { if h == nil { return nil } // 看圖理解 x := h.Left h.Left = x.Right x.Right = h x.Color = h.Color h.Color = RED return x } // 紅色左移 // 節點 h 是紅節點,其左兒子和左兒子的左兒子都爲黑節點,左移後使得其左兒子或左兒子的左兒子有一個是紅色節點 func MoveRedLeft(h *LLRBTNode) *LLRBTNode { // 應該確保 isRed(h) && !isRed(h.left) && !isRed(h.left.left) ColorChange(h) // 右兒子有左紅連接 if IsRed(h.Right.Left) { // 對右兒子右旋 h.Right = RotateRight(h.Right) // 再左旋 h = RotateLeft(h) ColorChange(h) } return h } // 紅色右移 // 節點 h 是紅節點,其右兒子和右兒子的左兒子都爲黑節點,右移後使得其右兒子或右兒子的右兒子有一個是紅色節點 func MoveRedRight(h *LLRBTNode) *LLRBTNode { // 應該確保 isRed(h) && !isRed(h.right) && !isRed(h.right.left); ColorChange(h) // 左兒子有左紅連接 if IsRed(h.Left.Left) { // 右旋 h = RotateRight(h) // 變色 ColorChange(h) } return h } // 顏色變換 func ColorChange(h *LLRBTNode) { if h == nil { return } h.Color = !h.Color h.Left.Color = !h.Left.Color h.Right.Color = !h.Right.Color } // 左傾紅黑樹添加元素 func (tree *LLRBTree) Add(value int64) { // 跟節點開始添加元素,由於可能調整,因此須要將返回的節點賦值回根節點 tree.Root = tree.Root.Add(value) // 根節點的連接永遠都是黑色的 tree.Root.Color = BLACK } // 往節點添加元素 func (node *LLRBTNode) Add(value int64) *LLRBTNode { // 插入的節點爲空,將其連接顏色設置爲紅色,並返回 if node == nil { return &LLRBTNode{ Value: value, Color: RED, } } // 插入的元素重複 if value == node.Value { node.Times = node.Times + 1 } else if value > node.Value { // 插入的元素比節點值大,往右子樹插入 node.Right = node.Right.Add(value) } else { // 插入的元素比節點值小,往左子樹插入 node.Left = node.Left.Add(value) } // 輔助變量 nowNode := node // 右連接爲紅色,那麼進行左旋,確保樹是左傾的 // 這裏作完操做後就能夠結束了,由於插入操做,新插入的右紅連接左旋後,nowNode節點不會出現連續兩個紅左連接,由於它只有一個左紅連接 if IsRed(nowNode.Right) && !IsRed(nowNode.Left) { nowNode = RotateLeft(nowNode) } else { // 連續兩個左連接爲紅色,那麼進行右旋 if IsRed(nowNode.Left) && IsRed(nowNode.Left.Left) { nowNode = RotateRight(nowNode) } // 旋轉後,可能左右連接都爲紅色,須要變色 if IsRed(nowNode.Left) && IsRed(nowNode.Right) { ColorChange(nowNode) } } return nowNode } // 找出最小值的節點 func (tree *LLRBTree) FindMinValue() *LLRBTNode { if tree.Root == nil { // 若是是空樹,返回空 return nil } return tree.Root.FindMinValue() } func (node *LLRBTNode) FindMinValue() *LLRBTNode { // 左子樹爲空,表面已是最左的節點了,該值就是最小值 if node.Left == nil { return node } // 一直左子樹遞歸 return node.Left.FindMinValue() } // 找出最大值的節點 func (tree *LLRBTree) FindMaxValue() *LLRBTNode { if tree.Root == nil { // 若是是空樹,返回空 return nil } return tree.Root.FindMaxValue() } func (node *LLRBTNode) FindMaxValue() *LLRBTNode { // 右子樹爲空,表面已是最右的節點了,該值就是最大值 if node.Right == nil { return node } // 一直右子樹遞歸 return node.Right.FindMaxValue() } // 查找指定節點 func (tree *LLRBTree) Find(value int64) *LLRBTNode { if tree.Root == nil { // 若是是空樹,返回空 return nil } return tree.Root.Find(value) } func (node *LLRBTNode) Find(value int64) *LLRBTNode { if value == node.Value { // 若是該節點剛剛等於該值,那麼返回該節點 return node } else if value < node.Value { // 若是查找的值小於節點值,從節點的左子樹開始找 if node.Left == nil { // 左子樹爲空,表示找不到該值了,返回nil return nil } return node.Left.Find(value) } else { // 若是查找的值大於節點值,從節點的右子樹開始找 if node.Right == nil { // 右子樹爲空,表示找不到該值了,返回nil return nil } return node.Right.Find(value) } } // 中序遍歷 func (tree *LLRBTree) MidOrder() { tree.Root.MidOrder() } func (node *LLRBTNode) MidOrder() { if node == nil { return } // 先打印左子樹 node.Left.MidOrder() // 按照次數打印根節點 for i := 0; i <= int(node.Times); i++ { fmt.Println(node.Value) } // 打印右子樹 node.Right.MidOrder() } // 修復左傾紅黑樹特徵 func (node *LLRBTNode) FixUp() *LLRBTNode { // 輔助變量 nowNode := node // 紅連接在右邊,左旋恢復,讓紅連接只出如今左邊 if IsRed(nowNode.Right) { nowNode = RotateLeft(nowNode) } // 連續兩個左連接爲紅色,那麼進行右旋 if IsRed(nowNode.Left) && IsRed(nowNode.Left.Left) { nowNode = RotateRight(nowNode) } // 旋轉後,可能左右連接都爲紅色,須要變色 if IsRed(nowNode.Left) && IsRed(nowNode.Right) { ColorChange(nowNode) } return nowNode } // 對該節點所在的子樹刪除最小元素 func (node *LLRBTNode) DeleteMin() *LLRBTNode { // 輔助變量 nowNode := node // 沒有左子樹,那麼刪除它本身 if nowNode.Left == nil { return nil } // 判斷是否須要紅色左移,由於最小元素在左子樹中 if !IsRed(nowNode.Left) && !IsRed(nowNode.Left.Left) { nowNode = MoveRedLeft(nowNode) } // 遞歸從左子樹刪除 nowNode.Left = nowNode.Left.DeleteMin() // 修復左傾紅黑樹特徵 return nowNode.FixUp() } // 左傾紅黑樹刪除元素 func (tree *LLRBTree) Delete(value int64) { // 當找不到值時直接返回 if tree.Find(value) == nil { return } if !IsRed(tree.Root.Left) && !IsRed(tree.Root.Right) { // 左右子樹都是黑節點,那麼先將根節點變爲紅節點,方便後面的紅色左移或右移 tree.Root.Color = RED } tree.Root = tree.Root.Delete(value) // 最後,若是根節點非空,永遠都要爲黑節點,賦值黑色 if tree.Root != nil { tree.Root.Color = BLACK } } // 對該節點所在的子樹刪除元素 func (node *LLRBTNode) Delete(value int64) *LLRBTNode { // 輔助變量 nowNode := node // 刪除的元素比子樹根節點小,須要從左子樹刪除 if value < nowNode.Value { // 由於從左子樹刪除,因此要判斷是否須要紅色左移 if !IsRed(nowNode.Left) && !IsRed(nowNode.Left.Left) { // 左兒子和左兒子的左兒子都不是紅色節點,那麼無法遞歸下去,先紅色左移 nowNode = MoveRedLeft(nowNode) } // 如今能夠從左子樹中刪除了 nowNode.Left = nowNode.Left.Delete(value) } else { // 刪除的元素等於或大於樹根節點 // 左節點爲紅色,那麼須要右旋,方便後面能夠紅色右移 if IsRed(nowNode.Left) { nowNode = RotateRight(nowNode) } // 值相等,且沒有右孩子節點,那麼該節點必定是要被刪除的葉子節點,直接刪除 // 爲何呢,反證,它沒有右兒子,但有左兒子,由於左傾紅黑樹的特徵,那麼左兒子必定是紅色,可是前面的語句已經把紅色左兒子右旋到右邊,不該該出現右兒子爲空。 if value == nowNode.Value && nowNode.Right == nil { return nil } // 由於從右子樹刪除,因此要判斷是否須要紅色右移 if !IsRed(nowNode.Right) && !IsRed(nowNode.Right.Left) { // 右兒子和右兒子的左兒子都不是紅色節點,那麼無法遞歸下去,先紅色右移 nowNode = MoveRedRight(nowNode) } // 刪除的節點找到了,它是中間節點,須要用最小後驅節點來替換它,而後刪除最小後驅節點 if value == nowNode.Value { minNode := nowNode.Right.FindMinValue() nowNode.Value = minNode.Value nowNode.Times = minNode.Times // 刪除其最小後驅節點 nowNode.Right = nowNode.Right.DeleteMin() } else { // 刪除的元素比子樹根節點大,須要從右子樹刪除 nowNode.Right = nowNode.Right.Delete(value) } } // 最後,刪除葉子節點後,須要恢復左傾紅黑樹特徵 return nowNode.FixUp() } // 驗證是否是棵左傾紅黑樹 func (tree *LLRBTree) IsLLRBTree() bool { if tree == nil || tree.Root == nil { return true } // 判斷樹是不是一棵二分查找樹 if !tree.Root.IsBST() { return false } // 判斷樹是否遵循2-3樹,也就是紅連接只能在左邊,不能連續有兩個紅連接 if !tree.Root.Is23() { return false } // 判斷樹是否平衡,也就是任意一個節點到葉子節點,通過的黑色連接數量相同 // 先計算根節點到最左邊葉子節點的黑連接數量 blackNum := 0 x := tree.Root for x != nil { if !IsRed(x) { // 是黑色連接 blackNum = blackNum + 1 } x = x.Left } if !tree.Root.IsBalanced(blackNum) { return false } return true } // 節點所在的子樹是不是一棵二分查找樹 func (node *LLRBTNode) IsBST() bool { if node == nil { return true } // 左子樹非空,那麼根節點必須大於左兒子節點 if node.Left != nil { if node.Value > node.Left.Value { } else { fmt.Printf("father:%#v,lchild:%#v,rchild:%#v\n", node, node.Left, node.Right) return false } } // 右子樹非空,那麼根節點必須小於右兒子節點 if node.Right != nil { if node.Value < node.Right.Value { } else { fmt.Printf("father:%#v,lchild:%#v,rchild:%#v\n", node, node.Left, node.Right) return false } } // 左子樹也要判斷是不是平衡查找樹 if !node.Left.IsBST() { return false } // 右子樹也要判斷是不是平衡查找樹 if !node.Right.IsBST() { return false } return true } // 節點所在的子樹是否遵循2-3樹 func (node *LLRBTNode) Is23() bool { if node == nil { return true } // 不容許右傾紅連接 if IsRed(node.Right) { fmt.Printf("father:%#v,rchild:%#v\n", node, node.Right) return false } // 不容許連續兩個左紅連接 if IsRed(node) && IsRed(node.Left) { fmt.Printf("father:%#v,lchild:%#v\n", node, node.Left) return false } // 左子樹也要判斷是否遵循2-3樹 if !node.Left.Is23() { return false } // 右子樹也要判斷是不是遵循2-3樹 if !node.Right.Is23() { return false } return true } // 節點所在的子樹是否平衡,是否有 blackNum 個黑連接 func (node *LLRBTNode) IsBalanced(blackNum int) bool { if node == nil { return blackNum == 0 } if !IsRed(node) { blackNum = blackNum - 1 } if !node.Left.IsBalanced(blackNum) { fmt.Println("node.Left to leaf black link is not ", blackNum) return false } if !node.Right.IsBalanced(blackNum) { fmt.Println("node.Right to leaf black link is not ", blackNum) return false } return true } func main() { tree := NewLLRBTree() values := []int64{2, 3, 7, 10, 10, 10, 10, 23, 9, 102, 109, 111, 112, 113} for _, v := range values { tree.Add(v) } // 找到最大值或最小值的節點 fmt.Println("find min value:", tree.FindMinValue()) fmt.Println("find max value:", tree.FindMaxValue()) // 查找不存在的99 node := tree.Find(99) if node != nil { fmt.Println("find it 99!") } else { fmt.Println("not find it 99!") } // 查找存在的9 node = tree.Find(9) if node != nil { fmt.Println("find it 9!") } else { fmt.Println("not find it 9!") } tree.MidOrder() // 刪除存在的9後,再查找9 tree.Delete(9) tree.Delete(10) tree.Delete(2) tree.Delete(3) tree.Add(4) tree.Add(3) tree.Add(10) tree.Delete(111) node = tree.Find(9) if node != nil { fmt.Println("find it 9!") } else { fmt.Println("not find it 9!") } if tree.IsLLRBTree() { fmt.Println("is a llrb tree") } else { fmt.Println("is not llrb tree") } }
運行:
find min value: &{2 0 <nil> <nil> false} find max value: &{113 0 0xc0000941e0 <nil> false} not find it 99! find it 9! 2 3 7 9 10 10 10 10 23 102 109 111 112 113 not find it 9! is a llrb tree
PS:咱們的程序是遞歸程序,若是改寫爲非遞歸形式,效率和性能會更好,在此就不實現了,理解左傾紅黑樹添加和刪除的整體思路便可。
紅黑樹能夠用來做爲字典 Map 的基礎數據結構,能夠存儲鍵值對,而後經過一個鍵,能夠快速找到鍵對應的值,相比哈希表查找,不須要佔用額外的空間。咱們以上的代碼實現只有value
,沒有key:value
,能夠簡單改造實現字典。
Java 語言基礎類庫中的 HashMap,TreeSet,TreeMap 都有使用到,C++ 語言的 STL 標準模板庫中,map 和 set 類也有使用到。不少中間件也有使用到,好比 Nginx,但 Golang 語言標準庫並無它。
最後,上述應用場景使用的紅黑樹都是普通紅黑樹,並非本文所介紹的左傾紅黑樹。
左傾紅黑樹做爲紅黑樹的一個變種,只是被設計爲更容易理解而已,變種只能是變種,工程上使用得更多的仍是普通紅黑樹,因此咱們仍然須要學習普通的紅黑樹,請看下一章節。
我是陳星星,歡迎閱讀我親自寫的 數據結構和算法(Golang實現),文章首發於 閱讀更友好的GitBook。