1. 程式人生 > 實用技巧 >演算法入門——紅黑樹(二)

演算法入門——紅黑樹(二)

  演算法入門——紅黑樹(二)

2020-09-0111:40:35 hawk


概述

  上一節中,我們分析了一下關於完美平衡的2-3查詢樹的各種操作,但僅僅是理論上的——換而言之,我們忽略了實現上的具體細節。這裡我們將介紹一下基於紅黑樹(更具體的是左偏紅黑樹)的具有完美平衡性質的2-3查詢樹的具體程式碼實現,從而最終實現具有完美平衡的2-3查詢樹。

  這裡首先簡單介紹一下紅黑樹——紅黑樹實際上是基於二叉查詢樹進行實現的,因為2-3查詢樹實際上也是有2-節點和3-節點構成的。因此紅黑樹通過在二叉查詢樹的節點中新增一些額外屬性,從而抽象模擬2-節點和3-節點,進而模擬出2-3查詢樹。並且通過對於二叉查詢樹的簡單修改,從而可以模擬出符合完美平衡性質的2-3查詢樹。

  因此,本節我們將基於二叉查樹的結構,對其進行一些簡單的修改,並且介紹一些簡單基礎的操作,包裝出紅黑樹這個資料結構,進而來模擬符合完美平衡性質的2-3查詢樹的種種性質。


二叉查詢樹

  二叉查詢樹是一個比較基本且簡單的資料結構,其有當前節點的值以及一些指向後驅節點的指標(因為是二叉查詢樹,所以包含兩個後驅節點)構成,這裡簡單的列出其資料結構

typedef struct NODE {
    Value val;
    int number
    struct NODE *left, *right;
} *Node;

  對於該資料結構的操作,諸如插入、查詢以及刪除等,這裡就不再贅述,會在後面介紹具有完美平衡性質的2-3查詢樹的對應操作時一併提及。


紅黑樹

  這裡首先介紹一下基於二叉查詢樹的紅黑樹的資料結構,然後在具體介紹和分析一下紅黑樹與2-3查詢樹之間的聯絡等。

  實際上,之所以成為紅黑樹,是因為其將連結分為了紅連結和黑連結。而一般情況下,通過想二叉查詢樹的節點新增color屬性,從而通過連結指向的節點對應的屬性來表明該連結的顏色——如果指向的節點的color屬性為紅色,則該連結為紅色;如果指向的節點的color屬性為黑色,則該連結為黑色。其餘基本和二叉查詢樹沒有太大的區別,因此其資料結構也基本和二叉查詢樹的資料結構差別不大,如下所示

typedef struct RBNODE {
    Value val;
    
int number; int color; struct RBNODE *left, *right; } *RBNode;

  所以看起來,實際上紅黑樹的資料結構和二叉查詢樹也沒有什麼大的區別。那麼我們如何通過紅黑樹這個資料結構來模擬具有完美平衡的2-3查詢樹。實際上並不是十分的困難,這裡的基本思路是普通的黑色連結仍然是連結兩個2-節點(即普通的二叉查詢結點);紅色連結也連結普通的二叉查詢節點,但是需要將其當作一個3-結點。這樣子,我們就基於二叉查詢結點,包裝出了紅黑樹這個資料結構,從而可以模擬2-3查詢樹。可能說起來仍然比較抽象,這裡我們通過示意圖進行說明,如下所示

  可以看到,在2-3查詢樹中,P1P2是一個3-節點;但是在對應的紅黑樹中,實際上3-節點是由兩個2-節點和一個紅連結構成的。這樣子實際上基本就通過紅黑樹模擬了2-3查詢樹的資料結構了。

  當然,對於紅黑樹這個抽象結構來說,為了更好地模擬完美平衡的2-3查詢樹,我們需要新增幾條規則——否則如果任意設定紅連結等,不方便進行操作,我們規定一個紅黑樹需要滿足如下性質

1.    紅連結均為左連結
2.    沒有任何一個節點同時和兩條紅連結相連
3.    該樹是完美黑色平衡的,即任意空連結到根節點的路徑上的黑連結數量相等

  實際上,這樣子的紅黑樹就可以較好的模擬完美平衡的2-3查詢樹。


操作

  實際上,我們在構建或者操作紅黑樹的時候,往往需要將基礎的二叉查詢樹或者錯誤的紅黑樹轉變為一個符合上述要求的紅黑樹,這裡往往會用到左旋轉、右旋轉以及顏色轉換等操作——實際上,基本通過紅黑樹模擬的完美平衡的2-3查詢樹,基本就是二叉查詢樹和上面那幾個操作的組合而已,因此這裡著重分析一下這幾個操作。這裡需要說明的是,我們下面的操作都要基於一個假設,當前的樹是黑色完美平衡的,但是其無法滿足其他紅黑樹的特性,因此我們需要通過下述的操作進行調整,從而使該黑色完美平衡的樹轉換為完美平衡的紅黑樹。實際上這個假設是很容易滿足的——實際上從最開始部落格中提到的插入或者刪除操作,如果我們簡單將其轉換為紅黑樹的操作,則任何時候都是黑色平衡的,只不過是可能不滿足紅黑樹的要求罷了(出現4-節點——即連續兩個左連線為紅連結、右連結為紅連結),因此下面的操作是用來進行調整。

左旋轉

  這個操作的物件通常是紅連結為右連結的節點的父節點,其效果是將紅連結轉變為左連線,且仍然保持區域性性——僅僅改變該節點和其子節點的只想關係,其餘節點並不發生變化(用來保持完美平衡性)。說起來比較抽象,這裡首先給出對應的示意圖,然後放出這部分的程式碼。

  實際上,可以看到,一開始是指向N2節點的連結為紅連結,即左連線為紅連結;然後經過左旋轉後轉換為了指向P1節點的連結為紅連結,即右連線為紅連結。可以觀察一下,除了P1和N2以外,其餘的節點的情況沒有發生任何變化(一個是子節點;另一個是從空連結到根節點的黑連結個數)。這裡說明一下,完美平衡的2-3查詢樹對應的是完美黑色平衡的紅黑樹,也就是這裡所有空連結到根節點的黑色連結個數相等。所以對於這個圖來說,雖然N1的高度發生了變化,左旋後比左旋前高度高了1,但是這高的是紅連結,所以黑色連結個數仍然不變。大家可以配合這張圖進行理解,如圖所示

  

  實際上我們可以將紅連結抽象的3-節點畫出來,如果我們想象這個3-節點的父節點的連結,那麼左旋的意義相當於將該連結由指向P1更換為了指向N2,其餘沒有任何變化。但是對於我們的紅黑樹來說,卻滿足了我們之前提到的三個性質,因此這個左旋操作大概為這樣。其具體的程式碼實現也很簡單,如下所示


#define RED (1)
RBNode rotateLeft(RBNode node) {
    RBNode right = node->right;
    
    node->right = right->left;    right->left = node;
    right->color = node->color;  node->color = RED;
    right->number = node->number;
    node->number = Size(node->left) + Size(node->right) + 1;    

    return right;
}

  可以結合程式碼和示意圖看到,這個操作除了當前3-節點外沒有修改其他任何節點的資訊,從而完成了區域性性調整。

右旋轉

  這個操作的物件通常是連續兩個左子連結都是紅連結的節點,其效果是將消除連續兩個左子連結都是紅連結,轉換為當前節點的左連線和右連結為紅連結,仍然保持區域性性質,即僅僅父節點和兩個左子節點的關係和連結,其餘節點並不發生變化(用來保持完美平衡性)。說起來比較抽象,這裡首先給出對應的示意圖,然後放出這部分的程式碼。示意圖如下所示

  實際上,可以看到,一開始是指向N1節點、S1節點的連結皆為為紅連結,即連續兩個左子連結都為紅連結;然後經過右旋轉後轉換為了指向S1節點、P1節點的連結為紅連結,即左連線和右連結為紅連結。可以觀察一下,除了S1、P1和N1以外,其餘的節點的情況沒有發生任何變化(一個是子節點;另一個是從空連結到根節點的黑連結個數)。這裡說明一下,完美平衡的2-3查詢樹對應的是完美黑色平衡的紅黑樹,也就是這裡所有空連結到根節點的黑色連結個數相等。所以對於這個圖來說,雖然T1、T2和N2節點的高度發生了變化,但是變換的主要是紅連結,所以黑色連結個數仍然不變。大家可以配合這張圖進行理解,如圖所示

  實際上我們可以將連續兩個左連結為紅連結抽象的4-節點畫出來,如果我們想象這個4-節點的父節點的連結,那麼右旋的意義相當於將該連結由指向P1更換為了指向N1,其餘沒有任何變化。但是對於我們的紅黑樹來說,雖然仍然不滿足紅黑樹的性質,但是實際上如果我們直接把當前的節點N1提取出來融合到父節點上,則容易判斷其仍然是滿足完美平衡性質的,並且稍微修改節點的屬性即可完成轉變為符合要求的紅黑樹。因此右旋操作大概是這樣。其具體的程式碼實現也很簡單,如下所示

#define RED (1)

RBNode rotateRight(RBNode node) {
    left = node->left;
    
    node->left = left->right;    left->right = node;
    left->color = node->color;       node->color = RED;

    left->number = node->number;
    node->number = Size(node->left) + Size(node->right) + 1;

    return left;
}

顏色轉換

  實際上這個操作主要是用來調整上面的情況,其操作的物件是子左連結和子右連結結尾紅連結的節點。其效果是將消除左右節點的紅連結,轉換為當前節點和其父節點的連結為紅連結,仍然保持區域性性質,即僅僅修改該節點、父節點和兩個左子節點的關係和連結,其餘節點並不發生變化(用來保持完美平衡性)。說起來比較抽象,這裡首先給出對應的示意圖,然後放出這部分的程式碼。示意圖如下所示

  實際上,可以看到,一開始是P1節點指向N1節點、N2節點的連結皆為為紅連結,即左子連結和右子連結都為紅連結;然後經過顏色轉換後轉換為了指向P1節點的連結為紅連結,P1節點指向其子節點的連結為黑連結。可以觀察一下,除了N2、P1和N1以外,其餘的節點的情況沒有發生任何變化(一個是子節點;另一個是從空連結到根節點的黑連結個數)。這裡說明一下,完美平衡的2-3查詢樹對應的是完美黑色平衡的紅黑樹,也就是這裡所有空連結到根節點的黑色連結個數相等。大家可以配合這張圖進行理解,如圖所示

  實際上我們可以將連左子連結和右子連結皆為紅連結抽象的4-節點畫出來,如果我們想象這個4-節點的父節點的連結,那麼顏色轉換的意義相當於將P1節點合併入其父節點上(父節點可能轉變換為3-節點,可能轉變為4-節點),其餘沒有任何變化。但是對於我們的紅黑樹來說,雖然仍然不滿足紅黑樹的性質(父節點可能為4-節點),但是可以看到仍然滿足大部分紅黑樹的要求,並且仍然是完美平衡的——其空連結到父節點的仍然相同。因此顏色轉換操作大概是這樣。其具體的程式碼實現也很簡單,如下所示

#define BLACK (0)
#define RED (1)

void flipColors(RBNode node) {
    node->left->color = node->right->color = BLACK;
    node->color = BLACK;
}

總結

  實際上我們已經把從二叉查詢樹轉換為紅黑樹所需要的操作講述完畢了,大家已經可以結合前面分析和思想,構造出基於紅黑樹的完美平衡的2-3查詢樹了。這裡面最重要的就是理解一下這幾個操作的意義,我在說一下我在學習時的坑

  1.  操作時始終保持黑色完美平衡——實際上在前面分析的時候,對於完美平衡的2-3查詢樹,我們的任何操作的任何時候,都保持著完美平衡,放到基於紅黑樹中的2-3查詢樹來說,就是始終需要保持黑色完美平衡,大家可以認真理解和思考一下。

  2.  對於3-、4-節點的理解——我認為4-節點即為2個連續的紅連結,可以是左右子節點的連結為紅連結,也可以是一個子節點、一個父節點的連結為紅連結,但是都可以抽象為4-節點。對於最後的基於紅黑樹的完美平衡的2-3查詢樹來說,是不允許存在4-節點的,但是根據前面的思路,我們首先會允許存在4-節點,在遞迴的時候,最後由下至上時,我們可以通過將4-節點的中間節點融合進父節點(就是上面介紹的顏色轉換操作),從而消除4-節點為2個2-節點。

  3.  注意完美平衡的性質——由於前面說過了,上一篇部落格的所有對於2-3查詢樹的操作,都可以保持其完美平衡,也就是紅黑樹的黑色完美平衡,因此我們往往可以利用完美平衡的性質來化簡所要分析的情況——如果有左子節點/右子節點,這一層上的節點都有子節點;如果左子節點/右子節點為空,則這一層所有節點都為空,需要注意利用一下這個性質,方便分析。