National Central University
Uedu Main Site
Explore Uedu
Student Console
Register as Member/Login
Research Informed Consent Center
Survey Center
Educational Big Data Challenge
Claim or create a course
Try an AI Scenario Task
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

3

Local AI

--

Uedu Code

--

中央大學 AQI 72 24°C PM2.5 13
AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

Unit 12

2-3-4 樹與紅黑樹

一個節點放多個鍵的平衡樹,以及用顏色在二元樹裡表示它
Interaction實作 25 minute(s)

Learning Objectives

  • 說明 2-3-4 樹的三種節點,以及為什麼它的所有葉節點永遠在同一層
  • 追蹤 2-3-4 樹由上往下分裂 4-節點的插入,說明樹只會在根分裂時長高
  • 列出紅黑樹的五條性質,把一棵紅黑樹換算成對應的 2-3-4 樹
  • 說明紅黑樹插入時新節點塗紅的理由,以及「叔叔紅色就變色、叔叔黑色就旋轉」各對應 2-3-4 樹的什麼動作
  • 比較 AVL 樹與紅黑樹,說明 C++ std::map 與 Java TreeMap 的底層結構

換個想法:讓一個節點放好幾個鍵

上一課的 AVL 樹用「左右高度最多差 1」維持平衡,代價是插入、刪除時常常要旋轉。這一課換一個角度:

如果一個節點可以放 1~3 個鍵,樹就能做到所有的葉節點永遠在同一層,完美平衡。

這種樹叫 2-3-4 樹。它很好理解,但直接寫成程式很麻煩,所以實務上把它「翻譯」成二元樹,每條連結塗上紅色或黑色,就是紅黑樹(red-black tree)。

歷史上,Bayer 在 1972 年提出「對稱二元 B 樹」(symmetric binary B-tree),就是用二元樹表示這種多鍵節點;1978 年 Guibas 與 Sedgewick 用紅、黑兩種顏色描述它,紅黑樹這個名稱由此而來。

這一課先看 2-3-4 樹怎麼插入,再看紅黑樹的規則、兩者怎麼一一對應,最後看紅黑樹的插入。

2-3-4 樹:三種節點

2-3-4 樹的節點依鍵的個數分成三種,名稱是「子節點的個數」:

節點 鍵 子節點 範例
2-節點 1 個 2 個 [20]:左邊 < 20 < 右邊
3-節點 2 個 3 個 [20 | 40]:左 < 20 < 中 < 40 < 右
4-節點 3 個 4 個 [20 | 40 | 60]

搜尋和二元搜尋樹一樣:在節點裡找到目標落在哪兩個鍵之間,往對應的子節點走。

插入一律放在葉節點。葉節點如果已經是 4-節點(3 個鍵),就沒有位置了,要先分裂:

     [ ... ]                    [ ... 40 ... ]
        |          分裂         /            \
  [20 | 40 | 60]   ───>      [20]           [60]

中間的鍵 40 往上送到父節點,剩下的兩個鍵各自變成 2-節點。父節點如果也滿了怎麼辦?常見的做法是由上往下:從根往下走的時候,路上遇到 4-節點就先分裂,這樣父節點一定有空位接住送上來的鍵。

樹只有在根是 4-節點、被分裂時才長高一層:中間的鍵成為新的根。所有的葉節點同時往下一層,所以它們永遠在同一層。

2-3-4 樹的插入:路上遇到 4-節點先分裂

從只有 10 的樹開始,依序插入 30、20、40、50、60。插完 20,根是 [10 | 20 | 30];插入 40 時根是 4-節點,先分裂:20 成為新的根,樹長高一層,再把 40 放進右邊的葉節點。插入 60 時,往下走的下一站 [30 | 40 | 50] 是 4-節點,先分裂,40 送上根,60 才放進去。

紅黑樹:用顏色畫出 2-3-4 樹

2-3-4 樹的節點有三種大小,直接寫程式要處理各種情況,也浪費空間。紅黑樹的做法是:每個 2-3-4 節點畫成一小群二元樹節點, 其中一個是黑色的「中心」,同一個節點裡的其他鍵是掛在它下面的紅色節點:

2-3-4 節點 紅黑樹
[20] 黑色 20
[20 | 40] 黑色 20、右子節點紅色 40(或黑色 40、左子節點紅色 20)
[20 | 40 | 60] 黑色 40,左右子節點是紅色 20、紅色 60

所以紅色節點代表「我和父節點其實在同一個 2-3-4 節點裡」。這個對應寫成規則,就是紅黑樹的五條性質:

  1. 每個節點不是紅色就是黑色
  2. 根是黑色
  3. 每個空的子節點(叫 NIL,視為葉)是黑色
  4. 紅色節點的子節點一定是黑色,也就是不能有兩個紅色節點相連
  5. 從任何一個節點往下走到每一個 NIL,經過的黑色節點數量都相同,這個數叫黑高度(black height)

第 4 條對應「一個 2-3-4 節點最多 3 個鍵」(一個黑色加最多兩個紅色);第 5 條對應「2-3-4 樹的葉節點都在同一層」,因為每個 2-3-4 節點剛好貢獻一個黑色節點。

樹高的保證:最短的路徑全是黑色節點,最長的路徑紅黑交錯,最多是最短的 2 倍。n 個節點的紅黑樹,高度不超過 2 log₂(n + 1),搜尋 O(log n)。

檢查一棵樹是不是紅黑樹

black_height 用後序檢查第 4、5 條:紅色節點不能有紅色子節點;左右兩邊算出來的黑高度要相同。原本的樹(根 20,左 10,右 40 帶著紅色的 30、50)黑高度是 2。把 40 改成紅色,40 和 30 連續紅色,回傳 -1;改回來後把 10 改成紅色,根的左邊黑高度變成 0、右邊是 1,也回傳 -1。

把紅黑樹換算成 2-3-4 樹

fill 從每個黑色節點出發,把它的紅色子節點併進同一個 2-3-4 節點,再對下面的黑色節點遞迴。根 40 帶著紅色的 20、60,合成 [20 | 40 | 60];10 帶著紅色的 5 合成 [5 | 10];80 帶著兩個紅色子節點合成 [70 | 80 | 90]。紅黑樹有 4 層,換算出來的 2-3-4 樹只有 2 層,剛好等於黑高度。

想一想:2-3-4 樹與紅黑樹的對應

1.

2-3-4 樹裡一個有 3 個鍵的節點,有幾個子節點(不是葉節點的情況)?

2.

一棵紅黑樹:根是黑色 30,左子節點是紅色 10(10 的兩個子節點是黑色 5、黑色 20),右子節點是黑色 40。對應的 2-3-4 樹長什麼樣子?

3.

用「檢查一棵樹是不是紅黑樹」範例的 black_height 檢查下面這棵樹:根是黑色 20,兩個子節點是紅色 10 和紅色 30。會印出什麼?

第 1 次作答

紅黑樹的插入:塗紅、看叔叔

插入時先照二元搜尋樹的規則把新節點放到葉的位置,而且一律塗成紅色:紅色節點不會改變任何路徑上的黑色節點數,第 5 條不會被破壞。 唯一可能出問題的是第 4 條:父節點剛好也是紅色,出現兩個紅色相連。這時看叔叔(父節點的兄弟)的顏色:

情況 紅黑樹的動作 在 2-3-4 樹裡的意思
父節點是黑色 什麼都不用做 新鍵放進一個還有空位的節點
父節點紅、叔叔也紅 變色:父、叔改黑,祖父改紅,再從祖父往上檢查 祖父所在的節點已經是 4-節點,放不下了:分裂,中間的鍵(祖父)往上送
父節點紅、叔叔黑(空的也算黑),新節點在外側 在祖父旋轉一次,父節點改黑、祖父改紅 3-節點多了一個鍵變成 4-節點,把中間的鍵擺成黑色
父節點紅、叔叔黑,新節點在內側 先在父節點旋轉,轉成外側,再照上一列處理(雙旋轉) 同上
最後根是紅色 根改成黑色 根分裂後中間的鍵成為新根,樹長高一層

外側、內側和 AVL 一樣:父節點是祖父的左子節點、新節點也在左邊,就是外側(LL);方向相反就是內側(LR)。

變色可能讓祖父和曾祖父又連續紅色,所以要繼續往上檢查;旋轉之後那一段的頂端是黑色,就結束了。所以一次插入最多旋轉 2 次,變色最多往上 O(log n) 層。

範例用遞迴寫:insert 回程時對每一層呼叫 fix(h),把 h 當成祖父,檢查它底下兩層有沒有紅色相連。

紅黑樹插入:變色與旋轉

依序插入 10、30、20、40、50,和上面 2-3-4 樹的範例前幾個鍵相同。插入 20 時,20 在 30 的左邊、30 在 10 的右邊,屬於內側(RL):先在 30 右旋,再在 10 左旋並交換顏色,20 成為黑色的根。插入 40 時叔叔 10 是紅色,只要變色,根 20 被改紅後再塗回黑色,黑高度加 1,對應 2-3-4 樹的根分裂。插入 50 時叔叔是空的(黑色),在 30 左旋一次。root 要等最外層的 insert 回傳才會換人。

實驗:兩棵樹一起長

輸入一個鍵按「插入」,或按下方的按鈕插入一組鍵。右邊是紅黑樹,左邊是由它換算出來的 2-3-4 樹(紅字的鍵是紅黑樹裡的紅色節點)。每一步都會說明紅黑樹做了什麼、在 2-3-4 樹裡對應什麼,下方列出這次插入的所有步驟。注意上方的「黑高度」永遠等於「2-3-4 樹高」。

刪除、和 AVL 的比較、內建容器

刪除的調整比插入複雜,這裡只說明想法,不實作:

  1. 照二元搜尋樹的方法刪除(兩個子節點時用後繼者或前驅頂替),最後真正被拿掉的節點最多只有一個子節點
  2. 拿掉的是紅色節點:黑高度不變,結束
  3. 拿掉的是黑色節點:那條路徑少了一個黑色,第 5 條被破壞。修正時看兄弟節點的顏色與它的子節點,分成四種情況,用變色與旋轉補回來

在 2-3-4 樹裡,這相當於節點被拿空了:兄弟有多的鍵就借一個過來(對應旋轉),兄弟也只有一個鍵就和它合併(對應變色,可能讓父節點也少一個鍵,問題往上傳)。 整個刪除最多旋轉 3 次。

AVL 樹和紅黑樹怎麼選?

AVL 樹 紅黑樹
平衡條件 左右高度最多差 1 五條顏色規則
樹高上限 約 1.44 log₂n 約 2 log₂n
插入的旋轉 最多 2 次 最多 2 次
刪除的旋轉 最多 O(log n) 次 最多 3 次
每個節點多存 高度(整數) 顏色(1 個位元就夠)

AVL 樹比較矮,搜尋略快;紅黑樹的插入、刪除調整比較少。兩者的操作都是 O(log n)。

內建的有序容器:Java 的 TreeMap 官方文件第一句就寫明是「以紅黑樹實作」(TreeSet 內部用 TreeMap),並保證 get、put、remove 是 log(n)。 C++ 標準只規定 std::map、std::set 的操作是 O(log n),沒有指定用哪種樹;GCC 的 libstdc++ 和 LLVM 的 libc++ 都是用紅黑樹實作。 Python 標準函式庫沒有平衡樹,需要時常用排好序的串列配 bisect。

想一想:插入的調整

1.

紅黑樹插入一個新節點後,它的父節點是紅色、叔叔也是紅色。下一步要做什麼?

2.

用「紅黑樹插入」範例的程式,把 1~10 依序插入一棵空的紅黑樹。印出的樹高與黑高度是什麼?

3.

下列關於內建有序容器的敘述,哪一個正確?

第 1 次作答

實作:變色

下面的紅黑樹插入已經寫好旋轉的情況,只缺「父節點和叔叔都是紅色」的變色。請在 fix 裡補上三行:把 h 改成紅色、h 的兩個子節點改成黑色。測試依序插入 10、20、30、15、25、5、1、27,每次印出前序走訪與顏色;沒有變色時,從插入 15 開始就會和預期輸出不同。

你的輸出
預期輸出

重點整理

  • 2-3-4 樹:節點有 1~3 個鍵;插入一律放進葉節點,路上遇到 4-節點先分裂、中間鍵往上送;只有根分裂時樹才長高,所以葉節點永遠在同一層
  • 紅黑樹是 2-3-4 樹的二元樹寫法:黑色節點+它的紅色子節點=一個 2-3-4 節點
  • 五條性質:非紅即黑、根是黑、NIL 是黑、紅色不能相連、每條路徑黑色節點數相同;樹高不超過 2 log₂(n + 1)
  • 插入:新節點塗紅;父節點紅時看叔叔,叔叔紅就變色(=分裂),叔叔黑就旋轉(外側一次、內側兩次);最後根塗黑(=長高一層)
  • 插入最多旋轉 2 次,刪除最多 3 次;AVL 比較矮,紅黑樹調整比較少
  • Java TreeMap/TreeSet 以紅黑樹實作;C++ std::map/std::set 常見的實作也是紅黑樹

下一課:B 樹與 B+ 樹。2-3-4 樹其實就是最小的 B 樹;把一個節點能放的鍵增加到幾百個,樹就矮到只要讀幾次磁碟,資料庫的索引就是這樣做的。

這一課完成了!
Next lesson:B 樹與 B+ 樹
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。