每次面試都會被問,什麼是紅黑樹?

程式設計碼農發表於2021-11-23

前言

理解紅黑樹需要掌握下面知識

  • 二分查詢演算法
  • 二叉查詢樹
  • 自平衡樹(AVL樹和紅黑樹)

基於二分演算法設計出了二叉查詢樹,為了彌補二叉查詢樹傾斜缺點,又出現了一些自平衡樹,比如AVL樹,紅黑樹等。

二分查詢演算法

在40億資料中查詢一個指定資料最多隻需要32次,這就是二分查詢演算法的魅力。

二分查詢演算法(又叫折半查詢演算法)是一種在有序陣列中查詢某一特定元素的搜尋演算法。注意有序陣列的前提。

下圖中查詢 4 ,查詢從中間元素開始 4 < 7 ,從左邊查詢 4 > 3 ,從右邊查詢 4 < 6,然後找到元素。

Binary_search_into_array.png

二分查詢演算法時間和空間複雜度,\( {n} \) 是陣列長度。

平均時間複雜度 \( {O(\log n)} \)

最壞時間複雜度 \( {O(\log n)} \)

最優時間複雜度 \( {O(1)} \)

迴圈空間複雜度 \( {O(1)} \)

遞迴空間複雜度 \( {O(\log n)} \)

Java 遞迴實現二分查詢。

    public static int binarySearch(int[] arr, int start, int end, int hkey) {
        if (start > end) {
            return -1;
        }
        int mid = start + (end - start) / 2;    //防止溢位
        if (arr[mid] > hkey) {
            return binarySearch(arr, start, mid - 1, hkey);
        }
        if (arr[mid] < hkey) {
            return binarySearch(arr, mid + 1, end, hkey);
        }
        return mid;
    }

Java 迴圈實現二分查詢。

    public static int binarySearch(int[] arr, int start, int end, int hkey) {
        int result = -1;
        while (start <= end) {
            int mid = start + (end - start) / 2;    //防止溢位
            if (arr[mid] > hkey) {
                end = mid - 1;
            } else if (arr[mid] < hkey) {
                start = mid + 1;
            } else {
                result = mid;
                break;
            }
        }
        return result;
    }

二叉查詢樹

二叉查詢樹(Binary Search Tree,簡稱BST)是一棵二叉樹,它具有以下性質:

  1. 若任意節點的左子樹不空,則左子樹上所有節點的值都小於它的根節點的值;
  2. 若任意節點的右子樹不空,則右子樹上所有節點的值都大於它的根節點的值;
  3. 任意節點的左、右子樹也分別為二叉查詢樹。
二叉樹:每個節點最多隻有兩個分支,分別稱為“左子樹”或“右子樹”。

二叉查詢樹操作(搜尋,插入,刪除)效率依賴樹高度。

最壞情況,樹向一邊傾斜,樹高度 $n$ (節點數量),此時操作時間複雜度為 $O(n)$

傾斜

理想情況,樹高度 $log(n)$ ,操作時間複雜度 $O(log(n))$ ,此時它是一棵平衡的二叉查詢樹。

理想平衡

演算法平均最差
空間O(n)O(n)
搜尋O(log n)O(n)
插入O(log n)O(n)
刪除O(log n)O(n)

為了讓二叉查詢樹儘可能達到理想情況,出現了一些自平衡二叉查詢樹,如AVL樹紅黑樹

AVL樹

AVL樹中的每個節點都有一個平衡因子屬性(左子樹高度減去右子樹高度)。每次元素插入刪除操作後,會重新進行平衡計算,如果節點平衡因子不為 [1,0,-1] 時,需要通過旋轉使樹到達平衡。AVL 樹中有 4 種旋轉操作。

  1. 左旋(Left Rotation)
  2. 右旋(RightRotation)
  3. 左右旋轉(Left-Right Rotation)
  4. 左右旋轉(Right-Left Rotation)

AVL_Tree_Example

下面是 Java AVL 樹的例子

    private Node insert(Node node, int key) {
          .....
        return rebalance(node); // 重新平衡計算
    }

    private Node delete(Node node, int key) {
          .....
        node = rebalance(node); // 重新平衡計算
        return node;
    }

        private Node rebalance(Node z) {
        updateHeight(z);
        int balance = getBalance(z);
        if (balance > 1) {
            if (height(z.right.right) > height(z.right.left)) {
                z = rotateLeft(z);
            } else {
                z.right = rotateRight(z.right);
                z = rotateLeft(z);
            }
        } else if (balance < -1) {
            if (height(z.left.left) > height(z.left.right)) {
                z = rotateRight(z);
            } else {
                z.left = rotateLeft(z.left);
                z = rotateRight(z);
            }
        }
        return z;
    }
https://github.com/eugenp/tut...

紅黑樹

性質

紅黑樹中的每個節點都有一個顏色屬性。每次元素插入刪除操作後,會進行重新著色旋轉達到平衡。

紅黑樹屬於二叉查詢樹,它包含二叉查詢樹性質,同時還包含以下性質:

  1. 每個節點要麼是黑色,要麼是紅色。
  2. 所有的葉子節點(NIL)被認為是黑色的。
  3. 每個紅色節點的兩個子節點一定都是黑色(不會出現兩個連續紅色節點)。
  4. 從根到葉子節點(NIL)的每條路徑都包含相同數量的黑色節點。

Red-black_tree_example

查詢

查詢不會破壞樹的平衡,邏輯也比較簡單,通常有以下幾個步驟。

  1. 從根節點開始查詢,把根節點設定為當前節點;
  2. 當前節點為空,返回null;
  3. 當前節點不為空,查詢key小於當前節點key,左子節點設為當前節點。
  4. 當前節點不為空,查詢key大於當前節點key,右子節點設為當前節點。
  5. 當前節點不為空,查詢key等於當前節點key,返回當前節點。

程式碼實現可以參考 Java 裡面的 TreeMap。

    Entry<K,V> p = root;
    while (p != null) {
        int cmp = k.compareTo(p.key);
        if (cmp < 0){
            p = p.left;
    }else if (cmp > 0){
      p = p.right;
    }else{
          return p;
    }
  }
    return null;

插入

插入操作分兩大塊:一查詢插入位置;二插入後自平衡。

  1. 將根節點賦給當前節點,迴圈查詢插入位置的節點;
  2. 當查詢key等於當前節點key,更新節點儲存的值,返回;
  3. 當查詢key小於當前節點key,把當前節點的左子節點設定為當前節點;
  4. 當查詢key大於當前節點key,把當前節點的右子節點設定為當前節點;
  5. 迴圈結束後,構造新節點作為當前節點左(右)子節點;
  6. 通過旋轉變色進行自平衡。

程式碼實現可以參考 Java 裡面的 TreeMap。

    Entry<K,V> t = root;
  Entry<K,V> parent;
    int cmp;
    do {
        parent = t;
        cmp = k.compareTo(t.key);
    if (cmp < 0){
            t = t.left; 
    }else if (cmp > 0){
            t = t.right;
    }else {
            return t.setValue(value);   // 更新節點的值,返回
    }
  } while (t != null);

    Entry<K,V> e = new Entry<>(key, value, parent);
        if (cmp < 0){
          parent.left = e;
        }else {
            parent.right = e;  
    }
  fixAfterInsertion(e); // 通過旋轉變色自平衡

插入場景分析

  1. 根節點為空,將插入節點設定為根節點並設定為黑色;
  2. 插入節點的key已存在,只需要更新插入值,無需再自平衡;
  3. 插入節點的父節點為黑色,直接插入,無需自平衡;
  4. 插入節點的父節點為紅色。

場景 4 插入節點後出現兩個連續的紅色節點,所以需要重新著色旋轉。這裡面又有很多種情況,具體看下面。

先宣告下節點關係,祖節點(10),叔節點(20),父節點(9),插入節點(8)。

節點關係

一般通過判斷插入節點的叔節點來確定合適的平衡操作。

插入場景

叔叔節點存在且為紅色

rb_insert_01.gif

  1. 先查詢位置將節點8 插入;
  2. 父節點9 叔節點20 變為黑色,祖節點10 變為紅色;
  3. 祖節點10 是根節點,所以又變為黑色。

叔叔節點不存在或為黑色,父節點是祖節點的左節點,插入節點是父節點的左子節點。

rb_insert_02.gif

  1. 先查詢位置將節點7 插入;
  2. 祖節點9 進行右旋轉;
  3. 父節點8 變為黑色,祖節點9 變為紅色;

叔叔節點不存在或為黑色,父節點是祖節點的左節點,插入節點是父節點的右子節點。

rb_insert_03.gif

  1. 先查詢位置將節點8 插入;
  2. 父節點7 進行左旋轉;
  3. 祖節點9 進行右旋轉;
  4. 將插入節點8 變為黑色,祖節點9 變為紅色;

叔叔節點不存在或為黑色,父節點是祖節點的右節點,插入節點是父節點的右子節點。

rb_insert_04.gif

  1. 先查詢位置將節點10 插入;
  2. 祖節點8 進行左旋轉;
  3. 父節點9 變為黑色,祖節點8 變為紅色;

叔叔節點不存在或為黑色,父節點是祖節點的右節點,插入節點是父節點的左子節點。

rb_insert_05.gif

  1. 先查詢位置將節點9 插入;
  2. 父節點10 進行右旋轉;
  3. 祖節點8 進行左旋轉;
  4. 將插入節點9 變為黑色,祖節點8 變為紅色;

刪除

刪除操作分兩大塊:一查詢節點刪除;二刪除後自平衡。刪除節點後需要找節點來替代刪除的位置。

根據二叉查詢樹性質,刪除節點之後,可以用左子樹中的最大值右子樹中的最小值來替換刪除節點。如果刪除的節點無子節點,可以直接刪除,無需替換;如果只有一個子節點,就用這個子節點替換。

思考一些刪除場景,使用下面視覺化工具模擬場景。

https://www.cs.csubak.edu/~ms...

替換節點和刪除節點其中一個紅色

rb_del_01.gif

  1. 查詢到刪除節點3,將它刪除;
  2. 節點2 替換刪除位置,並變為刪除節點3 的黑色。

替換節點和刪除節點都是黑色,它兄弟節點是黑色,兄弟節點的子節點至少有一個紅色。

替換節點和刪除節點都是黑色,它兄弟節點是黑色,兄弟節點的子節點至少有一個紅色。

替換節點和刪除節點都是黑色,它兄弟節點是黑色,兄弟節點的兩個子節點都是黑色。

替換節點和刪除節點都是黑色,它兄弟節點是紅色

AVL樹和紅黑樹對比

下面是[1-10]分別儲存在AVL樹紅黑樹的圖片。可以看出:

  • AVL樹更嚴格平衡,帶來查詢速度快。為了維護嚴格的平衡,需要付出頻繁旋轉的效能代價。
  • 紅黑樹相較於要求嚴格的AVL樹來說,它的旋轉次數少。

1-10 AVL樹

1-10 紅黑樹

相關文章