紅黑樹
轉自:http://www.cnblogs.com/yangecnu/p/Introduce-2-3-Search-Tree.html
前面一篇文章介紹了2-3查找樹,可以看到,2-3查找樹能保證在插入元素之後能保持樹的平衡狀態,最壞情況下即所有的子節點都是2-node,樹的高度為lgN,從而保證了最壞情況下的時間復雜度。但是2-3樹實現起來比較復雜,本文介紹一種簡單實現2-3樹的數據結構,即紅黑樹(Red-Black Tree)
定義
紅黑樹的主要是想對2-3查找樹進行編碼,尤其是對2-3查找樹中的3-nodes節點添加額外的信息。紅黑樹中將節點之間的鏈接分為兩種不同類型,紅色鏈接,他用來鏈接兩個2-nodes節點來表示一個3-nodes節點。黑色鏈接用來鏈接普通的2-3節點。特別的,使用紅色鏈接的兩個2-nodes來表示一個3-nodes節點,並且向左傾斜,即一個2-node是另一個2-node的左子節點。這種做法的好處是查找的時候不用做任何修改,和普通的二叉查找樹相同。
根據以上描述,紅黑樹定義如下:
紅黑樹是一種具有紅色和黑色鏈接的平衡查找樹,同時滿足:
- 紅色節點向左傾斜
- 一個節點不可能有兩個紅色鏈接
- 整個書完全黑色平衡,即從根節點到所以葉子結點的路徑上,黑色鏈接的個數都相同。
下圖可以看到紅黑樹其實是2-3樹的另外一種表現形式:如果我們將紅色的連線水平繪制,那麽他鏈接的兩個2-node節點就是2-3樹中的一個3-node節點了。
表示
我們可以在二叉查找樹的每一個節點上增加一個新的表示顏色的標記。該標記指示該節點指向其父節點的顏色。
private const bool RED = true;
private const bool BLACK = false;
private Node root;
class Node
{
public Node Left { get; set; }
public Node Right { get; set; }
public TKey Key { get; set; }
public TValue Value { get; set; }
public int Number { get; set; }
public bool Color { get; set; }
public Node(TKey key, TValue value,int number, bool color)
{
this.Key = key;
this.Value = value;
this.Number = number;
this.Color = color;
}
}
private bool IsRed(Node node)
{
if (node == null) return false;
return node.Color == RED;
}
實現
查找
紅黑樹是一種特殊的二叉查找樹,他的查找方法也和二叉查找樹一樣,不需要做太多更改。
但是由於紅黑樹比一般的二叉查找樹具有更好的平衡,所以查找起來更快。
//查找獲取指定的值
public override TValue Get(TKey key)
{
return GetValue(root, key);
}
private TValue GetValue(Node node, TKey key)
{
if (node == null) return default(TValue);
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);
}
平衡化
在介紹插入之前,我們先介紹如何讓紅黑樹保持平衡,因為一般的,我們插入完成之後,需要對樹進行平衡化操作以使其滿足平衡化。
旋轉
旋轉又分為左旋和右旋。通常左旋操作用於將一個向右傾斜的紅色鏈接旋轉為向左鏈接。對比操作前後,可以看出,該操作實際上是將紅線鏈接的兩個節點中的一個較大的節點移動到根節點上。
左旋操作如下圖:
//左旋轉
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;
return x;
}
左旋的動畫效果如下:
右旋是左旋的逆操作,過程如下:
代碼如下:
//右旋轉
private Node RotateRight(Node h)
{
Node x = h.Left;
h.Left = x.Right;
x.Right = h;
x.Color = h.Color;
h.Color = RED;
return x;
}
右旋的動畫效果如下:
顏色反轉
當出現一個臨時的4-node的時候,即一個節點的兩個子節點均為紅色,如下圖:
這其實是個A,E,S 4-node連接,我們需要將E提升至父節點,操作方法很簡單,就是把E對子節點的連線設置為黑色,自己的顏色設置為紅色。
有了以上基本操作方法之後,我們現在對應之前對2-3樹的平衡操作來對紅黑樹進行平衡操作,這兩者是可以一一對應的,如下圖:
現在來討論各種情況:
Case 1 往一個2-node節點底部插入新的節點
先熱身一下,首先我們看對於只有一個節點的紅黑樹,插入一個新的節點的操作:
這種情況很簡單,只需要:
- 標準的二叉查找樹遍歷即可。新插入的節點標記為紅色
- 如果新插入的節點在父節點的右子節點,則需要進行左旋操作
Case 2往一個3-node節點底部插入新的節點
先熱身一下,假設我們往一個只有兩個節點的樹中插入元素,如下圖,根據待插入元素與已有元素的大小,又可以分為如下三種情況:
- 如果帶插入的節點比現有的兩個節點都大,這種情況最簡單。我們只需要將新插入的節點連接到右邊子樹上即可,然後將中間的元素提升至根節點。這樣根節點的左右子樹都是紅色的節點了,我們只需要調研FlipColor方法即可。其他情況經過反轉操作後都會和這一樣。
- 如果插入的節點比最小的元素要小,那麽將新節點添加到最左側,這樣就有兩個連接紅色的節點了,這是對中間節點進行右旋操作,使中間結點成為根節點。這是就轉換到了第一種情況,這時候只需要再進行一次FlipColor操作即可。
- 如果插入的節點的值位於兩個節點之間,那麽將新節點插入到左側節點的右子節點。因為該節點的右子節點是紅色的,所以需要進行左旋操作。操作完之後就變成第二種情況了,再進行一次右旋,然後再調用FlipColor操作即可完成平衡操作。
有了以上基礎,我們現在來總結一下往一個3-node節點底部插入新的節點的操作步驟,下面是一個典型的操作過程圖:
可以看出,操作步驟如下:
- 執行標準的二叉查找樹插入操作,新插入的節點元素用紅色標識。
- 如果需要對4-node節點進行旋轉操作
- 如果需要,調用FlipColor方法將紅色節點提升
- 如果需要,左旋操作使紅色節點左傾。
- 在有些情況下,需要遞歸調用Case1 Case2,來進行遞歸操作。如下:
代碼實現
經過上面的平衡化討論,現在就來實現插入操作,一般地插入操作就是先執行標準的二叉查找樹插入,然後再進行平衡化。對照2-3樹,我們可以通過前面討論的,左旋,右旋,FlipColor這三種操作來完成平衡化。
具體操作方式如下:
- 如果節點的右子節點為紅色,且左子節點位黑色,則進行左旋操作
- 如果節點的左子節點為紅色,並且左子節點的左子節點也為紅色,則進行右旋操作
- 如果節點的左右子節點均為紅色,則執行FlipColor操作,提升中間結點。
根據這一邏輯,我們就可以實現插入的Put方法了。
public override void Put(TKey key, TValue value)
{
root = Put(root, key, value);
root.Color = BLACK;
}
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.Right) && IsRed(h.Left.Left)) h = RotateRight(h);
if (IsRed(h.Left) && IsRed(h.Right)) h = FlipColor(h);
h.Number = Size(h.Left) + Size(h.Right) + 1;
return h;
}
private int Size(Node node)
{
if (node == null) return 0;
return node.Number;
}
分析
對紅黑樹的分析其實就是對2-3查找樹的分析,紅黑樹能夠保證符號表的所有操作即使在最壞的情況下都能保證對數的時間復雜度,也就是樹的高度。
在分析之前,為了更加直觀,下面是以升序,降序和隨機構建一顆紅黑樹的動畫:
- 以升序插入構建紅黑樹:
- 以降序插入構建紅黑樹:
- 隨機插入構建紅黑樹
從上面三張動畫效果中,可以很直觀的看出,紅黑樹在各種情況下都能維護良好的平衡性,從而能夠保證最差情況下的查找,插入效率。
下面來詳細分析下紅黑樹的效率:
1. 在最壞的情況下,紅黑樹的高度不超過2lgN
最壞的情況就是,紅黑樹中除了最左側路徑全部是由3-node節點組成,即紅黑相間的路徑長度是全黑路徑長度的2倍。
下圖是一個典型的紅黑樹,從中可以看到最長的路徑(紅黑相間的路徑)是最短路徑的2倍:
2. 紅黑樹的平均高度大約為lgN
下圖是紅黑樹在各種情況下的時間復雜度,可以看出紅黑樹是2-3查找樹的一種實現,他能保證最壞情況下仍然具有對數的時間復雜度。
下圖是紅黑樹各種操作的時間復雜度。
應用
紅黑樹這種數據結構應用十分廣泛,在多種編程語言中被用作符號表的實現,如:
- Java中的java.util.TreeMap,java.util.TreeSet
- C++ STL中的:map,multimap,multiset
- .NET中的:SortedDictionary,SortedSet 等
下面以.NET中為例,通過Reflector工具,我們可以看到SortedDictionary的Add方法如下:
public void Add(T item)
{
if (this.root == null)
{
this.root = new Node<T>(item, false);
this.count = 1;
}
else
{
Node<T> root = this.root;
Node<T> node = null;
Node<T> grandParent = null;
Node<T> greatGrandParent = null;
int num = 0;
while (root != null)
{
num = this.comparer.Compare(item, root.Item);
if (num == 0)
{
this.root.IsRed = false;
ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_AddingDuplicate);
}
if (TreeSet<T>.Is4Node(root))
{
TreeSet<T>.Split4Node(root);
if (TreeSet<T>.IsRed(node))
{
this.InsertionBalance(root, ref node, grandParent, greatGrandParent);
}
}
greatGrandParent = grandParent;
grandParent = node;
node = root;
root = (num < 0) ? root.Left : root.Right;
}
Node<T> current = new Node<T>(item);
if (num > 0)
{
node.Right = current;
}
else
{
node.Left = current;
}
if (node.IsRed)
{
this.InsertionBalance(current, ref node, grandParent, greatGrandParent);
}
this.root.IsRed = false;
this.count++;
this.version++;
}
}
可以看到,內部實現也是一個紅黑樹,其操作方法和本文將的大同小異,感興趣的話,您可以使用Reflector工具跟進去查看源代碼。
總結
前文講解了自平衡查找樹中的2-3查找樹,這種數據結構在插入之後能夠進行自平衡操作,從而保證了樹的高度在一定的範圍內進而能夠保證最壞情況下的時間復雜度。但是2-3查找樹實現起來比較困難,紅黑樹是2-3樹的一種簡單高效的實現,他巧妙地使用顏色標記來替代2-3樹中比較難處理的3-node節點問題。紅黑樹是一種比較高效的平衡查找樹,應用非常廣泛,很多編程語言的內部實現都或多或少的采用了紅黑樹。
紅黑樹