1. 程式人生 > >資料結構之紅黑樹-動圖演示(上)

資料結構之紅黑樹-動圖演示(上)

紅黑樹是比較常見的資料結構之一,在Linux核心中的完全公平排程器、高精度計時器、多種語言的函式庫(如,Java的TreeMap)等都有使用。

在學習紅黑樹之前,先來熟悉一下二叉查詢樹。

二叉查詢樹(Binary Search Tree)

二叉查詢樹,它有一個根節點,且每個節點下最多有隻能有兩個子節點,左子節點的值小於其父節點,右子節點的值大於其父節點。

插入節點

從根節點向下查詢,當新插入節點大於比較的節點時,新節點插入到比較節點的右側,當小於比較的節點時,插入到比較節點的左側,一直向下比較大小,找到要插入元素的位置並插入元素。

如圖: 依次插入節點[100,50,200,80,300,10]

虛擬碼(來源Java TreeMap,有省略和修改):

void put(K key, V value) {
     if (root == null) {
        root = new Node<>(key, value, null); 
        return;
    }       
    Node<K,V> t = root; 
    int cmp; // 比較結果    
    Node<K,V> parent;  
    Comparable<? super K> k = (Comparable<? super K>) key;
    do {
        parent = t;
        cmp = k.compareTo(t.key);
        if (cmp < 0)
            t = t.left;
        else if (cmp > 0)
            t = t.right;
        else
            return; // 節點存在直接返回
    } while (t != null);
    
    Node<K,V> e = new Node<>(key, value, parent);
    if (cmp < 0){
         parent.left = e;
    }else{
       parent.right = e;  
    }  
}

查詢節點

從根節點開始向下查詢,當查詢節點大於比較的節點時,向右查詢,當小於當前比較節點時,就向左查詢。一直向下查詢,直到找到對應的節點或到終點查詢結束。

如圖: 查詢節點[80]

虛擬碼(來源Java TreeMap,有省略和修改):

Node<K,V> getNode(Object key) {
    Comparable<? super K> k = (Comparable<? super K>) key;
    Node<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. 刪除的節點有一個子節點
  3. 刪除的節點沒有子節點

Case 1:

該種情況下,涉及到節點的“位置變換”,用右子樹中的最小節點替換當前節點。從右子樹一直 left 到 NULL。最後會被轉換為 Case 2 或 Case 3 的情況。

所以對於刪除有兩個孩子的節點,刪除的是其右子樹的最小節點,最小節點的內容會替換要刪除節點的內容。

如圖:刪除節點[50]

Case 2:

有一個子節點的情況下,將其父節點指向其子節點,然後刪除該節點。

如圖:刪除節點[200]

Case 3:

在沒有子節點的情況,其父節點指向空,然後刪除該節點。

如圖:刪除節點[70]

虛擬碼(來源Java TreeMap,有省略和修改):

Node remove(Object key) {
    // 查詢節點(參考上面查詢程式碼)
    Node<K,V> p = getNode(key); 
  
     // 節點變換。 p 有兩個子節點,將其轉換為刪除後繼節點
    if (p.left != null && p.right != null) {
        Entry<K,V> s = t.right;
        while (s.left != null){
            s = s.left;
        }
        p.key = s.key;
        p.value = s.value;
        p = s;
    } 

    Entry<K,V> replacement = (p.left != null ? p.left : p.right);
    // p 有一個子節點
    if (replacement != null) {
        replacement.parent = p.parent;
        if (p.parent == null){
            root = replacement;
        }  else if (p == p.parent.left){
            p.parent.left  = replacement;
        } else{
            p.parent.right = replacement;
        }
        p.left = p.right = p.parent = null;

    } else if (p.parent == null) { // 根節點
       
         root = null;
    } else { //  p 沒有子節點
       
         if (p == p.parent.left){
            p.parent.left = null;
        } else if (p == p.parent.right){
            p.parent.right = null;
        }
        p.parent = null;   
    } 
    return p;
}

樹的優勢

我們知道,有序陣列刪除或插入資料較慢(向陣列中插入資料時,涉及到插入位置前後資料移動的操作),但根據索引查詢資料很快,可以快速定位到資料,適合查詢。而連結串列正好相反,查詢資料比較慢,插入或刪除資料較快,只需要引用移動下就可以,適合增刪。

而二叉樹就是同時具有以上優勢的資料結構。

該樹缺點

上面的樹是非平衡樹,由於插入資料順序原因,多個節點可能會傾向根的一側。極限情況下所有元素都在一側,此時就變成了一個相當於連結串列的結構。

如圖:依次插入節點[100,150,170,300,450,520 ...]

這種不平衡將會使樹的層級增多(樹的高度增加),查詢或插入元素效率變低。

那麼只要當插入元素或刪除元素時還能維持樹的平衡,使元素不至於向一端嚴重傾斜,就可以避免這個問題。

到此,紅黑樹閃亮登場, 紅黑樹就是一種平衡二叉樹。

紅黑樹(Red Black Tree)

紅黑樹是一種平衡二叉樹,遵守如下規則來保證紅黑樹的平衡,保證每個節點在它左邊的後代數目和在它右邊的後代數目應該是大致相等(最長路徑也不會超過最短路徑的2倍)。

紅黑樹的規則

紅黑樹是在二叉查詢樹基礎之上再遵循如下規則的樹

  1. 每個節點顏色不是黑色就是紅色
  2. 根節點一定為黑色
  3. 兩個紅色節點不能相鄰(紅色節點的子節點一定是黑色)
  4. 從任意節點到葉子節點的每條路徑包含的黑色節點數目相同(黑色高度)
  5. 每個葉子節點(NULL節點,空節點)是黑色

當插入或刪除節點時,必須要遵守紅黑樹的規則,根據這些規則來決定是否需要改變樹的結構或節點顏色,使其達到平衡。

查詢節點並不影響樹的平衡,所以紅黑樹的節點查詢和二叉查詢樹的操作是一樣的(請參考二叉查詢樹)。

如圖: 紅黑樹 - 依次插入節點[100,200,300,400,500,600,700,800]

最終樹的結構是大致平衡的,不像二叉查詢樹那樣偏向一側。

瞭解變色和旋轉

如果新插入元素或刪除元素後,紅黑樹的規則被破壞,這時需要對樹進行調整來重新滿足紅黑樹規則。調整有變色和旋轉(左旋或右旋)兩種方式,接下來分別瞭解這兩種方式:

  • 變色

通過改變節點顏色修正紅黑樹,節點由紅變黑或黑變紅

  • 旋轉

通過改變節點的位置關係修正紅黑樹

如圖: 以右旋為例

左旋則與右旋對稱,為逆時針旋轉。

圖中空節點位置可以是多個節點構成的子樹,也可以是一個具體節點。

右旋(來源Java TreeMap):

private void rotateRight(Entry<K,V> p) {
    if (p != null) {
        Entry<K,V> l = p.left;
        p.left = l.right;
        if (l.right != null) 
            l.right.parent = p;
        l.parent = p.parent;
        if (p.parent == null)
            root = l;
        else if (p.parent.right == p)
            p.parent.right = l;
        else p.parent.left = l;
        l.right = p;
        p.parent = l;
    }
}

左旋(來源Java TreeMap):

private void rotateLeft(Entry<K,V> p) {
    if (p != null) {
        Entry<K,V> r = p.right;
        p.right = r.left;
        if (r.left != null)
            r.left.parent = p;
        r.parent = p.parent;
        if (p.parent == null)
            root = r;
        else if (p.parent.left == p)
            p.parent.left = r;
        else
            p.parent.right = r;
        r.left = p;
        p.parent = r;
    }
}

紅黑樹的插入和刪除節點請看下一篇: 資料結構之紅黑樹-動圖演示(下) - 更新中 ...