1. 程式人生 > >Java 資料結構與演算法------紅黑樹

Java 資料結構與演算法------紅黑樹

資料結構的本質是:先存資料,然後在用的時候
前面一篇文章介紹了2-3查詢樹,2-3查詢樹能保證在插入元素之後能保證樹的平衡狀態,最壞情況下即所有的子節點都是2-node,樹的高度為lgN,從而保證了最壞情況下的時間複雜度,但是2-3樹實現起來比較複雜,本文介紹一種簡單實現2-3樹的資料結構,即紅黑樹(Red-Black Tree)

紅黑樹的介紹
紅黑樹(Red-BlackTree,簡稱R-BTree),它是一種特殊的二叉查詢樹。紅黑樹是特殊的二叉查詢樹,意味著它滿足二叉查詢樹的特徵:任意一個節點所包含的鍵值,大於等於左孩子的鍵值,小於等於右孩子的鍵值。除了具備該特性之外,紅黑樹還包括許多額外的資訊。紅黑樹的每個節點上都有儲存位表示節點的顏色,顏色是紅(Red)或黑(Black)。
紅黑樹的特性:
(1)每個節點或者是黑色,或者是紅色。
(2)根節點是黑色。
(3)每個葉子節點是黑色,(注意:這裡葉子節點,是指為空的葉子 節點)
(4)如果一個節點的紅色的,則它的子節點必須是黑色的。
(5)從一個節點到該節點的子孫節點的所有路徑上包含相同數目的黑節點。

關於它的特性,需要注意的是:
第一,特性(3)中的葉子節點,是隻為空(NIL或null)的節點。
第二,特性(5),確保沒有一條路徑迴避其他路徑長出兩倍,因而,紅黑樹是接近平衡的二叉樹。
紅黑樹的主要是想對2-3查詢樹進行編碼,尤其是對2-3查詢樹中的3-nodes節點新增額外資訊,紅黑樹中將節點之間的連結分為兩種不同型別,紅色連結,他用來連結兩個2-nodes節點來表示一個3-nodes節點。黑色連結用來連結普通的2-3節點,並且向左傾斜,即一個2-node是另一個2-node的左子節點,這種做法的好處是查詢的時候不用做任何修改,和普通的二叉查詢樹相同。

紅黑樹的主要是想對2-3查詢樹進行編碼,尤其是對2-3查詢樹中的3-nodes節點新增額外的資訊。紅黑樹中將節點之間的連結分為兩種不同型別,紅色連結,他用來連結兩個2-nodes節點來表示一個3-nodes節點。黑色連結用來連結普通的2-3節點。特別的,使用紅色連結的兩個2-nodes來表示一個3-nodes節點,並且向左傾斜,即一個2-node是另一個2-node的左子節點。這種做法的好處是查詢的時候不用做任何修改,和普通的二叉查詢樹相同。

定義

紅黑樹(英語:Red–black tree)是一種自平衡二叉查詢樹,是在電腦科學中用到的一種資料結構,**典型的用途是實現關聯陣列。
紅黑樹的另一種定義是含有紅黑連結並滿足下列條件的二叉查詢樹:
紅連結均為左連結;沒有任何一個結點同時和兩條紅連結相連;該樹是完美黑色平衡的,即任意空連結到根結點的路徑上的黑連結數量相同。
滿足這樣定義的紅黑樹和相應的2-3樹是一一對應的。**

這裡寫圖片描述
根據以上描述,紅黑樹定義如下:
紅黑樹是一種具有紅色和黑色連結的平衡查詢樹,同時滿足:
1 紅色節點向左傾斜
2 一個節點不可能有兩個紅色連結
3整個樹完全黑色平衡,即從根節點到所有葉子節點的路勁上,褐色連結的個數都相同。
紅黑樹平著畫圖就變成2-3樹,那麼他連結的兩個2-node節點就是2-3樹中的一個3-node節點了。

旋轉

旋轉又分為左旋和右旋。通常左旋操作用於將一個向右傾斜的紅色連結旋轉為向左連結。對比操作前後,可以看出,該操作實際上是將紅線連結的兩個節點中的一個較大的節點移動到根節點上。
左旋操作如下圖:
這裡寫圖片描述
右旋操作如下圖:
這裡寫圖片描述

複雜度

紅黑樹的平均高度大約為lgN。
下圖是紅黑樹在各種情況下的時間複雜度,可以看出紅黑樹是2-3查詢樹的一種實現,他能保證最壞情況下仍然具有對數的時間複雜度。

這裡寫圖片描述
1.節點

我們可以在二叉查詢樹的每一個節點上增加一個新的表示顏色的標記。該標記指示該節點指向其父節點的顏色。

private class Node {
Node left, right;//左右子樹
TKey key;//鍵
TValue value;//相關聯的值
int n;//這顆子樹中的節點總數
boolean color;//由父節點指向它的連線的顏色

    public Node(TKey key, TValue value, int number, boolean color) {
        this.key = key;
        this.value = value;
        this.n = number;
        this.color = color;
    }
}

private boolean isRed(Node node) {
    if (node == null) return false;
    return node.color == RED;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
這裡寫圖片描述

2.查詢

紅黑樹是一種特殊的二叉查詢樹,他的查詢方法也和二叉查詢樹一樣,不需要做太多更改。

但是由於紅黑樹比一般的二叉查詢樹具有更好的平衡,所以查詢起來更快。

//查詢獲取指定的值
public TValue get(TKey key) {
return getValue(root, key);
}

private TValue getValue(Node node, TKey key) {
    if (node == null) return null;
    int cmp = key.compareTo(node.Key);
    if (cmp == 0) {
        return node.value;
    } else if (cmp > 0) {
        return getValue(node.right, key);
    } else {
        return getValue(node.left, key);
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
3.平衡化

在介紹插入之前,我們先介紹如何讓紅黑樹保持平衡,因為一般的,我們插入完成之後,需要對樹進行平衡化操作以使其滿足平衡化。

旋轉
旋轉又分為左旋和右旋。通常左旋操作用於將一個向右傾斜的紅色連結旋轉為向左連結。對比操作前後,可以看出,該操作實際上是將紅線連結的兩個節點中的一個較大的節點移動到根節點上。

左旋操作如下圖:
這裡寫圖片描述

這裡寫圖片描述

//左旋轉
private Node rotateLeft(Node h) {
Node x = h.right;
//將x的左節點複製給h右節點
h.right = x.left;
//將h複製給x右節點
x.left = h;
x.color = h.color;
h.color = RED;
x.n = h.n;
h.n = 1 + size(h.left) + size(h.right);
return x;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
左旋的動畫效果如下:
這裡寫圖片描述

右旋是左旋的逆操作,過程如下:
這裡寫圖片描述

這裡寫圖片描述

程式碼如下:

//右旋轉
private Node rotateRight(Node h) {
Node x = h.left;
h.left = x.right;
x.right = h;

    x.color = h.color;
    h.color = RED;
    x.n = h.n;
    h.n = 1 + size(h.left) + size(h.right);
    return x;
}

1
2
3
4
5
6
7
8
9
10
11
12
右旋的動畫效果如下:
這裡寫圖片描述

顏色反轉
當出現一個臨時的4-node的時候,即一個節點的兩個子節點均為紅色,如下圖:
這裡寫圖片描述

這裡寫圖片描述

這其實是個A,E,S 4-node連線,我們需要將E提升至父節點,操作方法很簡單,就是把E對子節點的連線設定為黑色,自己的顏色設定為紅色。

有了以上基本操作方法之後,我們現在對應之前對2-3樹的平衡操作來對紅黑樹進行平衡操作,這兩者是可以一一對應的,如下圖:
這裡寫圖片描述

現在來討論各種情況:

Case 1 往一個2-node節點底部插入新的節點

先熱身一下,首先我們看對於只有一個節點的紅黑樹,插入一個新的節點的操作:
這裡寫圖片描述

這種情況很簡單,只需要:

1.標準的二叉查詢樹遍歷即可。新插入的節點標記為紅色
2.如果新插入的節點在父節點的右子節點,則需要進行左旋操作

Case 2往一個3-node節點底部插入新的節點
假設我們往一個只有兩個節點的樹中插入元素,如下圖,根據待插入元素與已有元素的大小,又可以分為如下三種情況:
這裡寫圖片描述

1.如果帶插入的節點比現有的兩個節點都大,這種情況最簡單。我們只需要將新插入的節點連線到右邊子樹上即可,然後將中間的元素提升至根節點。這樣根節點的左右子樹都是紅色的節點了,我們只需要調研FlipColor方法即可。其他情況經過反轉操作後都會和這一樣。
2.如果插入的節點比最小的元素要小,那麼將新節點新增到最左側,這樣就有兩個連線紅色的節點了,這是對中間節點進行右旋操作,使中間結點成為根節點。這是就轉換到了第一種情況,這時候只需要再進行一次FlipColor操作即可。
3.如果插入的節點的值位於兩個節點之間,那麼將新節點插入到左側節點的右子節點。因為該節點的右子節點是紅色的,所以需要進行左旋操作。操作完之後就變成第二種情況了,再進行一次右旋,然後再呼叫FlipColor操作即可完成平衡操作。

有了以上基礎,我們現在來總結一下往一個3-node節點底部插入新的節點的操作步驟,下面是一個典型的操作過程圖:
這裡寫圖片描述

可以看出,操作步驟如下:
1.執行標準的二叉查詢樹插入操作,新插入的節點元素用紅色標識。
2.如果需要對4-node節點進行旋轉操作
3.如果需要,呼叫FlipColor方法將紅色節點提升
4.如果需要,左旋操作使紅色節點左傾。
5.在有些情況下,需要遞迴呼叫Case1 Case2,來進行遞迴操作。如下:
這裡寫圖片描述

void flipColors(Node h) {
h.color = RED;
h.left.color = BLACK;
h.right.color = BLACK;
}
1
2
3
4
5
插入的實現

private Node put(Node h, TKey key, TValue value) {
if (h == null) {
return new Node(key, value, 1, RED);
}
int cmp = key.compareTo(h.key);
if (cmp < 0) {
h.left = put(h.left, key, value);
} else if (cmp > 0) {
h.right = put(h.right, key, value);
} else {
h.value = value;
}
if (isRed(h.right) && !isRed(h.left)) {
h = rotateLeft(h);
}
if (isRed(h.left) && isRed(h.left.left)) {
h = rotateRight(h);
}
if (isRed(h.left) && isRed(h.right)) {
flipColors(h);
}
h.n = size(h.left) + size(h.right) + 1;
return h;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
紅黑樹的複雜度

對紅黑樹的分析其實就是對2-3查詢樹的分析,紅黑樹能夠保證符號表的所有操作即使在最壞的情況下都能保證對數的時間複雜度,也就是樹的高度。

  1. 在最壞的情況下,紅黑樹的高度不超過2lgN
    最壞的情況就是,紅黑樹中除了最左側路徑全部是由3-node節點組成,即紅黑相間的路徑長度是全黑路徑長度的2倍。

下圖是一個典型的紅黑樹,從中可以看到最長的路徑(紅黑相間的路徑)是最短路徑的2倍:
這裡寫圖片描述

  1. 紅黑樹的平均高度大約為lgN
    下圖是紅黑樹在各種情況下的時間複雜度,可以看出紅黑樹是2-3查詢樹的一種實現,他能保證最壞情況下仍然具有對數的時間複雜度。

下圖是紅黑樹各種操作的時間複雜度。
這裡寫圖片描述

前文講解了自平衡查詢樹中的2-3查詢樹,這種資料結構在插入之後能夠進行自平衡操作,從而保證了樹的高度在一定的範圍內進而能夠保證最壞情況下的時間複雜度。但是2-3查詢樹實現起來比較困難,紅黑樹是2-3樹的一種簡單高效的實現,他巧妙地使用顏色標記來替代2-3樹中比較難處理的3-node節點問題。紅黑樹是一種比較高效的平衡查詢樹,應用非常廣泛,很多程式語言的內部實現都或多或少的採用了紅黑樹。