通俗易懂的紅黑樹圖解(下)

政採雲前端團隊發表於2020-04-07

通俗易懂的紅黑樹圖解(下)

這是第 45 篇不摻水的原創,想獲取更多原創好文,請掃 ?上方二維碼關注我們吧~
本文首發於政採雲前端團隊部落格:通俗易懂的紅黑樹圖解(下)

通俗易懂的紅黑樹圖解(下)

前言

回顧一下通俗易懂的紅黑樹圖解(上),上篇首先介紹了二叉樹的定義以及二叉樹的查詢,然後介紹了紅黑樹的五點性質以及紅黑樹的變色、左旋以及右旋等操作,最後結合變色、左旋及右旋詳細講解了插入節點的五種場景。而本篇通俗易懂的紅黑樹圖解(下)是在上篇的基礎上講解紅黑樹最後一種操作-刪除節點,刪除節點相對插入節點會複雜一點,但通過分類歸納出不同的場景,能更容易理解和記憶。

○ 紅黑樹刪除

紅黑樹刪除操作包括兩部分,一是查詢到刪除節點,二是刪除節點以及刪除之後的自平衡。查詢節點與二叉樹的查詢方式一樣。而刪除操作,當刪除節點不存在時,結束本次刪除操作;當刪除節點存在時,刪除節點,然後找到一個節點替換已刪除的節點位置,重新連線上已刪除節點的父節點與孩子節點。

如下圖,刪除節點 D ,需要找到一個節點可以替換到 D 節點位置,否則節點 P 和節點 L 及 R 之間的連結會斷開,破壞了紅黑樹的性質,形成獨立的樹形結構。

image-20200301173609870.png

關鍵字:查詢節點 替換節點

○ 查詢節點

查詢刪除節點與二叉樹查詢節點邏輯相同,通過與當前節點值比較,返回當前節點或者繼續從左子樹或者右子樹繼續查詢。

在二叉查詢樹中查詢節點 N ,首先從根節點開始,將根節點設定為當前節點,若當前節點為空,則查詢失敗,若 N 與當前節點值相等,返回當前節點,若 N 大於當前節點值,則從當前節點的右子節點開始查詢,否則從當前節點的左子節點開始查詢,直到返回目標節點或者查詢失敗;

圖片

○ 替換節點

回顧一下二叉查詢樹的性質:

  • 若任意節點左子樹不為空,它的左子樹上所有節點值均小於它的根節點的值
  • 若任意節點的右子樹不為空,它的右子樹上所有節點的值均大於它的根節點的值

根據二叉查詢樹的性質,刪除節點之後,可以找到兩個替換節點,即可以用左子樹中的最大值以及右子樹中的最小值來替換刪除節點。

刪除節點找替換節點又分三種情景:

  • 情景1:刪除節點無子節點,可以直接刪除,無需替換
  • 情景2:刪除節點只有一個子節點,用子結點替換刪除節點
  • 情景3:刪除節點有兩個子節點,可以用後繼節點或者前繼節點替換刪除節點。本文采用前者,即後繼節點替換刪除節點

後繼節點:刪除節點的右子樹中的最小節點,即右子樹中最左節點。

前繼節點:刪除節點的左子樹中最大節點,即左子樹中最右節點。

綜上所述,尋找一個節點替換已刪除節點位置,在不考慮節點值情況下,可等同於刪除替換節點

○ 節點刪除

刪除節點可等同於刪除替換節點,所以節點刪除就轉換到了替換節點的各種場景。節點刪除又分 9 種場景,在如下的描述場景中,場景 2 中的四種情況與場景 3 中的四種情況分別互為映象,可參照對比著看。

  • 刪除場景1:替換節點是紅色節點

    即替換的節點是紅色節點,刪除之後不影響紅黑樹的平衡,只需要把替換節點的顏色設成被刪除節點的顏色即可重新平衡。

處理: 刪除節點D,查詢到替換節點R,R設成D節點的顏色,再替換D節點位置。

image-20200229234617585.png

  • 刪除場景 2:替換節點是黑色節點、且是其父節點的左子節點

    替換節點是黑色節點時,刪除之後破壞了紅黑樹的平衡,需要考慮自平衡處理。而此又細分為 4 種場景。

    • 場景 2.1:替換節點的兄弟節點是紅色。刪除黑色節點,左子樹中黑色節點數減少一個,可以通過一些操作,達到間接借用紅色的兄弟節點來補充左子樹中黑色節點數。

    處理:替換節點的父節點 P 設定紅色、兄弟節點 S 設定成黑色,再對節點 P 左旋操作,變成場景 2.4。

    image-20200229211126273.png

    • 場景2.2:替換節點的兄弟節點是黑色且兄弟節點的右子節點是紅色、左子節點任意顏色。同樣是間接借用兄弟節點的紅色右子節點補充到左子樹中,達到紅黑樹的平衡。

    處理:替換節點的兄弟節點 S 設定成父節點P的顏色,兄弟節點的右子節點 SR 設定為黑色,父節點P設定為黑色,再對節點 P 左旋操作。此時節點R替換到刪除節點位置之後,紅黑樹重新達到平衡狀態。

    image-20200229214756335.png

    • 場景2.3:替換節點的兄弟節點是黑色且兄弟節點的左子節點是紅色,右子節點是黑色。

    處理:替換節點的兄弟節點 S 設定成紅色,兄弟節點的左子節點 SL 設定為黑色,再對節點 S 右旋操作,轉換到了場景 2.2,再進行場景 2.2 的操作。

    image-20200229220440371.png

    • 場景2.4:替換節點的兄弟節點的左右子節點都是黑色。兄弟節點的子節點不能借用,就只能借用兄弟節點了。

    處理:替換節點的兄弟節點 S 設定成紅色,以父節點 P 當作替換節點,然後自底向上處理。

    image-20200229222019042.png

  • 場景3:替換節點是黑色節點、且是其父節點的右子節點。(與場景 2 映象)

    • 場景3.1:替換節點的兄弟節點是紅色。

      處理:替換節點的父節點 P 設定紅色、兄弟節點 S 設定成黑色,再對節點 P 右旋操作,變成場景3.4。

    image-20200229223345697.png

    • 場景3.2:替換節點的兄弟節點是黑色且兄弟節點的左子節點是紅色、右子節點任意顏色。

      處理:替換節點的兄弟節點 S 設定成父節點 P 的顏色,兄弟節點的左子節點 SL 設定為黑色,父節點P 設定為黑色,再對節點 P 右旋操作。此時節點 R 替換到刪除節點位置之後,紅黑樹重新達到平衡狀態。

      image-20200229233258336.png

    • 場景3.3:替換節點的兄弟節點是黑色且兄弟節點的右子節點是紅色、左子節點為黑色。

      處理:替換節點的兄弟節點 S 設定成紅色,兄弟節點的右子節點 SL 設定為黑色,再對節點S左旋操作,轉換到了場景 3.2,再進行場景 3.2 的操作。

    image-20200301212153602.png

    • 場景3.4:替換節點的兄弟節點的左右子節點都是黑色。

      處理:替換節點的兄弟節點 S 設定成紅色,以父節點 P 當作替換節點,然後自底向上處理。

      image-20200229234250179.png

節點刪除及平衡程式碼:

 /**
  * 查詢節點 
  * @param key 節點key值
  */
search(key) {
  let node = this.root
  while (node) {
    if (key < node.key) {
      node = node.left
    } else if (key > node.key) {
      node = node.right
    } else if (key === node.key) {
      break
    }
  }
  return node
}

/**
 * 替換u節點,重置v節點
 * @param u 待刪除節點
 * @param v 子節點
 */
const replace = function(u, v) {
  if(!u.parent){
    // u是根節點,設定v為根節點
    this.root = v
  } else if(u === u.parent.left){
    // 重置u的父節點的左節點
    u.parent.left = v
  } else {
    // 重置u的父節點的右節點
    u.parent.right = v
  }
  // 重置v的父節點
  v.parent = u.parent
}

 /**
  * 查詢node節點的後繼節點
  */
  findSuccessor(node) {
    while (node.left) {
      node = node.left;
    }
    return node;
  }

/**
 * 刪除節點
 * @param key 刪除節點key值
 */
delete(key) {
  const node = search(key)
  if(!node){
    return
  }
  let fix
  let color = node.color
  if(!node.left){
    //左節點為空值
    fix = node.right
    this.replace(node, node.right)
  } else if(!node.right){
    //右節點為空值
    fix = node.left
    this.replace(node, node.left)
  } else {
    // 左右節點都不為空值
    const successor = this.findSuccessor(node.right)
    //替換節點的顏色
    color = successor.color
    //後繼節點只存在右節點或者兩個nil子節點情況
    fix = successor.right
    //如果後繼節點是父節點的非直接子節點
    if(successor.parent !== node){
      this.replace(successor, successor.right)
      successor.right = node.right
      successor.right.parent = successor
    }
    this.replace(node, successor)
    successor.color = node.color
    successor.left = node.left
    successor.left.parent = successor
  }
  if(color === Color.BLACK){
    this.balanceDeletion(fix)
  }
}
/**
 * 刪除節點平衡修正
 * @param node 節點
 */
  balanceDeletion(node) {
    while (node !== this.root && node.color === Color.BLACK) {
      // 節點是父節點的左子節點
      if (node === node.parent.left) {
        //兄弟節點
        let sibling = node.parent.right;
        if (sibling.color === Color.RED) {
          // 場景2.1:兄弟節點是紅色
          // 兄弟節點設定為黑色
          sibling.color = Color.BLACK;
          //替換節點的父節點設定為紅色
          node.parent.color = Color.RED;
          // 左旋
          this.rotateLeft(node.parent);
          sibling = node.parent.right;
        }
        if (sibling.left.color === Color.BLACK && sibling.right.color === Color.BLACK) {
          // 場景2.4: 兄弟節點兩個子節點都是黑色
          sibling.color = Color.RED;
          //再次以父節點為新節點作自平衡處理。
          node = node.parent;
          continue;
        } else if (sibling.left.color === Color.RED) {
          // 場景2.3: 兄弟節點的左子節點是黑色,轉換到場景2.2.
          sibling.left.color = Color.BLACK;
          sibling.color = Color.RED;
          //對兄弟節點右旋
          this.rotateRight(sibling)
          sibling = node.parent.right;
        }
        if (sibling.right.color === Color.RED) {
          //場景2.2:兄弟節點的右節點是紅色
          sibling.color = node.parent.color;
          node.parent.color = Color.BLACK;
          sibling.right.color = Color.BLACK;
          //對父節點左旋
          this.rotateLeft(node.parent);
          // 左旋之後,紅黑樹重新平衡
          node = this.root;
        }
      } else {
        //節點是父節點的左節點
        let sibling = node.parent.left;
        if (sibling.color === Color.RED) {
          // 場景 3.1:替換節點的史弟節點是紅色
          sibling.color = Color.BLACK;
          node.parent.color = Color.RED;
          this.rotateRight(node.parent);
          sibling = node.parent.left;
        }
        if (sibling.right.color === Color.BLACK && sibling.left.color === Color.BLACK) {
          //場景3.4:替換節點的兩個子節點都是黑色
          sibling.color = Color.RED;
          //再次以父節點為新節點作自平衡處理。
          node = node.parent;
          continue
        } else if (sibling.right.color === Color.RED) {
          // 場景3.3:兄弟節點的右子節點是紅色
          sibling.right.color = Color.BLACK;
          sibling.color = Color.RED;
          this.rotateLeft(sibling);
          sibling = node.parent.left;
        }
        if (sibling.left.color === Color.RED) {
          // 場景3.2:兄弟節點的左子節點是紅色
          sibling.color = node.parent.color;
          node.parent.color = Color.BLACK;
          sibling.left.color = Color.BLACK;
          this.rotateRight(node.parent);
          node = this.root;
        }
      }
    }
    node.color = Color.BLACK;
  }
}
複製程式碼

紅黑樹應用

紅黑樹廣泛用在 Java 的集合框架 (HashMap、TreeMap、TreeSet)、Nginx 的 Timer 管理、Linux 虛擬記憶體管理以及 C++ 的 STL 等等場景。

在Linux核心中,每個使用者程式都可以訪問4GB的線性虛擬空間,虛擬空間往往需要多個虛擬記憶體區域描述,對這些記憶體區域,Linux核心採用了連結串列以及紅黑樹形式組織。記憶體區域按地址排序,連結成一個連結串列以及一顆紅黑樹,尋找空閒區域時只需要遍歷這個連結串列,在發生缺頁中斷時通過紅黑樹快速檢索特定記憶體區域。

總結

紅黑樹的刪除操作就基本介紹完了,總結一下刪除操作就是,刪除節點等同於刪除替換節點,若替換節點是紅色節點時,直接刪除不會影響平衡;若替換節點是黑色節點時,就需要借用兄弟節點的右子節點、左子節點或者兄弟節點。

紅黑樹最吸引人的是它的所有操作在最差情況下可以保證 O(logN) 的時間複雜度,穩定且高效。例如要在10 萬條(2^20)資料中查詢一條資料,只需要 20 次的操作就能完成。但這些保證有一個前置條件,就是資料量不大,且資料可以完全放到記憶體中。在資料量比較大時,因為紅黑樹的深度比較大造成磁碟 IO 的頻繁讀寫,會導致它的效率低下。 另外推薦Data Structure Visualizations網站,它包含非常多的資料結構方面的視覺化演算法題。其中就有紅黑樹的演算法,對照著線上生成的紅黑樹看,會更容易理解紅黑樹中各種操作場景。

推薦閱讀

前端工程實踐之視覺化搭建系統(一)

可能是最全的 “文字溢位截斷省略” 方案合集

圖文並茂,為你揭開“單點登入“的神祕面紗

招賢納士

政採雲前端團隊(ZooTeam),一個年輕富有激情和創造力的前端團隊,隸屬於政採雲產品研發部,Base 在風景如畫的杭州。團隊現有 50 餘個前端小夥伴,平均年齡 27 歲,近 3 成是全棧工程師,妥妥的青年風暴團。成員構成既有來自於阿里、網易的“老”兵,也有浙大、中科大、杭電等校的應屆新人。團隊在日常的業務對接之外,還在物料體系、工程平臺、搭建平臺、效能體驗、雲端應用、資料分析及視覺化等方向進行技術探索和實戰,推動並落地了一系列的內部技術產品,持續探索前端技術體系的新邊界。

如果你想改變一直被事折騰,希望開始能折騰事;如果你想改變一直被告誡需要多些想法,卻無從破局;如果你想改變你有能力去做成那個結果,卻不需要你;如果你想改變你想做成的事需要一個團隊去支撐,但沒你帶人的位置;如果你想改變既定的節奏,將會是“ 5 年工作時間 3 年工作經驗”;如果你想改變本來悟性不錯,但總是有那一層窗戶紙的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望參與到隨著業務騰飛的過程,親手推動一個有著深入的業務理解、完善的技術體系、技術創造價值、影響力外溢的前端團隊的成長曆程,我覺得我們該聊聊。任何時間,等著你寫點什麼,發給 ZooTeam@cai-inc.com

通俗易懂的紅黑樹圖解(下)

相關文章