【數據結構】之紅黑樹

1、引言

紅黑樹是一種平衡的二叉樹,其在二分搜索樹的基礎上添加了一些特性,保證其不會退化成鏈表。java

一、紅黑樹的定義

  1. 知足二分搜索樹的基本性質
  2. 每一個節點要麼是紅色的,要麼是黑色的
  3. 根節點是黑色的
  4. 每個葉子節點(最後的空節點)是黑色的
  5. 若是一個節點是紅色的,那麼它的孩子節點都是黑色的
  6. 從任意一個節點到葉子節點,通過的黑色節點是同樣的

說明:以上紅黑樹的定義看完很快就會忘記,疑惑點在於爲何要這麼定義。爲何有的節點是紅色或是黑色。爲何這麼幾條限制就能保證紅黑樹的平衡性。這些疑問點在看完下面對於「2-3樹」的介紹後會逐漸變得明朗,「2-3樹」和紅黑樹是等價的,紅黑樹的各個特性能夠類比於「2-3樹」。node

2、2-3樹

一、什麼是2-3樹

2-3樹是一顆絕對平衡的樹,從根節點到任意葉子節點所應該的節點數量是相等的;它知足二分搜索樹的基本性質;有的節點僅包含一個元素,它容許有2個孩子節點,被稱爲「2節點」;有的節點包含兩個元素,它容許有三個孩子節點,被稱爲「3節點」。
2-3樹.png算法

二、2-3樹的定義

  • 知足二分搜索樹的基本性質
  • 節點能夠存放一個元素或者兩個元素
  • 每一個節點有2個(2節點)後者3個孩子(3節點)

2-3樹的節點.png

三、2-3樹維持絕對平衡

一、元素添加到2節點

元素添加到2節點.png

說明:元素添加到2-3樹的2節點直接融合成一個3節點便可。性能

二、元素添加到3節點

元素添加到3節點.png

說明:元素添加到2-3樹的3節點會臨時融合成「4節點」,而後會裂變成三個2節點,以其中一個節點爲根,裂變後仍然知足二分搜索樹的性質。this

三、元素添加到3節點,且父親節點爲2節點

元素添加到3節點,且父親節點爲2節點.png

說明:元素添加到2-3樹的3節點會臨時融合成「4節點」,而後會裂變成三個2節點,以其中一個節點爲根;此臨時4節點裂變後產生的新子樹高度增長了1,破壞了絕對平衡性,因而這顆新子樹的根鬚要繼續向其父親節點融合,融合後新子樹增長的高度被抹平,從新保持絕對平衡。spa

四、元素添加到3節點,且父親節點爲3節點

元素添加到3節點,且父親節點爲3節點.png

說明:2-3樹維護絕對平衡,靠的就是融合、裂變的操做,融合後樹的高度保持變後者高度下降,而裂變操做後會增長樹的高度,所以裂變後會伴隨着融和操做,融合操做中和裂變操做。從而保證了2-3的絕對平衡。3d

3、紅黑樹和2-3樹

一、紅黑樹與2-3樹的等價性

紅黑樹與2-3樹的節點等價性.png

紅黑樹和2-3樹.png

紅黑樹和2-3樹類比.png

  • 紅黑樹用黑色節點表示2-3樹的2節點
  • 紅黑樹用紅色節點表示與其祖先節點融合組成3節點

說明:紅黑樹之因此定義節點的紅黑顏色,實際上就是模擬2-3樹的2節點或者3節點。咱們再來回顧一下關於開頭的紅黑樹定義,下面會作個簡單說明。code

  • 爲何每一個節點要麼是紅色的,要麼是黑色的?

說明:模擬2-3樹的2節點和3節點只須要兩種顏色便可,紅色節點表示與其父親節點融合。blog

  • 爲何根節點是黑色的?

說明:根節點做爲紅黑樹的根,不須要向上融合了。繼承

  • 爲何一個節點是紅色的,那麼它的孩子節點都是黑色的?

說明:由於一個紅色節點與其父親節點已經組成了相似於2-3樹的3節點了,若是其孩子節點依然是紅色的,那就構成了2-3樹中的臨時「4節點」,此時2-3樹要進行裂變和融合操做保證絕對平衡性。那麼相似於2-3樹,紅黑樹有連續兩個節點是紅色的話,紅黑樹須要進行旋轉操做(相似於2-3樹的裂變操做)配合顏色翻轉操做(相似於2-3樹的融合操做),以使得紅黑樹保持平衡性。連續連續兩個紅色節點也是紅黑樹出發自平衡的觸發點,因此不會出現連續兩個紅色節點。

  • 爲何從任意一個節點到葉子節點,通過的黑色節點是同樣的?

說明:因爲紅黑樹是模擬於2-3樹,而2-3樹是絕對平衡的樹,因此除去表示融合的紅色節點,紅黑樹中的黑色節點也是絕對平衡的,紅黑樹也被稱爲是「黑平衡」的二叉樹,所以從紅黑樹的任意節點出發到葉子節點,所經歷的黑色節點數是一致的。

4、紅黑樹相關操做

紅黑樹在添加元素、刪除元素後會影響樹的平衡性,可能會破壞紅黑樹的定義。相似於2-3樹的平衡操做(裂變節點、融合節點),紅黑樹也有其平衡操做,左右旋轉相似於2-3樹的裂變節點;顏色翻轉相似於2-3樹的融合節點。
紅黑樹相關操做.png

說明:如下會實現一個左傾紅黑樹,在遞歸算法實現的紅黑樹添加操做中,當遞歸到深層的葉子節點融合元素(紅色節點)後可能形成不平衡,須要遞歸逐層向上維護節點狀態。

一、紅黑樹左旋操做

  • 左傾左旋操做前

左旋操做前.png

說明:左傾紅黑樹規定節點的右孩子不能是紅色節點,須要左旋操做。

  • 左旋操做先後

左旋操做先後.png

說明:左旋操做後當前子樹的根節點發生了變化,當前的根節點須要繼承保留原有根節點的顏色,同時原來的根節點變成紅色節點。若是原有根節點是紅色節點,那麼上層須要繼續進行相關操做。

二、紅黑樹的右旋操做

  • 右旋操做前

右旋操做前.png

說明:連續兩個節點爲紅色節點,違反了紅黑樹的定義,相似於造成了2-3樹的臨時4節點,此時紅黑樹的平衡被破壞,須要再平衡操做。

  • 右旋操做後

右旋操做後.png

說明:右旋操做後,發現節點狀態依然相似於2-3樹的臨時4節點狀態,須要繼續裂變和融合操做,而紅黑樹此種場景只須要向上融合便可,即須要顏色翻轉,使x節點變成紅色節點表示向上融合。

三、顏色翻轉操做

  • 顏色翻轉前

顏色翻轉前.png

  • 顏色翻轉後

顏色翻轉後.png

總結:紅黑樹模擬於2-3樹也等價於2-3樹。紅黑樹的左右旋轉等價於2-3樹的裂變節點操做;紅黑樹的顏色翻轉等價於2-3樹的融合節點操做;不一樣點在於紅黑樹的左右旋轉會能夠下降子樹的高度,而2-3樹的裂變節點操做會增長子樹的高度;紅黑樹的顏色翻轉操做不會改變子樹的高度,是一個抽象的融合操做,而2-3樹的融合操做能夠下降子樹高度。最終紅黑樹也會趨於平衡,而2-3會保持絕對平衡。

5、紅黑樹的實現

一、左傾紅黑樹的實現

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;

        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;
    }

    /**
     * 判斷節點是不是紅色
     * @param node 樹節點
     * @return
     */
    private boolean isRed(Node node) {
        if (node == null) {
            return BLACK;
        }
        return node.color;
    }

    /**
     * 節點左旋轉操做
     *   node                     x
     *  /   \     左旋轉         /  \
     * T1   x   --------->   node   T3
     *     / \              /   \
     *    T2 T3            T1   T2
     * @param node
     * @return
     */
    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;
    }

    /**
     * 右旋轉操做
     *     node                   x
     *    /   \     右旋轉       /  \
     *   x    T2   ------->   y   node
     *  / \                       /  \
     * y  T1                     T1  T2
     * @param node
     * @return
     */
    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;
    }

    /**
     * 顏色翻轉
     * @param node
     */
    private void flipColors(Node node) {
        node.color = RED;
        node.left.color = BLACK;
        node.right.color = BLACK;
    }

    /**
     * 向紅黑樹中添加新的元素(key, value)
     * @param key 元素key
     * @param value 元素value
     */
    public void add(K key, V value){
        root = add(root, key, value);
        root.color = BLACK;//最終根節點爲黑色節點
    }

    /**
     * 向以node爲根的二分搜索樹中插入元素(key, value),遞歸算法
     * 返回插入新節點後二分搜索樹的根
     * @param node 樹節點
     * @param key 元素key
     * @param value 元素value
     * @return 返回插入新節點後二分搜索樹的根
     */
    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所在的節點
     * @param node 節點
     * @param key 元素key
     * @return
     */
    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爲根的二分搜索樹的最小值所在的節點
     * @param node
     * @return
     */
    private Node minimum(Node node){
        if(node.left == null)
            return node;
        return minimum(node.left);
    }
}

6、時間複雜度分析

紅黑樹相比於AVL樹,實際上是犧牲了平衡性的,紅黑樹並不徹底知足平衡二叉樹的定義,紅黑樹的最大高度達到了2logn的高度,紅色節點影響了紅黑樹的的平衡性。紅黑樹雖然犧牲了必定的查詢性能,可是在增刪改操做的性能獲得了彌補,紅黑樹的綜合性能仍是要優於AVL樹的。

相關文章
相關標籤/搜索