數據結構之從2-3 樹理解紅黑樹

2-3 樹

在介紹紅黑樹之前有必要先介紹一下2-3樹,因爲直接理解紅黑樹是有一定難度的,而紅黑樹其實是2-3 樹的等價二叉樹形式,清楚了2-3 樹的邏輯,紅黑樹自然就迎刃而解了。
2-3 樹是最簡單的B樹,是搜索樹的一種,其結構中有兩種節點,有兩個孩子的被稱爲2-節點,三個孩子的被稱爲3-節點:

它滿足BST的基本性質,所謂滿足性質是:對於一個2-節點來說其左子樹的值比該節點小,右子樹值比該節點大。對於一個3-節點來說其中的兩個值從小到大排列,中間孩子的值是介於該節點兩個值之間的。
2-3樹是一棵絕對平衡的樹結構,在插入過程中會始終保持絕對平衡,絕對平衡是指:在該樹中,對於任意節點,其子樹的高度相等;或者從根出發到該樹任意一個葉子節點經過的節點數相等
下面通過2-3樹的建樹過程來看一下爲什麼2-3樹是一棵絕對平衡樹,假設添加的順序是:

42,37,12,18,6,11,5

  • 2-3樹的添加過程除根節點外不會往空位置添加元素,它會依次添加出2-節點,3-節點;當新來一個元素要往3-節點繼續添加時,則先形成4-節點,當然此時已不符合2-3樹的定義了,再將4-節點分裂即可:
  • 若葉子節點形成4-節點,則分裂後的根節點要與其父親節點融合:
  • 若分裂後與父親節點融合導致父親節點成爲4-節點,則父親節點再進行一次分裂:

自此,這個建樹過程已完畢,其中要處理的一共就這三種情況,可以看到,這種建樹方式可以保證所得的樹是一棵絕對平衡樹。而且可以發現 ,我們的添加過程是從大到小的,若是往BST中添加這些元素,樹早已偏斜。可見2-3樹的平衡性。

紅黑樹與2-3樹的等價

好了,現在可以開始介紹紅黑樹了,紅黑樹是一種二叉樹。紅黑樹與2-3樹之所以等價是因爲紅黑樹相當於在2-3樹的節點上做了對應,2-節點對應到紅黑樹變化不大,3-節點對應到紅黑樹則要做相應變化:

其中紅色節點表示在紅黑樹對應的原2-3樹中,該節點與其父親節點是融合在一塊的。
將紅色節點放在左邊的紅黑樹叫做左傾紅黑樹,相應地有有右傾紅黑樹,原理是一致的。本篇所有討論均基於左傾紅黑樹。
在算法導論中,作者Thomas H. Cormen直接給出了紅黑樹的性質(定義):

翻譯:
1.每個節點都是紅或黑
2.根節點是黑色
3.每個葉子(這裏的葉子是指最後的空節點)是黑色
4.若一個節點是紅色,則它的孩子節點都是黑色
5.從任一節點到葉子節點,經過的黑色節點個數相等

如果直接看這個定義一定會懵逼的,但是現在再來看這個定義就清晰多了,其中1、2顯然,3、4、5都可以由2-3樹往紅黑樹的轉換中推出。值得注意的是,紅黑樹並不是AVL那樣的平衡二叉樹,可以計算得到紅黑樹的最大高度可能是2logn級別(當經過的每個黑節點之前都有紅節點時),但從任一節點到葉子節點,經過的黑色節點個數相等,故也稱紅黑樹爲“黑平衡”。

紅黑樹的添加(插入)操作

接下來我們討論紅黑樹的添加元素操作,由紅黑樹的性質不難知道,插入的每個元素初始必然都是紅色的,不過根比較特殊,根是黑色的這是定義。對於本篇實現的紅黑樹的添加操作,不失一般性,共有5種情況:

  • 1.前兩種對應於2-3樹向2節點插入元素。插入節點在黑色節點左側(就是比父親節點小),則直接插入。
  • 2.插入節點在黑色節點右側,此時執行左旋轉操作:
  • 3.接下來三種是對應於2-3樹向3-節點添加元素。case1:添加x2在黑色節點右邊,形成3-節點,則進行顏色翻轉:
  • 4.case2:添加在x的左側形成3-節點,先執行右旋轉,再進行顏色翻轉即可:
  • 5.case3:添加在左側的右側形成3-節點,先執行左旋轉變爲case4,再按case4處理:

通過上面的分析,不難實現出一個紅黑樹,下面給出紅黑樹類的具體實現:

'''RBTree.java'''
import java.util.ArrayList;

public class RBTree<K extends Comparable<K>, V> {
    private static final boolean RED = true;
    private static final boolean BLACK = false;//私有且不可更改
    private class Node{
        public K key;
        public V value;
        public Node left, right;
        public boolean color;//true代表紅色

        public Node(K key, V value){
            this.key = key;
            this.value = value;
            left = null;
            right = null;
            color = RED;//初始化爲紅色,因爲新節點總要和某個節點融合
        }
    }

    private Node root;
    private int size;

    public RBTree(){
        root = null;
        size = 0;
    }

    public int getSize(){
        return size;
    }

    public boolean isEmpty(){
        return size == 0;
    }
    //輔助函數判斷節點顏色
    private boolean isRed(Node node){
        if(node == null)
            return BLACK;
        return node.color;
    }
    private void flipColors(Node node){
        //顏色翻轉
        node.color = RED;
        node.left.color = BLACK;
        node.right.color = BLACK;
    }
    // 向RB樹中添加新的元素(key, value)
    public void add(K key, V value){
        root = add(root, key, value);
        root.color = BLACK;//保持根節點爲黑色
    }
    //紅黑樹左旋轉,返回旋轉後的根
    private Node leftRotate(Node node){
        Node x = node.right;

        node.right = x.left;
        x.left = node;
        x.color = node.color;
        node.color = RED;
        return x;
    }
    //右旋轉
    private Node rightRotate(Node node){
        Node x = node.left;

        node.left = x.right;
        x.right = node;
        x.color = node.color;
        node.color = RED;
        return x;
    }
    // 向以node爲根的RB樹中插入元素(key, value),遞歸算法
    // 返回插入新節點後樹的根
    private Node add(Node node, K key, V value){

        if(node == null){
            size ++;
            return new Node(key, value);
        }

        if(key.compareTo(node.key) < 0)
            node.left = add(node.left, key, value);
        else if(key.compareTo(node.key) > 0)
            node.right = add(node.right, key, value);
        else // key.compareTo(node.key) == 0
            node.value = value;

        //維護紅黑樹性質
        //下面三個操作不是互斥的而是每次都要看一下是否滿足各個的條件,可能都要做
        if(isRed(node.right) && !isRed(node.left))
            node = leftRotate(node);
        if(isRed(node.left) && isRed(node.left.left))
            node = rightRotate(node);
        if(isRed(node.left) && isRed(node.right))
            flipColors(node);
        return node;
    }
    // 返回以node爲根節點索樹中,key所在的節點
    private Node getNode(Node node, K key){

        if(node == null)
            return null;

        if(key.equals(node.key))
            return node;
        else if(key.compareTo(node.key) < 0)
            return getNode(node.left, key);
        else // if(key.compareTo(node.key) > 0)
            return getNode(node.right, key);
    }

    public boolean contains(K key){
        return getNode(root, key) != null;
    }

    public V get(K key){

        Node node = getNode(root, key);
        return node == null ? null : node.value;
    }

    public void set(K key, V newValue){
        Node node = getNode(root, key);
        if(node == null)
            throw new IllegalArgumentException(key + " doesn't exist!");

        node.value = newValue;
    }

    // 返回以node爲根的樹的最小值所在的節點
    private Node minimum(Node node){
        if(node.left == null)
            return node;
        return minimum(node.left);
    }

    // 刪除掉以node爲根的樹中的最小節點
    // 返回刪除節點後新的樹的根
    private Node removeMin(Node node){

        if(node.left == null){
            Node rightNode = node.right;
            node.right = null;
            size --;
            return rightNode;
        }

        node.left = removeMin(node.left);
        return node;
    }

可以看到,上述代碼是在BST代碼的基礎上修改的,相比於BST的代碼,在節點中加入了表示顏色的布爾變量;設置了輔助函數左旋轉、右旋轉以及顏色翻轉;添加操作中對紅黑樹的性質進行維護。
(由於刪除操作有點複雜,暫時沒有給出,過兩天補上。。。)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章