淺析紅黑樹算法

紅黑樹簡介
node

    

    紅黑樹是一種自平衡二叉查找樹,也有着二叉搜索樹的特性,保持着右邊始終大於左邊結點key的特性。前面提到過的AVL樹,也是二叉搜索樹的一種變形,紅黑樹沒有達到AVL樹的高度平衡,換句話說,它的高度,並無AVL樹那麼高的要求,但他的應用卻更加的普遍,實踐中是至關高效的,他能夠在O(log n)的時間內作查找、插入、刪除操做。在C++ STL中,set、multiset、map、multimap等都應用到的紅黑樹的變體。算法

    紅黑樹在平衡二叉搜索樹的前提下,每一個節點新增了 _color 這一成員變量,用來對各個節點作出標記。接下來,咱們就來分析紅黑樹的插入算法。
ide


    一棵AVL樹,須要知足如下幾條要求。
函數

    一、每一個結點,不是黑色就是紅色
ui

    二、樹的根結點必須是黑色
spa

    三、從根節點到葉子結點的任意一條路上,不容許存在兩個連續的紅色結點。
指針

    四、對於每一個結點,從他開始到每一個葉結點的簡單路徑上,黑色結點樹相同。blog

    

    這裏多說一點,若是知足以上條件的話,從根節點開始,到葉子結點,最長的不會超過最長路徑的兩倍。(能夠考慮最爲極端的狀況)
get


思路簡析it


    和AVL樹相同,要保證樹的平衡性,必需要用到的是旋轉算法。因爲紅黑樹的狀況比較多(儘管寫起代碼來不是很複雜),因此在這裏旋轉的過程當中,咱們不像AVL樹同樣,旋轉的同時對平衡因子進行調整,紅黑樹的旋轉算法,只是單純調整當前結點與其parent 、grandparent 、uncle結點的相對位置,在旋轉完成以後,咱們再對結點顏色進行設置。

    插入算法會在下面給出。


首先咱們給出結點的定義。

enum Color
{
RED,
BLACK
};
template<typename K, typename V>
struct RBTreeNode
{
RBTreeNode<K, V>* _left;
RBTreeNode<K, V>* _right;
RBTreeNode<K, V>* _parent;
K _key;
V _value;
Color _color;
RBTreeNode(const K& key,const V& value)
:_left(NULL)
, _right(NULL)
, _parent(NULL)
, _key(key)
, _value(value)
, _color(RED)//默認構造紅色結點
{}
};

    _key爲關鍵碼(_key值是不容許重複的),_value爲值,關於這裏結點的構造函數,想多說一點,爲何結點顏色要默認給紅色?很明顯,通常狀況下,黑色結點比紅色結點多,但這裏咱們須要注意的是,咱們針對的調整,其實大多數是紅色。黑色結點下若是追加了紅色結點,是不須要調整的,紅色結點下若是多增長了一個黑色結點,是必定要進行調整的。


接下來開始插入結點。

    一、處理特殊狀況

    當樹爲空樹時,直接 new 一個結點給根,而後再改變顏色便可。


if (_root == NULL)
{
_root = new Node(key, value);
_root->_color = BLACK;
return true;
}


    二、樹不爲空樹時,咱們首先須要找到咱們待插入結點的位置。因爲紅黑樹是二叉搜索樹,經過循環,比較待插入結點的key值和當前結點的大小,找到待插入結點的位置。同時給該節點開闢空間,肯定和parent節點的指向關係。

Node* cur = _root;
Node* parent = NULL;
while (cur != NULL)
{
if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;
}
}
cur = new Node(key, value);
if (key > (parent->_key))
{
parent->_right = cur;
cur->_parent = parent;
}
else
{
parent->_left = cur;
cur->_parent = parent;
}

    當插入結點的parent結點爲黑色結點時,不須要作任何調整,只須要和parent結點創建聯繫便可。

    三、下面是須要咱們特殊處理的幾種狀況。

    咱們給出四個Node結點  cur(待插入結點)、parent (cur的父親結點)、grandparent(cur的祖父結點)、uncle(cur的叔叔結點)。

狀況1、

    parent爲黑色,uncle存在且爲紅色

如圖:

wKiom1gYVE2CRjhqAABWzAQAwGw226.png

    三角形結點只是表示可能存在的結點,可能爲空。

    當cur爲新插入結點時,a-e結點均爲空結點,因爲不能夠存在連續的紅結點,所以,咱們須要將parent結點和uncle結點變爲黑色。細心的話能夠發現,grandparent結點變爲了紅色,這是由於當grandparent不爲根節點時,咱們這棵子樹的一條支路上的黑色結點就會多出一個,所以咱們須要將grandparent結點變爲紅色,而後繼續向上進行調整。在插入完成以後,咱們只須要統一將根節點從新賦值爲紅色便可。

狀況2、

    parent爲紅色,uncle結點不存在,或uncle結點存在,但爲黑色

如圖:

wKiom1gYYAjykLoIAAD0M3hG5tM480.png

   看到第一張圖的時候,不要懷疑這裏畫的有問題,這種狀況是可能存在的,那就是說,cur是調整上來的,從個人上一種狀況調整過來的,雖然看着grandparent的左右支路黑色結點數不相同,但我還有下面的三角形結點。

    如今我這裏就須要進行旋轉,爲何這裏不能直接顏色變換?由於咱們拋過三角形結點,以grandparent結點爲分界,最左支路和最後支路的,黑色結點數差一。旋轉的圖示如上圖所示,以grandparent結點爲軸,向右旋轉。將grandparent結點做爲parent結點的右子樹進行旋轉。同時須要的是,grandparent結點不必定是根節點,咱們須要提早保留並判斷grandparent->_parent結點,以後從新賦給parent->_parent。


狀況3、

    若是能夠理解了第二種狀況,第三種狀況就容易理解了許多,和第二種狀況同樣,只不過cur是parent的右子樹,咱們須要先以parent爲軸,向左旋轉,獲得上面這種狀況以後,再以grandparent爲軸向右旋轉。以下圖。


wKiom1gYXv2gYEMCAADKDeliXQg520.png

值得注意的一點,也是一開始寫代碼老是驗證出錯的一個問題,咱們先以parent爲軸左旋,以後看上圖,cur此時變成了parent->_parent,若是此時按照狀況二的處理方式,結點顏色必定會發生問題,所以,在上圖中,我專門給出了一張圖,將parent和cur指針交換,注意,只交換的是指針。


到這裏,紅黑樹的基本狀況以及處理完畢,再有的話就是當parent一開始就是在grandparent的右子樹上的幾種狀況,和上面的旋轉成鏡像的關係。下面給出具體的代碼:


bool Insert(const K& key,const V& value)
{
//空樹
if (_root == NULL)
{
_root = new Node(key, value);
_root->_color = BLACK;
return true;
}
//構建節點,並插入到對應位置
Node* cur = _root;
Node* parent = NULL;
while (cur != NULL)
{
if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else
{
return false;
}
}
cur = new Node(key, value);
if (key > (parent->_key))
{
parent->_right = cur;
cur->_parent = parent;
}
else
{
parent->_left = cur;
cur->_parent = parent;
}
//開始調整
while (cur != _root && parent->_color == RED)
{
//若是parent的color爲RED,parent必定不是根節點,且祖父節點color爲BLACK
Node* grandparentnode = parent->_parent;//grandparentnode->_color = BLACK;
if (parent == grandparentnode->_left)
{
Node* unclenode = grandparentnode->_right;//叔叔節點uncle
if (unclenode && (unclenode->_color == RED))//uncle不爲空,且uncle->color爲RED
{
parent->_color = BLACK;
unclenode->_color = BLACK;
grandparentnode->_color = RED;
cur = grandparentnode;
parent = cur->_parent;
}
else//uncle爲空,或uncle->color爲BLACK
{
if (cur == parent->_right)
{
RotateL(parent);
std::swap(parent, cur);
}
RotateR(grandparentnode);
parent->_color = BLACK;
grandparentnode->_color = RED;
break;
}
}
else//parent == grandparent->_right
{
Node* unclenode = grandparentnode->_left;
if (unclenode && (unclenode->_color == RED))//uncle存在,且color爲 RED
{
parent->_color = BLACK;
unclenode->_color = BLACK;
grandparentnode->_color = RED;
cur = grandparentnode;
parent = cur->_parent;
}
else//uncle不存在,或uncle->color爲黑色
{
if (cur == parent->_left)
{
RotateR(parent);
std::swap(cur,parent);
}
RotateL(grandparentnode);
grandparentnode->_color = RED;
parent->_color = BLACK;
break;
}
}
}
//統一將根節點的顏色變爲黑色
_root->_color = BLACK;
return true;
}


    紅黑樹結點的插入到這裏就結束了,能夠發現的是,咱們其實一直在關注的是uncle結點,也就是cur的叔叔結點。這是紅黑樹插入思想裏面的一個核心。

    下面,就紅黑樹的基本特徵,給出一段檢驗函數,判斷紅黑樹是否知足要求。


bool IsBalance()
{
if (_root == NULL)
return true;
if (_root->_color == RED)
return false;
int count = 0;
Node* cur = _root;
while (cur != NULL)
{
if (cur->_color == BLACK)
{
count++;
}
cur = cur->_left;
}
int k = 0;
return _IsBalance(_root, count, k);
}
bool _IsBalance(Node* root, const int& count, int k)
{
if (root == NULL)
return true;
if (root != _root && root->_color == RED)
{
if (root->_parent->_color == RED)
{
cout << "連續紅色結點" << root->_key << endl;
return false;
}
}
if (root->_color == BLACK)
k++;
if (root->_left == NULL && root->_right == NULL)
{
if (k == count)
return true;
else
{
cout << "黑色節點不相等" << root->_key << endl;
return false;
}
}
return _IsBalance(root->_left, count, k) \
&& _IsBalance(root->_right, count, k);
}


    紅黑樹的應用遠比AVL樹多,仍是一開始咱們說的,其實紅黑樹的高度相對來講要比AVL樹高出一些的,但這其實並不影響太多。由於咱們的時間複雜度都是在O(log n)附近,當n = 10億時,log(n)也僅僅只有30。可是另外一方面,因爲紅黑樹要比AVL樹的要求低,因此當咱們插入一個結點時,相對來講調整的次數也就少了許多,這個是紅黑樹的優點。

                                    ------muhuizz整理

相關文章
相關標籤/搜索