主頁 >  其他 > 紅黑樹是怎么來的

紅黑樹是怎么來的

2023-05-23 09:03:20 其他

平衡的關鍵

書接前文,

在前文《二叉搜索樹的本質》中我們提到,二叉搜索樹在實踐中有個很大的問題:樹傾斜,按如下順序插入資料會讓二叉搜索樹退化成普通鏈表,進而相關操作的時間復雜度退化到 O(n):

怎樣讓這棵樹在寫操作后仍然保持平衡呢?

R 教授一邊呷著黑咖啡,一邊盯著這棵“畸形”的二叉樹發愣,

“要怎樣才能在添加新節點的時候不會破壞樹的平衡性呢——或許應該這樣問:添加新節點的程序是如何破壞樹的平衡性的?”R 教授緊鎖雙眉喃喃道,

忽然,他兩眼放光,好像發現了什么!

“對!問題就出現在這:向下生長,”R 教授發現,每次添加新節點,都會給現有的葉節點生成新的孩子節點,如此,每次添加一個新節點,都使得樹的高度加 1,最終變成上圖中的鏈表,

“那么,如果不讓這棵樹向下生長呢?”這是個大膽的想法,

比如,添加元素 2 時,不是作為節點 1 的孩子節點,而是和 1 放在一起(按序)呢?像這樣:

如此,樹便不再生長了,繼續:

樹高度是不再生長了,但有個問題:整棵樹只有一個節點了,不就退化成陣列了嗎?

既然它叫樹,就必然需要生長,

“既然它要生長,又不能向下長,便只能向上長了,”

如何讓樹向上生長呢?

回想一下前文《二叉搜索樹的本質》中我們如何通過二分法將有序陣列構造成二叉樹?

我們取有序陣列的中間元素作為父節點 P,將左右子陣列分別作為 P 的左右子樹,通過這種方式我們最終構造出了一棵平衡二叉樹,

這里我們不妨也借鑒這種思想試試,

當節點中元素數量達到 3 個時,我們取中間的元素作為父節點 P,兩邊的元素分別作為 P 的左右子節點,

如上圖,原來包含 3 個元素的節點裂變成了 三個節點,以該節點為根的子樹高度加 1,

為啥選擇在包含 3 個元素的時候裂變,而不是 2 個或者更多呢?我們選擇在奇數元素數的時候裂變,好進行左右對稱操作,3 是自然數中除 1 以外最小的奇數,操作起來足夠簡單(至少理論上是),
我們當然可以選擇在節點包含 n 個元素的時候裂變,

也就是說,在我們設計的這種裂變型的搜索樹中,一個節點最多可以包含 2 個元素,最少包含一個元素,當元素數超過 2 個時便會發生裂變,導致樹(可能)向上生長——向上生長的含義是,裂變程序產生的父節點 P 總是向上冒泡,它可能在上層生成一個新節點(當原本上層沒有節點時),也可能和上層的節點合并(當原本上層有一個單元素節點時),也可能導致上層節點繼續裂變(當上層原本是 2 元素節點時),

在二叉搜索樹中,每個節點 P 最多可以有兩個孩子節點:左孩子的值小于等于 P 的值,右孩子的值大于等于 P 的值,在我們新型搜索樹中,一個節點最多可以有兩個元素,當有兩個元素 a, b 時,是可以表示三個區間的:小于 a 的元素集合 [,a)、介于 a,b 之間的元素集合 [a,b) 以及大于等于 b 的元素集合 [b,),所以,當某節點 P 有兩個元素時,它最多可以有三個子節點(想象成有序陣列中兩個元素分割開的三個區域),

因而,在這種新型搜索樹中,一個節點最多可能有 2 個或者 3 個子節點,我們“望文生義”地給它起個名字就叫 2-3 樹

我們按照上面的思想按序插入 1 ~ 11 的元素,看看最終生成的 2-3 樹是怎樣的,

在上圖中,我們總是將新元素添加到已有的葉節點中,而不是直接創建新節點,所以添加元素本身并不會導致樹高度發生改變(甚至不會導致節點數量發生改變),只是在添加新元素后,可能導致既有節點分裂,進而導致節點數以及數高度發生變化,

進一步,我們發現,只有一種情況會導致樹高度增加:根節點分裂——當根節點填入第三個元素時,會將中間的元素提升作為新根,而該元素左右兩邊的元素分別裂變成左右子樹,該程序是對稱的(左右子樹高度同時加 1),因而無論如何裂變,整棵樹都是平衡的,

也就是說,通過自下而上裂變式生長,真的能保證搜索樹的平衡性(至少插入元素時是這樣)——發現這點后,R 教授樂壞了,趕緊開瓶香檳以示慶祝,


2-3 樹的問題

R 教授一邊呷著香檳一邊試圖實作這種神奇的搜索樹,

不過在實作上他遇到了問題,

他發現這種看似很簡單的資料結構,實作起來特復雜,需要分別考慮 2 節點和 3 節點的情況,寫出來的代碼又臭又長,

他在寫洗掉邏輯的時候終于忍無可忍,將一大瓶香檳扔進了垃圾桶里,

“難道喝高了?不能啊,真理應該是簡單的才對,這坨 SHIT 算個什么東西!”

于是他找來 L 教授幫忙,

“我覺得問題就出現在 2-3 上,它給人一種搖擺不定感,完美的設計應該是對稱的,” L 教授故弄玄虛,

“但我沒有什么好辦法讓它變成二叉樹或者三叉樹,一個節點在成為 3 節點之前必然要先成為 2 節點,所以這兩種節點都必然會實際存在,”

“嗯,所以兩類節點并存屬于邏輯事實,” L 教授盯著面前的 2-3 樹若有所思,

“不過我們能否在實作上消除一類節點呢——我的意思是,能否用 2 節點來表示 3 節點?”

“這是什么鬼?” R 教授疑惑地望著 L 教授,

L 教授解釋道:“無論是 2 節點還是 3 節點,它們在邏輯上都是等價的:都是表示有序序列(有序陣列),而且相互之間很容易轉化,”

我們可以用如下三種代碼來說明情況:

/**
 * 最基本的陣串列示法
 */
var arr = [...[-∞, a), a, ...[a, b), b,  ...[b, ∞)]

/**
 * 2-3 搜索樹的 3 節點表示法
 */
var node = {
  // 本節點元素數量
  size: 2,
  // 元素陣列,升序排列
  elements: [a, b],
  // 子節點陣列
  // node1 子樹中元素范圍:[-∞, a)
  // node2 子樹中元素范圍:[a, b)
  // node3 子樹中元素范圍:[b, ∞)
  children: [node1, node2, node3]
}

/**
 * 二叉搜索樹表示法
 */
var nodeB = {
  element: b,
  left: nodeA,
  // nodeZ 子樹的元素范圍:[b, ∞)
  right: nodeZ
}

var nodeA = {
  element: a,
  // nodeX 子樹的元素范圍:[-∞, a)
  left: nodeX,
  // nodeY 子樹的元素范圍:[a, b)
  right: nodeY
}

L 教授畫出如下草圖:

“如此,便可以用 2 節點來表示 3 節點,只不過我們將 a、b 之間的連線用特殊顏色標記以示區分,也就是說,我們完全可以用二叉樹來表示 2-3 樹!” L 教授語調高亢,得意之神情溢于言表,

為了著重表示左邊的 3 節點如何拆分成右邊的 2 節點,L 教授將 a、b 之間的連線用紅色筆畫出,

“有點意思!” R 教授看到這里兩眼放光,不知是因為喝高了,還是因為興奮,

“所以待分裂的 4 節點可以用兩條紅線來表示,” R 教授順著 L 教授的思路畫了下圖:

如上圖,四節點可以用連續兩條紅線表示 a、b、c 之間的關系,不過由于最終不可能存在 4 節點,也就意味著不可能存在兩條連續的紅線,它最侄訓裂變成 3 個 2 節點,

“不過,這種轉換有何用?我們不過是把 2-3 樹又變成了二叉樹而已,” R 教授興奮過后兩眼又泛起了疑惑,

“我們可以將 2-3 平衡二叉樹的特性應用于等價二叉搜索樹上,如此,理論上便能保持二叉樹的平衡性——既然它是由 2-3 樹轉換來的,”

“不過在繼續之前,我們先做個小小的優化,” L 教授繼續道,“因為我們編碼的時候是基于節點(而不是邊)來編碼的,所以我們可以將邊的顏色轉移給兩端之一的節點——我們決定統一轉給下端節點,”

如下圖:

至此,這棵轉換來的二叉搜索樹中存在兩種節點:紅色節點和黑色節點,為了避免每次都叫“轉換后的二叉樹”,我們給它起個名字,不妨就叫紅黑樹吧,

“很形象的名字!”


紅黑樹的特性

我們前面已經得到了一個特性:由于 2-3 樹中不可能存在 4 節點(擁有 3 個元素,4 個子節點的節點),對應地,紅黑樹中不可能存在兩個連續的紅節點

“對,這是個顯而易見卻十分重要的特性,” R 教授接茬道,“另一個重要問題是:紅黑樹中如何表示 2-3 樹的平衡特性,即所有的葉子節點到根節點的簡單路徑相等,”

L 教授盯著 2-3 樹和轉換后的紅黑樹思索良久,道:“2-3 樹中所有葉子節點到根節點形成的簡單路徑上擁有的節點數量都相等,對于 2 節點,由于和紅黑樹中的(黑色)節點一一對應,不難處理;對于 3 節點,它在紅黑樹中其實由兩個節點——一紅一黑——構成,如果我們將這兩個節點合二為一,那么紅黑樹中從任何葉節點到根節點的簡單路徑上節點數量也應該都是相同的,”

“也就是說,紅黑樹中,從任何葉節點到根節點的簡單路徑上,忽略掉所有紅色節點后,剩下的節點——也就是黑色節點——數量是相同的,”

至此,我們得到了紅黑樹兩條最重要的特性(核心特性):

特性 1: 不能存在兩個連續的紅節點;
特性 2: 從任何葉節點到根節點的簡單路徑上的黑節點數量是相同的;

因為 2-3 搜索樹的任何子樹也是 2-3 搜索樹,所以以上兩條性質對于紅黑樹的任何子樹也都成立,

另外,顯而易見,樹根節點一定是黑色的——因為它的上游不可能有節點跟它組成 2-3 樹中的 3 節點,

特性 3: 樹根節點是黑色的,

L 教授繼續道:“我想再加個人為的限制,前面我們將 2-3 樹轉成二叉樹的時候發現,那條紅線既可以往左傾,也可以往右傾,為了處理方便,我們限制只能往左傾,也就是讓兩個元素中大的那個作為父節點(黑色),小的作為左子節點(紅色),這樣的紅黑樹不妨叫左傾紅黑樹,”

特性 4: 紅節點必須是其父節點的左子節點,

最后,我們將一棵 2-3 樹用紅黑樹完整表示出來:

“嗯,性質 1 和 2 確實滿足,不過有個大問題:轉成紅黑樹后,樹的平衡性被破壞了!” R 教授盯著這棵紅黑樹大失所望,

“的確,3 節點轉成二叉樹中的兩個 2 節點后,有可能增加樹高度,因為二叉樹本質上仍然是自上而下生長的(而非自下而上裂變的),所以注定了它不可能是一棵完美的平衡樹,我們所追求的是實踐意義上的近似平衡性,” L 教授略顯尷尬地解釋道,

“嗯,那就將真理交給實踐去檢驗吧,現在我們不妨探索如何添加和洗掉元素,”


插入元素

先看看插入元素 1.5,

在 2-3 樹中的插入操作:

轉成紅黑樹:

貌似很簡單(先不管紅黑樹中為何 1 和 1.5 調換了位置),

再看看插入元素 11,

首先找到相關葉節點 node(9,10),插入 11 得到 node(9,10,11),這是個 4 節點,會發生分裂,將 10 提升到上層構成 node(6,8,10),這仍然是個 4 節點繼續發生分裂,最終波及到根節點,如圖:

在這個例子中,無論 2-3 樹還是紅黑樹的結構都發生了很大的變化,看來這“級聯炸彈”威力不小,

“雖然我們總能夠先操作 2-3 樹,然后將結果映射到紅黑樹,但這在實作上并不可行——到目前為止我尚未發現將 2-3 樹轉換成紅黑樹其實作意義何在,” R 教授一本正經,

“的確,如果紅黑樹不能降低 2-3 樹在實作上的復雜性,便沒有半毛錢的意義,所以我們必須從二叉搜索樹本身來解決元素操作問題,不過,先容我喝杯咖啡提提神,” L 教授說著走向咖啡機,


神奇的三板斧

喝完咖啡,聽了段莫扎特,兩位教授繼續研究紅黑樹,

插入或者洗掉節點后,必須對紅黑樹執行一系列操作來維護紅黑樹的特性,由于紅黑樹是二叉搜索樹,所以操作后必須同時滿足二叉搜索樹的性質,

“我發現前面插入元素 11 后,雖然導致整棵紅黑樹結構變化很大,不過還是有規律的,我感覺好像將節點 4 圍繞節點 8 逆時針旋轉了一下,將 4 和 8 調換了父子關系,然后將 8 的左子節點給 4,整個二叉搜索樹性質并未改變,” R 教授驚喜的說到,

“對!這真是個奇怪但有用的性質,我們不妨稱它為左旋吧——因為被旋的節點在軸的左邊,” L 教授補充道,

對應地就有右旋:

左右旋能保證二叉搜索樹性質不變(請仔細分析上兩圖中字母大小關系,其中橢圓節點表示子樹),如果在旋轉中同時保證所涉及路徑上的黑色節點數量不變,則旋轉操作便可以用于維護紅黑樹性質,

另外還有一種顯而易見的操作:顏色翻轉,如圖:

上圖左側,假設所有葉節點到根節點構成的簡單路徑上黑節點數量都相同,但 9 和 10 都是紅節點,不符合特性 1,由于黑節點 8 的兩個子節點都是紅節點,我們將黑節點 8 和它兩個子節點顏色交換,整棵樹仍然滿足紅黑樹性質,

接下來我們看看能否通過上面介紹的左旋、右旋和顏色翻轉來維護紅黑樹的性質(包括二叉搜索樹的性質),


細談插入

當我們向紅黑樹中插入一個新元素時,首先要決定該元素的初始顏色,

假設新節點初始化為黑色,那么它必然會導致某條路徑的黑色節點數加 1,進而破壞紅黑樹性質,

如果初始化為紅色,那么它本身不會導致黑節點增加(不違反特性 2),但可能會違反特性 1,即出現兩個連續的紅色節點(當其父節點也是紅色時),但也有可能不違反任何特性,

所以我們決定讓新節點初始化為紅色,然后試圖通過“三板斧”來修復特性 1,

場景一:待插入節點的父節點是黑色(有兩種情況):

注意上圖第二種情況,左旋后,由于 x 原本的黑色轉移給了其父節點 y,自己變成了紅色,所有經過 x 的簡單路徑(也必然經過 y)上的黑節點數并沒有發生變化,

場景二:待插入節點的父節點是紅色(也有兩種情況,不過我們可以通過左旋操作讓其變成一種情況):

我們將 x 的父節點 P 圍繞 x 右旋看看情況:

如上圖,右旋并適當調整節點顏色后,在保持二叉搜索樹性質的前提下, α 子樹和 β 子樹中任何葉節點到根節點簡單路徑上的黑節點數量并沒有發生變化,而且就圖中幾個節點而言,不再有兩個連續的紅節點了,

不過,我們得考察一下 α 子樹和 β 子樹,

由于原先 x 是紅節點,所以 β 子樹的樹根必然是黑節點,然而 α 子樹的樹根既可能是黑節點也可能是紅節點,

如果 α 子樹的樹根是紅節點,則節點 P 的顏色違反了特性 1,

我們再觀察節點 x、a、P 的顏色關系,發現可以翻轉:

如圖,翻轉顏色后,不會改變 x 子樹中任何一條路徑上黑節點數量,而且解決了 P 和 α 子樹可能的顏色沖突,

不過,細心的你肯定發現問題了,顏色翻轉并沒有解決問題,只是將可能的沖突從下面轉移到了上面:翻轉后,x 的顏色有可能跟其父節點顏色沖突(同為紅色),

另外,如果 x 是 x.parent 的右子節點,則違反了特性 3——不過這一點倒可以通過左旋解決,

因而,翻轉顏色后,我們還要判斷 x.parent 的顏色,如果是黑色則萬事大吉,倘若是紅色,則需要遞回處理,直到整棵樹的根節點,

透過現象看本質,場景二(連續兩個紅節點)情況下,我們的目的是讓兩個紅節點向上合并成一個紅節點以消除沖突,向上合并的程序有可能造成新的沖突(新的連續紅節點),所以處理程序是遞回的,有可能一直遞回到整棵樹的根節點,該向上合并的程序,類比到 2-3 樹就是向上裂變的程序(因為連續兩個紅節點對應到 2-3 樹上就是一個待裂變的 4 節點),裂變也是遞回的,最終可能波及到根節點,

紅節點合并與裂變的對應關系如下:

上圖中一些特殊情況:

  1. 如果 P 節點不存在,則 b 會成為新的根(在紅黑樹中,需要將它再變成黑色);
  2. 否則,在 2-3 樹中,b 提升后至少會形成 3 節點,對應紅黑樹中紅色 b 節點;
  3. b 提升后也可能形成 4 節點(當原本的 P 就是 3 節點時),此時會繼續裂變,對應到紅黑樹中就是紅色 b 的父節點仍然是紅色;

最后一個問題是,當紅色合并操作遞回到整棵樹的根節點時會發生什么?

答案是:直接將根節點變成黑色,然后結束戰斗,

我們看看遞回到根節點時的幾種情況:

無論是哪種情況,將根節點變成黑色后都仍然滿足紅黑樹的性質:

  1. 不存在連續的紅節點;
  2. 當原本根節點是黑色時,自然沒問題;
  3. 當原本根節點時紅色時,變成黑色后,所有路徑上黑節點數都加 1,仍然滿足紅黑樹的性質;

再談三板斧

從上面例子可知,當我們想將右傾紅節點變成左傾紅節點以滿足特性 4 時要用到左旋,旋轉后,父節點位置的顏色保持不變,原先右邊的紅色跑道左邊去了,如圖:

代碼實作(TypeScript):

/**
 * 將 h 圍繞其右子節點左旋,
 * @returns 回傳新的父節點
 */
function rotateLeft(h: Node): Node {
    assert(h && h.right)

    const x = h.right
    h.right = x.left
    x.left = h

    x.color = h.color
    h.color = 'RED'

    return x
}

對稱地,右旋如下:

代碼實作:

/**
 * 將 h 圍繞其左子節點右旋,
 * @returns 回傳新的父節點
 */
function rotateRight(h: Node): Node {
    assert(h && h.left)

    const x = h.left
    h.left = x.right
    x.right = h

    x.color = h.color
    h.color = 'RED'

    return x
}

顏色翻轉代碼實作:

/**
 * 將 h 和其兩個子節點執行顏色翻轉
 * 要求 h 的兩個子節點顏色相同而且和 h 的顏色不同
 */
function flipColors(h: Node) {
    assert(h && h.left && h.right)
    assert(h.left.color === h.right.color && h.left.color !== h.color)

    const hColor = h.color
    h.color = h.left.color
    h.left.color = hColor
    h.right.color = hColor
}

經分析發現,以上三種操作既不會破壞二叉搜索樹的性質,也不會導致任何一條“葉-根”路徑上黑節點數量發生改變,近乎完美——除了可能導致出現兩個連續的紅節點(破壞特性 1),因而實際使用中需要自下而上遞回處理,


插入操作的代碼實作

至此,我們完整實作一下紅黑樹的元素插入代碼:

interface Value {
    key: number;
    val: unknown;
}

/**
 * 定義樹的節點
 */
interface Node extends Value {
    left: Node | null;
    right: Node | null;
    color: 'RED' | 'BLACK';
}

class RedBlackTree {
    protected root: Node | null
    // 樹節點數量
    protected _size: number

    public constructor() {
        this.root = null
        this._size = 0
    }

    /**
     * 插入元素
     * 從根節點往下找,直到找到合適的位置后插入
     * 
     * @param key - 關鍵字
     * @param val - 衛星資料
     */
    public insert(key: number, val: unknown) {
        this.root = this.innerInsert(this.root, { key, val })
        // 處理完成后,將根節點變成黑色,結束戰斗
        this.root.color = 'BLACK'
        this._size++
    }

    /**
     * 遞回地往 p 子樹插入元素 value,并維護紅黑樹性質
     * @param p 
     * @param value
     * @returns 回傳子樹 p 新的根(插入并維護紅黑性質后,新的根節點不一定還是原來的那個節點了)
     */
    private innerInsert(p: Node | null, value: Value): Node {
        if (p === null) {
            // p 子樹是個空指標(空樹),創建并回傳新節點
            return this.newNode(value.key, value.val)
        }

        // p 是非空子樹,則視情況將 value 插入到 p 的左子樹或者右子樹中
        if (value.key < p.key) {
            p.left = this.innerInsert(p.left, value)
        } else {
            p.right = this.innerInsert(p.right, value)
        }

        // 修復 p 子樹的紅黑性質
        return this.balance(p)
    }

    /**
     * 對以 p 為根的子樹執行紅黑平衡修復讓該子樹符合紅黑樹的性質
     * 注意:情況一、二、三必須按順序執行,因為前面情況的解決可能帶來后面的情況
     * @param p 子樹的根
     * @returns 修復完成后回傳新的根(不一定是原先那個 p 了)
     */
    private balance(p: Node): Node {
        // 情況一(右傾),執行左旋
        if (!this.isRed(p.left) && this.isRed(p.right)) {
            p = this.rotateLeft(p)
        }

        // 情況二:連續兩個左傾的紅節點
        if (this.isRed(p.left) && this.isRed(p.left.left)) {
            p = this.rotateRight(p)
        }

        // 情況三:一個黑節點下面掛兩個紅節點,其中右邊的紅節點違反了“左傾”原則,通過翻轉顏色解決
        if (this.isRed(p.left) && this.isRed(p.right)) {
            this.flipColors(p)
        }

        return p
    }

    /**
     * 新節點默認是紅色
     */
    protected newNode(key: number, val: unknown): Node {
        return { key, val, left: null, right: null, color: 'RED', size: 1 }
    }

    /**
     * 讓節點 h 相對于 h.right 執行左旋
     */
    protected rotateLeft(h: Node): Node {
        // 見前面實作
    }

    /**
     * 讓節點 h 相對于 h.left 執行右旋
     */
    protected rotateRight(h: Node): Node {
        // 見前面實作
    }

    /**
     * 翻轉顏色
     */
    protected flipColors(h: Node) {
        // 見前面實作
    }

    /**
     * 判斷節點 node 是否為紅色
     * 規定 null 節點是黑色
     */
    protected isRed(node: Node | null): boolean {
        if (!node) {
            return false
        }

        return node.color === 'RED'
    }
}

洗掉

樹形資料結構中,洗掉操作總是最復雜的,紅黑樹也不例外,

“初步看,洗掉得分兩種情況:被洗掉的節點是葉子節點,或者是非葉子節點......”

“你等等,我好像發現了個重大問題!” R 教授突然打斷了 L 教授的滔滔不絕,

“啥?”被這樣硬生生地打斷,L 教授顯然有些不爽,

“你說葉子節點——這里有問題!” R 教授指著下面這棵紅黑樹:

現在刪掉葉子節點 7,會怎樣?

“嗯......不會怎樣,仍然滿足紅黑樹全部特性,換句話說,它仍然是一棵紅黑樹,” L 教授若有所思,他好像也意識到了什么,

“這正是問題所在,這棵樹刪掉節點 7 后仍然是一棵合法的紅黑樹,但它卻無法轉換成 2-3 樹!” R 教授道,

所以我們必須保證任何一條路徑不會因為刪掉一個節點而消失,上圖中如果路徑(7-6-8-4)不因洗掉 7 而消失,那么刪掉 7 以后這個路徑便少了一個黑節點,從而不再滿足紅黑樹的性質,

“或許我們可以引入哨兵——具體來說,我們讓每個實節點都必須有兩個子節點,如果沒有,則用哨兵節點代替,為了不讓哨兵的顏色影響實節點的顏色,哨兵節點必須是黑色(如實節點是紅色,如果哨兵也是紅色,則會違反特性 1),” L 教授說道,

“這是個好主意,既然這些哨兵都出現在空節點的位置上,不妨就稱這些哨兵為 nil 節點吧,” R 教授說著畫出添加哨兵后的紅黑樹:

也就是說,特性 2 所提及的“葉節點”實際上是指哨兵節點(nil),而不是最下面的實節點,

2-3 樹必須是完全平衡的搜索樹,而上面這個(刪掉節點 7 后)有問題的 2-3 樹并不是一棵平衡樹,不過這點從圖中并不能明顯地看出來,如果轉成代碼則較明顯:

var node = {
 // 本節點元素數量
 size: 2,
 // 元素陣列,升序排列
 elements: [6, 8],
 // 子節點陣列
 // node1 的值是 [5]
 // node2 是 null
 // node3 的值是 [9, 10]
 children: [node1, null, node3]
}

如上表示節點 node(6,8),從 children 可見,第二個子節點是 null,而其它兩個均有實際子節點,因而這里不再平衡,
所以,實際上在設計 2-3 樹的時候就應該引入哨兵節點的概念了,如果 2-3 樹引入了哨兵節點,那么轉換為紅黑樹后,自然將哨兵的概念帶過來了,

那么,我們如何洗掉上面的節點 7 呢?

我們知道,對于一條路徑,洗掉路徑上的紅色節點不會影響路徑上黑色節點數量,

所以,如果我們能先將節點 7 變成紅色,然后再洗掉就行了,

如果節點 7 本身就是紅色的,那么直接洗掉就行了,

如果節點 7 是黑色的(如上圖所示),怎樣將 7 變成紅色節點呢?

我們只看節點 7 到根節點這條路徑:

我們從這條路徑的上游找一個紅色節點(圖中的 6)和節點 7 互換顏色,如此節點 7 變成了紅色,且整條路徑上黑色節點數量不變,然后再洗掉 7 即可,

不過,由于節點 6 變成了黑色,6 的左子樹中所有路徑的黑色節點數都多了一個,因而需要將節點 6 的左子結點也變成紅色(即對節點 6 和其子節點執行 flipColors 操作),

然而,如果整條路徑上本來一個紅節點都沒有咋辦呢?

可以將根節點變成紅色——這相當于整棵樹所有“葉-根”路徑中黑節點數都減 1,紅黑性質不變,然后在向下轉移的程序中,根節點自然又變成黑色了,

如果要洗掉的節點不是葉子節點呢?比如如何洗掉節點 8?

在前文二叉搜索樹中我們提過如何洗掉非葉子節點:將待洗掉節點和其后繼節點(即比它大的最小節點)互換值,然后問題變為洗掉該后繼節點——紅黑樹中后繼節點是可以轉換為葉子節點(這里指沒有任何子節點的實節點)的,

此處只是描述了節點洗掉的大體邏輯思想,具體實作仍然是用前面提到的“三板斧”,而且實作代碼要比插入邏輯復雜,此處不再給出完整代碼實作,感興趣可參見本人 github https://github.com/linzier/algo-ts 中紅黑樹的實作,


搜索

由于紅黑樹是二叉搜索樹,所以可以直接用二叉搜索樹的搜索邏輯實作,也就是說,節點的顏色并不影響搜索邏輯——這正是紅黑樹的一大優勢,


總結

至此,我們在兩位教授的幫助下,大體厘清了紅黑樹的演化程序:

  1. 找到二叉樹傾斜的原因:向下生長;
  2. 阻止傾斜的策略:改變生長方向——采用裂變式向上生長策略;
  3. 基于 2,演化出 2-3 樹;
  4. 由于 2-3 樹實作上的復雜性,引出用二叉樹表示 2-3 樹的想法——紅黑樹;
  5. 基于 2-3 樹的相關特性,總結出左傾紅黑樹的 4 條特性,其中特性 1 和特性 2 是核心特性;
  6. 基于二叉搜索樹的特性,發現三個核心操作:左旋、右旋和顏色翻轉——有了這三板斧,便可以完全脫離 2-3 樹來處理紅黑樹的平衡性,至此紅黑樹有了實作上的可能;

紅黑樹的完整實作(TypeScript 版本)見本人 github:https://github.com/linzier/algo-ts,

本文來自博客園,作者:林子er,轉載請注明原文鏈接:https://www.cnblogs.com/linvanda/p/17400505.html

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/553096.html

標籤:其他

上一篇:解密Prompt7. 偏好對齊RLHF-OpenAI·DeepMind·Anthropic對比分析

下一篇:返回列表

標籤雲
其他(159482) Python(38162) JavaScript(25441) Java(18096) C(15230) 區塊鏈(8267) C#(7972) AI(7469) 爪哇(7425) MySQL(7204) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5871) 数组(5741) R(5409) Linux(5340) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4574) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2433) ASP.NET(2403) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) .NET技术(1975) 功能(1967) Web開發(1951) HtmlCss(1940) C++(1919) python-3.x(1918) 弹簧靴(1913) xml(1889) PostgreSQL(1878) .NETCore(1861) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 網閘典型架構簡述

    網閘架構一般分為兩種:三主機的三系統架構網閘和雙主機的2+1架構網閘。 三主機架構分別為內端機、外端機和仲裁機。三機無論從軟體和硬體上均各自獨立。首先從硬體上來看,三機都用各自獨立的主板、記憶體及存盤設備。從軟體上來看,三機有各自獨立的作業系統。這樣能達到完全的三機獨立。對于“2+1”系統,“2”分為 ......

    uj5u.com 2020-09-10 02:00:44 more
  • 如何從xshell上傳檔案到centos linux虛擬機里

    如何從xshell上傳檔案到centos linux虛擬機里及:虛擬機CentOs下執行 yum -y install lrzsz命令,出現錯誤:鏡像無法找到軟體包 前言 一、安裝lrzsz步驟 二、上傳檔案 三、遇到的問題及解決方案 總結 前言 提示:其實很簡單,往虛擬機上安裝一個上傳檔案的工具 ......

    uj5u.com 2020-09-10 02:00:47 more
  • 一、SQLMAP入門

    一、SQLMAP入門 1、判斷是否存在注入 sqlmap.py -u 網址/id=1 id=1不可缺少。當注入點后面的引數大于兩個時。需要加雙引號, sqlmap.py -u "網址/id=1&uid=1" 2、判斷文本中的請求是否存在注入 從文本中加載http請求,SQLMAP可以從一個文本檔案中 ......

    uj5u.com 2020-09-10 02:00:50 more
  • Metasploit 簡單使用教程

    metasploit 簡單使用教程 浩先生, 2020-08-28 16:18:25 分類專欄: kail 網路安全 linux 文章標簽: linux資訊安全 編輯 著作權 metasploit 使用教程 前言 一、Metasploit是什么? 二、準備作業 三、具體步驟 前言 Msfconsole ......

    uj5u.com 2020-09-10 02:00:53 more
  • 游戲逆向之驅動層與用戶層通訊

    驅動層代碼: #pragma once #include <ntifs.h> #define add_code CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS) /* 更多游戲逆向視頻www.yxfzedu.com ......

    uj5u.com 2020-09-10 02:00:56 more
  • 北斗電力時鐘(北斗授時服務器)讓網路資料更精準

    北斗電力時鐘(北斗授時服務器)讓網路資料更精準 北斗電力時鐘(北斗授時服務器)讓網路資料更精準 京準電子科技官微——ahjzsz 近幾年,資訊技術的得了快速發展,互聯網在逐漸普及,其在人們生活和生產中都得到了廣泛應用,并且取得了不錯的應用效果。計算機網路資訊在電力系統中的應用,一方面使電力系統的運行 ......

    uj5u.com 2020-09-10 02:01:03 more
  • 【CTF】CTFHub 技能樹 彩蛋 writeup

    ?碎碎念 CTFHub:https://www.ctfhub.com/ 筆者入門CTF時時剛開始刷的是bugku的舊平臺,后來才有了CTFHub。 感覺不論是網頁UI設計,還是題目質量,賽事跟蹤,工具軟體都做得很不錯。 而且因為獨到的金幣制度的確讓人有一種想去刷題賺金幣的感覺。 個人還是非常喜歡這個 ......

    uj5u.com 2020-09-10 02:04:05 more
  • 02windows基礎操作

    我學到了一下幾點 Windows系統目錄結構與滲透的作用 常見Windows的服務詳解 Windows埠詳解 常用的Windows注冊表詳解 hacker DOS命令詳解(net user / type /md /rd/ dir /cd /net use copy、批處理 等) 利用dos命令制作 ......

    uj5u.com 2020-09-10 02:04:18 more
  • 03.Linux基礎操作

    我學到了以下幾點 01Linux系統介紹02系統安裝,密碼啊破解03Linux常用命令04LAMP 01LINUX windows: win03 8 12 16 19 配置不繁瑣 Linux:redhat,centos(紅帽社區版),Ubuntu server,suse unix:金融機構,證券,銀 ......

    uj5u.com 2020-09-10 02:04:30 more
  • 05HTML

    01HTML介紹 02頭部標簽講解03基礎標簽講解04表單標簽講解 HTML前段語言 js1.了解代碼2.根據代碼 懂得挖掘漏洞 (POST注入/XSS漏洞上傳)3.黑帽seo 白帽seo 客戶網站被黑帽植入劫持代碼如何處理4.熟悉html表單 <html><head><title>TDK標題,描述 ......

    uj5u.com 2020-09-10 02:04:36 more
最新发布
  • 紅黑樹是怎么來的

    本文從二叉搜索樹傾斜的原因(自上而下生長)出發,推出維持樹形資料結構平衡性的關鍵:自下而上裂變式生長,進而引出裂變式生長的理論模型:2-3 樹。由于 2-3 樹實作上的復雜性,引出其實作上的替代品:紅黑樹。最后,我們討論如何通過左旋、右旋以及顏色翻轉這“三板斧”來維護紅黑樹插入和洗掉元素后的動態平衡... ......

    uj5u.com 2023-05-23 09:03:20 more
  • 解密Prompt7. 偏好對齊RLHF-OpenAI&#183;DeepMind&#183;Anthropi

    RLHF是針對有用,無害,事實性等原則,把模型輸出和人類偏好進行對齊的一種方案。以OpenAI為基礎,本章會對比DeepMind, Anthropic在RLHF步驟中的異同,試圖理解RLHF究竟做了啥 ......

    uj5u.com 2023-05-23 09:02:10 more
  • AIGC持續火爆大模型爭相推出,龐大市場造就算力供應模式演變

    本圖由AI生成 黃仁勛說的AI發展迎來iPhone時刻,對NVIDIA有什么影響? 文/王吉偉 近期的AIGC領域仍舊火爆例外。 但火的不只是AIGC應用,還有巨頭之間的AI競賽,以及接連不斷上新的AI大模型(LLM,Large Language Model)。 面對ChatGPT帶來的技術沖擊,為 ......

    uj5u.com 2023-05-23 09:01:07 more
  • 機器學習資料順序隨機打亂:Python實作

    本文介紹基于**Python**語言,實作機器學習、深度學習等模型訓練時,**資料集打亂**的具體操作。 # 1 為什么要打亂資料集 在機器學習中,如果不進行資料集的打亂,則可能導致模型在訓練程序中出現具有“**偏見**”的情況,降低其泛化能力,從而降低訓練精度。例如,如果我們做深度學習的分類,其中 ......

    uj5u.com 2023-05-23 09:01:02 more
  • 摳圖黨福音:教你一鍵分割影像

    摘要:輸入一個影像,通過Segment Anything模型即可獲得影像所有目標的分割點位置,再通過位置將影像進行分割保存。 本文分享自華為云社區《一鍵分割影像》,作者:雨落無痕 。 Segment Anything Segment Anything Model(SAM)通過點或框等輸入提示生成高質 ......

    uj5u.com 2023-05-23 09:00:49 more
  • [paper reading]|LinK: Linear Kernel for LiDAR-based 3D Perce

    摘要 將2D大核的成功推廣到3D感知具有挑戰性,因為: 1.處理3D資料的三次增加的開銷; 2. 資料的稀缺性和稀缺性給優化帶來了困難。 以前的作業通過引入塊共享權重,已經邁出了將內核大小從3 × 3 × 3尺度到7×7×7的第一步。但是,為了減少塊內的特征變化,它只使用了適度的塊大小,并沒有獲得像 ......

    uj5u.com 2023-05-23 08:55:20 more
  • 云原生周刊:2023 年可觀測性狀態報告發布 | 2023.5.22

    Splunk 與 Enterprise Strategy Group 合作發布了 State of Observability 2023,這是一份年度全球研究報告,探討了可觀測性在管理當今日益復雜的技識訓境中的作用。該報告將可觀測性領導者定義為具有至少 24 個月的可觀察性經驗的組織。 此外,領導者 ......

    uj5u.com 2023-05-23 08:48:44 more
  • 理論+實操,帶你了解多沙箱容器運行時Kuasar

    摘要:華為云DTSE技術布道師張天陽結合沙箱容器發展歷程,介紹華為云多沙箱容器運行時 Kuasar 專案優勢,開啟多沙箱容器運行時上手實踐體驗。 本文分享自華為云社區《理論+實操,帶你了解多沙箱容器運行時Kuasar》,作者:華為云社區精選。 本期《多沙箱容器運行時Kuasar開發上手實踐》主題直播 ......

    uj5u.com 2023-05-23 08:48:34 more
  • GPS北斗校時服務器(時間同步裝置)助力橋梁檢測系統建設

    GPS北斗校時服務器(時間同步裝置)助力橋梁檢測系統建設 GPS北斗校時服務器(時間同步裝置)助力橋梁檢測系統建設 京準電子科技官微——ahjzsz 一、系統概述 整個采集系統分散在橋梁的各個部位。橋梁按照區域劃分為若干區段,在主要幾個區段中安置著信號采集機站,每組采集機站均和GPS校時器相連,GP ......

    uj5u.com 2023-05-23 08:48:17 more
  • unity學習日志3(麥扣老師3DRPG專案學習)

    ##1.Shader Graphy基本使用 ![](https://img2023.cnblogs.com/blog/3163322/202305/3163322-20230522202712286-142074988.png) 1. 利用unity自帶的菲利涅效果通過Multiply用Color使 ......

    uj5u.com 2023-05-23 08:47:55 more