從2-3-4樹模型到紅黑樹實現

從2-3-4樹模型到紅黑樹實現

前言

紅黑樹,是一個高效的二叉查找樹。其定義特性保證了樹的路徑長度在黑色節點上完美平衡,使得其查找效率接近於完美平衡的二叉樹。java

可是紅黑樹的實現邏輯很複雜,各類旋轉,顏色變化,直接針對其分析,大多數都是死記硬背各類例子,不太容易有個直觀的理解。實際上,紅黑樹是實現手段,是其餘概念模型爲了方便在二叉樹上實現進而定義的節點顏色這個信息。若是從概念模型入手,再一一對應,就容易理解的多了。而紅黑樹可以對應的模型有2-3樹,2-3-4樹等,下面咱們會以2-3-4樹做爲概念模型,對紅黑樹進行分析。node

2-3-4樹

2-3-4樹是對完美平衡二叉樹的擴展,其定義爲:安全

  • 在一個節點中,能夠存在1-3個key
  • 2-節點,擁有1個key和2個子節點。
  • 3-節點,擁有2個key和3個子節點。
  • 4-節點,擁有3個key和4個子節點。
  • 子節點爲空的節點稱爲葉子節點。
  • 任意從根節點到葉子節點的路徑擁有相同的長度,即路徑上的連接數相同。

下圖就是一個2-3-4樹:數據結構

查找

2-3-4樹的查找很簡單,相似於二叉樹,步驟以下:less

  • 將查找key和節點內的key逐一對比。
  • 若是命中,則返回節點內key的對應值。
  • 若是節點內的key都不命中,則沿着合適的連接到下一節點重複該過程直到找到或者無後續節點。

舉個例子,若是咱們要在上面的2-3-4樹中查詢11,其步驟以下:this

插入

2-3-4樹的插入,不會發生在中間節點,只會在葉子節點上進行插入。編碼

code

在葉子節點上新增key,會使得2-節點變爲3-節點,3-節點變爲4-節點。而本來的4-節點就沒有空間能夠插入key了。爲了解決這個問題,能夠將4-節點中間的key推送給其父節點,剩下的2個key造成2個2-節點。效果以下blog

經過將4-的葉子節點拆分,產生了新的葉子節點可供key插入,同時將中間key送入父節點。該操做不會破壞樹的平衡性和高度。但若是葉子節點的父節點也是4-節點,這個操做就沒法進行了。爲了解決這個問題,有兩種思路:遞歸

  • 自底向上,從4-葉子節點開始分裂,若是分類後其父節點也是4-節點,繼續向上分裂,直到到達根節點。若是根節點也是4-節點,分裂後樹的高度+1。
  • 自頂向下,從根節點到插入所在的葉子節點路徑上,遇到4-節點就將其分裂。

兩種方法都能解決問題,不過自頂向下不須要遞歸,實現起來更簡單。經過這種處理方式,確保了1)最後到達的葉子節點必然是2-或者3-節點,搜索路徑上不存在4-節點。

樹的生長

2-3-4樹是向上生長的。這句話能夠從根節點的分裂理解:若是根節點是一個4-節點,當新增key時,根節點會分裂,將中間的key推入父節點。根節點沒有父節點,所以中間的key就會成爲新的根節點。以下所示:

整顆樹的生長能夠當作是葉子節點不斷的新增key,而且在成爲4-節點後被下一次的新增動做分解爲2個2-節點,同時將一個key送入父節點。隨着這個過程的不斷進行,不斷有key從葉子節點向根節點匯聚,直到根節點成爲4-節點並在下一次新增時被分類,進而讓樹升高1。

刪除

刪除是整個操做中最爲複雜的部分,由於刪除可能發生在任意節點上,而且刪除後可能破壞2-3-4樹的完美平衡。在這裏,咱們先來處理一些簡單的狀況,最後再思考能夠推而廣之的策略。

刪除最大key

在2-3-4樹中,刪除最大key必然是最右邊的葉子節點上。若是葉子節點是3-節點或者4-節點,只須要將其中最大的key刪除便可,不會對樹的平衡性形成影響。但若是刪除的key在2-節點上,狀況就變得麻煩,由於刪除2-節點,致使樹的平衡被破壞。爲了不這個狀況的發生,不能讓刪除發生在2-節點上。

爲了讓刪除不落在2-節點上,能夠將2-類型的葉子節點(最終要刪除的那個),從其兄弟節點「借」一個key進行融合變成3-節點;也能夠將父節點的key和兄弟節點的key融合,變成一個4-節點,主要保證變化過程當中樹的平衡性不被破壞便可。變換完成以後的節點類型是3-或4-,天然就能夠成功刪除了。變化的可能狀況有:

變化的策略是:

  1. 將父節點的key,自身的key,兄弟節點的key的合併後造成一個邏輯節點。
  2. 變化一:新節點爲4-節點的狀況下,父節點還有key,則新節點替換目標節點;
  3. 變化二:新節點爲5-節點的狀況下,最小key還給兄弟節點,次小key還給父節點,剩餘2個key設置到目標節點。
  4. 變化三:新節點爲6-節點的狀況下,最小key還給兄弟節點,次小key還給父節點,剩餘3個key設置到目標節點。

向下的搜索,最終達到須要刪除key的葉子節點。葉子節點的兄弟節點沒法控制,而若是能保證目標key所在的葉子節點的父節點不是2-節點,就能夠安全刪除key而不會破壞樹的結構。所以,在自頂向下的過程當中,非根節點若是爲2-節點,則經過變化成爲非2-節點。這個轉化,僅僅針對搜索路徑的下一個節點而言,所以可能出現節點1被轉化爲非2-節點後,其子節點是2-節點,子節點轉化爲非2-節點時將父節點(節點1)恢復成2-節點。轉化的最終目的是爲了保證葉子節點的父節點是非2-節點便可,只不過爲了達成這個保證,整個轉化行爲須要從根節點一直進行下去。所以若是在葉子節點的時候執行轉化可能會致使子樹高度減1,這種變化會影響到全局樹的平衡。就須要循環向上迭代到根節點,比較複雜。而從根節點開始一路轉化下去,則容易理解和實現,也不會影響樹的平衡。

經過執行這種變化,在葉子節點中,就能夠安全刪除key

刪除最小key

最小key的刪除思路和操做方式和刪除最大key類似,只不過搜索路徑的方向是最左而已,其節點變化策略也是類似的,具體的變化有如下幾種:

變化的策略是:

  1. 將父節點的key,自身的key,兄弟節點的key的合併後造成一個邏輯節點。
  2. 變化一:新節點爲4-節點的狀況下,父節點還有key,則新節點替換目標節點;
  3. 變化二:新節點爲5-節點的狀況下,最大key還給兄弟節點,次大key還給父節點,剩餘2個key設置到目標節點。
  4. 變化三:新節點爲6-節點的狀況下,最大key還給兄弟節點,次大key還給父節點,剩餘3個key設置到目標節點。

刪除任意key

刪除任一key就變得比較麻煩,key可能出如今中間節點上,刪除的話,樹的結構就被破壞了。這裏,咱們能夠採用一個取巧的思路:若是刪除的key是樹的中間節點,將該key替換爲其中序遍歷的後繼key;該後繼key是刪除key的節點的右子樹的最小key

key的替換對樹無影響;而將替換key刪除,則轉換爲刪除對應子樹最小Key的問。刪除最小Key,須要從根節點自頂向下變化2-節點才能保證葉子節點中key的成功刪除。所以,刪除任一Key的具體處理思路能夠總結爲:

  1. 從根節點開始自頂向下搜索,非根節點若是爲2-節點,則經過變化成爲非2-節點。
  2. 搜索發現目標key,將其替換爲中序搜索後繼key。
  3. 刪除步驟2節點的右子樹最小key。

左傾紅黑樹

2-3-4樹是一種概念模型,直接按照這個概念模型用代碼實現則比較複雜,主要的複雜有:

  • 維持3種節點類型。
  • 多種節點類型之間須要互相轉換。
  • 在樹中移動須要進行屢次比較,若是節點不是2-節點的話。

所以在表現形式上,咱們將2-3-4樹換另一種形式來展示,進行如下變換:

  • 將2-3-4樹用二叉樹的形式表現。
  • 節點之間的連接區分爲紅色和黑色。紅色連接用於將節點連接起來視做3-節點和4-節點。

這種轉換的關鍵點在於:

  • 轉換後的二叉樹可使用二叉樹的搜索方式。
  • 轉換後的二叉樹和2-3-4樹處於一致關係,改變的只是表現形式。

不過因爲3-節點兩種表現形式,增大了複雜性,所以對變換要求增長一條:紅色連接只能爲左鏈接。經過三個約束後,轉換獲得二叉樹咱們稱之爲左傾斜紅黑樹,其關鍵特性有:

  • 可使用二叉樹搜索方式。
  • 與2-3-4樹保持一一對應。
  • 紅黑樹是黑色連接完美平衡的,也就是從根節點到葉子節點的任意路徑上,黑色連接的數量一致。

其對應方式以下:

能夠看到,若是將紅色連接放平,就和2-3-4樹在展示上一致了。2-3-4樹是完美平衡的,其對應的左傾斜紅黑樹是黑色連接完美平衡,由於紅色連接是用於3-節點和4-節點的;而黑色連接就對應2-3-4樹中的連接。

左傾斜紅黑樹的轉換中不容許2種形式:

  • 右傾斜的紅色連接。
  • 兩個連續的紅連接在一個搜索路徑中(從根到葉子節點的路徑)

形象的說如下幾種不容許:

禁止的狀況,減小了須要考慮的情形,爲後續的編碼實現下降了難度。對於上述定義的左傾斜紅黑樹,使用數據結構來表達的話,在本來的二叉樹的節點中,增長一個屬性color,用於表示指向該節點的連接的顏色。

public class RedBlackTree
{
    private static final boolean RED   = true;
    private static final boolean BLACK = false;

    private       Node root;            // root of the BST
    private       int  heightBLACK;      // black height of tree

    private class Node
    {
        `key`   `key`;                  // `key`
        Value value;              // associated data
        Node  left, right;         // left and right subtrees
        boolean color;            // color of parent link
        private int    N;            // number of nodes in tree rooted here
        private int    height;       // height of tree rooted here

        Node(`key` `key`, Value value)
        {
            this.`key` = `key`;
            this.value = value;
            this.color = RED;
            this.N = 1;
            this.height = 1;
        }
    }
}

查找

紅黑樹的查找和二叉樹的一致,可是會更加快速,由於紅黑樹是黑色平衡的,搜索長度獲得了控制。

插入

在介紹插入實現以前,首先要介紹紅黑樹中的兩種旋轉操做:

  • 右旋:將一個左傾斜的紅連接轉化爲右連接。
  • 左旋:將一個右傾斜的紅連接轉化爲左鏈接。

這兩個操做的重要性在於其變化是局部的,不影響黑色鏈接的平衡性。其變化以下:

紅黑樹的插入和二叉樹插入是相同的,只不過新增的連接是紅色的。由於紅黑樹的概念模型是2-3-4樹,2-3-4樹新增節點是在葉子節點上,而且新增後必然成爲3-節點或者4-節點,因此新增連接均爲紅色。

在新增完畢後,根據紅連接具體狀況,進行旋轉處理,以保持左傾斜紅黑樹的要求。可能出現的狀況有:

  • 在2-節點上新增key,表如今紅黑樹上,就是一個黑色的節點新增左鏈接或者右鏈接
  • 在3-節點上新增key,表如今紅黑樹上,就是被紅連接相連的兩個節點上3個可新增鏈接的地方新增紅連接。

2-節點的狀況以下所示:

3-節點的狀況以下所示:

左旋和右旋的方法以下:

private Node rotateLeft(Node h)//將節點的右紅連接轉化爲左連接,而且返回本來節點的子樹轉化後新的子樹的根節點
    {
        Node x = h.right;
        h.right = x.left;
        x.left = setN(h);
        x.color = x.left.color;
        x.left.color = RED;
        return setN(x);//該方法用於計算節點x的子樹內的節點數量以及高度
    }

    private Node rotateRight(Node h)//將節點的左紅連接轉化爲右連接,而且返回本來節點的子樹轉化後新的子樹的根節點
    {
        Node x = h.left;
        h.left = x.right;
        x.right = setN(h);
        x.color = x.right.color;
        x.right.color = RED;
        return setN(x);
    }

在2-3-4樹的節點插入中,爲了不葉子節點是4-節點致使沒有空間插入,因此從根節點到葉子節點的搜索路徑中,採用自頂向下的4-節點分解策略。而在紅黑樹中,對4-節點的分解動做是經過對節點的顏色變化完成的,以下圖所示:

翻轉的過程很簡單,就是將節點,節點的左右孩子節點的顏色都進行調整便可,顏色翻轉的代碼以下

private void colorFlip(Node h)
    {
        h.color = !h.color;
        h.left.color = !h.left.color;
        h.right.color = !h.right.color;
    }

4-節點的分解帶來的效果是將紅色連接向上層移動,這個移動可能產生一個紅色的右連接,此時須要經過左旋來修正;或者產生2個連續的紅連接,此時須要將其右旋,造成一個符合定義的4-節點。

總結來講,插入的過程首先是自頂向下,遇到4-節點就進行分解,直到到達葉子節點插入新的key;因爲向下過程的4-節點可能產生右傾斜的紅連接,或者連續的2個紅連接,所以須要從葉子節點處向上到達根節點,修復產生的這些問題。處理方式主要是:

  • 右傾斜的紅連接左旋。
  • 連續的紅連接,經過右旋來達到符合定義的4-節點。

按照上述的總結,咱們能夠將新增節點的方法實現爲

private Node insert(Node h, `key` `key`, Value value)//將KV對插入以h節點爲根節點的樹,而且返回插入後該樹的根節點
    {
        if (h == null)//尋找到空白連接,返回新的節點。該節點爲紅色連接指向的節點。
        {
            return new Node(`key`, value);
        }
        if (isRed(h.left) && isRed(h.right))//自頂向下的過程當中,分裂4-節點。
        {
            colorFlip(h);
        }
        if (eq(`key`, h.`key`))
        {
            h.value = value;
        }
        else if (less(`key`, h.`key`))
        {
            h.left = insert(h.left, `key`, value);
        }
        else
        {
            h.right = insert(h.right, `key`, value);
        }
        if (isRed(h.right))//右傾斜的紅色連接,進行左旋。
        {
            h = rotateLeft(h);
        }
        if (isRed(h.left) && isRed(h.left.left))//連續的紅色連接,右旋變化爲符合定義的4-節點
        {
            h = rotateRight(h);
        }
        return setN(h);
    }

刪除

和概念模型相同的方法,咱們首先嚐試實現刪除最大key和刪除最小key,以後經過替換key位置來實現刪除任意Key功能。

刪除最大Key

和概念模型相同,刪除要發生在非2-節點上才能保證樹的平衡不被破壞。這就意味着刪除必定要發生在一個被紅色連接相連的節點上。概念模型當中,在自頂向下搜索過程須要保證中間節點不是2-節點來使得葉子節點必然能夠轉化爲非2-節點進行安全刪除;反應在紅黑樹中,搜索路徑的下一個節點,必需要被紅色連接相連。若是不是的話,則要進行變化,具體的手段包括:

  • **當前節點有左傾斜紅色連接時,將其進行右旋。**右旋能夠從概念模型上理解,能夠認爲搜索路徑是進行到3-節點或4-節點,而且從小key搜索到大key
  • **搜索路徑的下一節點爲2-節點,轉化爲非-2節點。**這個轉化過程,參考概念模型中的作法,將當前節點的key,右子節點的key,左子節點的key先合併,產生紅連接相連的邏輯節點。以後按照概念模型的拆分方式進行拆分。

針對步驟二,咱們作下具體的分析。

當前節點不是2-節點,且3-節點的紅色左鏈接被轉化爲右連接,所以在下一個節點爲2-節點的狀況下,當前節點必然是被右傾斜紅連接指向。所示初始狀態可能以下:

對於狀況一,咱們只須要對節點20進行顏色翻轉,就可讓其後繼節點變爲紅色,也就是連接變紅,便可。這種轉換對應概念模型中的變化1。

對於狀況二,比較複雜。首先咱們須要對節點20進行顏色翻轉。此時節點10和20在一行路徑上,對節點20的左鏈接右旋,右旋以後節點10變爲新的根節點,對齊進行顏色翻轉。整個過程以下

這種轉換對應概念模型中的變化2。

狀況三和狀況二能夠採用徹底相同的變化步驟,轉換方式對應概念模型中的變化3。以下圖所示:

綜合狀況1、2、三,咱們能夠將變換的代碼撰寫爲

private Node moveRedRight(Node h)
    {
        colorFlip(h);//先翻轉
        if (isRed(h.left.left))//此時爲狀況二或者三
        {
            h = rotateRight(h);
            colorFlip(h);
        }
        return h;
    }

結合紅連接右旋和轉換2-節點,咱們能夠將刪除最大key的代碼編寫以下:

public void deleteMax()
    {
        root = deleteMax(root);
        root.color = BLACK;//若是根節點右子節點爲2-節點,翻轉root節點會致使其顏色變紅,不符合定義。所以刪除完成後,將顏色恢復爲顏色。
    }
private Node deleteMax(Node h)//刪除h節點爲根節點的子樹中的最大節點,而且返回刪除後的子樹的根節點
    {
        if (isRed(h.left))
        {
            h = rotateRight(h);
        }
        if (h.right == null)//沒有右子樹了,刪除該節點
        {
            return null;
        }
        if (!isRed(h.right) && !isRed(h.right.left))//右子節點爲2-節點,進行變化過程。
        {
            h = moveRedRight(h);
        }
        h.right = deleteMax(h.right);
        return fixUp(h);
    }
    private Node fixUp(Node h) //修正可能存在的異常連接狀況。
    {
        if (isRed(h.right))
        {
            h = rotateLeft(h);
        }
        if (isRed(h.left) && isRed(h.left.left))
        {
            h = rotateRight(h);
        }
        if (isRed(h.left) && isRed(h.right))
        {
            colorFlip(h);
        }
        return setN(h);
    }

這個實現中引入了一個以前不曾提到的方法fixUp。由於在刪除的過程自頂向下的變換會產生一些不符合定義的連接狀況:好比右傾斜的紅連接,好比連續的紅連接。在刪除完畢後,須要沿着以前的搜索路徑,自底向上,進行異常連接修復。

刪除最小Key

刪除最小Key的思路和刪除最大Key的思路很是接近,只不過在於搜索的方向不一樣,是沿着左子節點一直向下搜索。相比於刪除最大Key,刪除最小Key在搜索路徑向下的過程當中不須要對紅連接方向進行旋轉,當搜索路徑的下一節點存在2-節點時轉化爲非2-節點。可能存在的初始狀況以下圖:

狀況一很簡單,只須要對節點20進行顏色翻轉。該變換對應概念樹中的變化1。

對於狀況二,先對節點20進行翻轉,再對節點30的左鏈接右旋,再對節點20的右連接左旋,最後對頂點進行翻轉。流程以下圖所示:

該變換對應概念模型中的變化2。

對於狀況三則更復雜一些,其對應概念模型中的變化3,流程以下

和刪除最大key不一樣的地方在於狀況三沒法複用狀況2的操做,不然會產生一個右傾斜的紅連接。不過即便是右傾斜紅連接,仍然是黑色平衡。可是與左傾斜紅黑樹定義不吻合,因此狀況三使用了更多的步驟來產生符合定義的紅黑樹。

結合上述過程,咱們能夠將刪除最小Key的代碼編寫以下

public void deleteMin()
    {
        root = deleteMin(root);
        root.color = BLACK;
    }

    private Node deleteMin(Node h)
    {
        if (h.left == null)
        {
            return null;
        }
        if (!isRed(h.left) && !isRed(h.left.left))
        {
            h = moveRedLeft(h);
        }
        h.left = deleteMin(h.left);
        return fixUp(h);
    }
private Node moveRedLeft(Node h)
    {
        colorFlip(h);
        if (isRed(h.right.left))
        {
            if (isRed(h.right.right))
            {
                h.right = rotateRight(h.right);
                h = rotateLeft(h);
                h = rotateLeft(h);
                h.left = rotateRight(h.left);
                colorFlip(h);
            }
            else
            {
                h.right = rotateRight(h.right);
                h = rotateLeft(h);
                colorFlip(h);
            }

        }
        return h;
    }

刪除任意Key

和概念模型的刪除操做類似,自頂向下搜索,按照key的比較結果,可能向左右任意方向前進,若是下一步是一個2-節點,則參照刪除最大最小key中的變化方式進行變化。在肯定key所在的節點後,將該key值替換爲中序遍歷的後繼節點,繼而刪除該節點右子樹的最小節點。代碼以下

public void delete(Key key)
    {
        root = delete(root, key);
        root.color = BLACK;
}
 private Node delete(Node h, Key key)
    {
        if (less(key, h.key))
        {
            if (!isRed(h.left) && !isRed(h.left.left))
            {
                h = moveRedLeft(h);
            }
            h.left = delete(h.left, key);
        }
        else
        {
            if (isRed(h.left))
            {
                h = rotateRight(h);
            }
            if (eq(key, h.key) && (h.right == null))
            {
                return null;
            }
            if (!isRed(h.right) && !isRed(h.right.left))
            {
                h = moveRedRight(h);
            }
            if (eq(key, h.key))
            {
                h.value = get(h.right, min(h.right));
                h.key = min(h.right);
                h.right = deleteMin(h.right);
            }
            else
            {
                h.right = delete(h.right, key);
            }
        }
        return fixUp(h);
    }

總結

紅黑樹做爲概念樹的實際實現,其代碼很複雜,要求的變化方式也不少。從概念樹的映射來講,2-3樹,2-3-4樹均可以映射到紅黑樹上。而左傾斜紅黑樹,使用遞歸方式實現的代碼,無疑是很好理解,代碼量也較少的。而JDK中TreeMap採用概念模型也是2-3-4樹,不過並不限制右傾斜。整體而言,紅黑樹有太多變種,瞭解其原理最爲重要,實現上,能使用便可,深究的話,意義不大。

參考文獻

《Left-Leaning Red-Black Trees》(Princeton University)


文章原創首發於公衆號:林斌說Java,轉載請註明來源,謝謝。 更多高質量原創文章,歡迎掃碼關注

相關文章
相關標籤/搜索