github地址:https://github.com/duchenlong/Cpp/tree/master/RB_tree/RB_tree
紅黑樹
什麼是紅黑樹
紅黑樹,也是二叉搜索樹的一種形式。
在紅黑樹中,每個節點上增加了一個節點的顏色(RED 和 BLACK)
。他通過對從根節點到葉子節點的路徑上的各個節點的顏色,來判斷是否是接近平衡的。
紅黑樹的性質
- 每一個結點不是紅色就是黑色
- 根節點始終爲黑色
- 如果一個節點是紅色的,那麼他的兩個孩子節點以及父親節點一定是黑色的(沒有相連的紅色節點)
- 對於每一個結點,從該節點到葉子節點的所有路徑中,均包含相同數目的黑色節點
所以說,一個紅黑樹,他的最長路徑中的節點數一定不會超過最短路徑節點個數的兩倍。
最長路徑
:從根節點開始,就是一個黑節點,一個紅色節點交替連接最短路徑
:全部都是黑色節點的路徑
對比AVL樹:
時間複雜度
:AVL樹的時間複雜度爲O(log N)
,紅黑樹的時間複雜度爲O(log 2 * N)
。AVL樹中
,他是一個嚴格平衡的樹,也就是當左右子樹一旦出現不平衡的狀態,就直接進行旋轉操作。紅黑樹
中,只有當破壞了紅黑樹性質的條件時,才進行旋轉調節操作。
所以說,AVL樹
與紅黑樹
對比而言,他們的時間複雜度都是差不多的,只是AVL樹存在着大量的旋轉操作,比較費時間;而紅黑樹的旋轉操作相對而言比較少。
紅黑樹節點的定義
- 節點的顏色
enum Color
{
BLACK,
RED,
};
在STL中對於節點的顏色採用的時候兩個bool類型的變量
,紅色節點爲false,黑色節點爲true。
- 節點的定義
template<class T>
struct RBTreeNode
{
typedef RBTreeNode<T> node;
node* _left;
node* _right;
node* _parent;
T _data;
Color _col;
//構造的節點爲紅色節點
RBTreeNode(const T& data)
{
_left = _right = _parent = nullptr;
_data = data;
_col = RED;
}
};
- 紅黑樹中類的定義
template<class K,class T,class KOfT>
class RBTree
{
public:
typedef RBTreeNode<T> node;
//迭代器
typedef __TreeIterator<T, T&, T*> iterator;
typedef __TreeIterator<T, const T&, const T*> const_iterator;
public:
RBTree()
:_root(nullptr)
{}
private:
node* _root;
};
其中,K表示鍵值,T表示樹中節點的數據類型,一般是一個pair類型的結構體,KOFT是一個結構體,他的作用就是取出T結構體中的鍵值進行比較
一般KOFT的描述可以這樣來,在他的裏面必須重載()
template<class K,class V>
struct cmp
{
const K& operator()(const pair<K, V>& kv)
{
return kv.first;
}
}
插入操作
pair<iterator,bool> Insert(const T& data)
{
}
在插入一個新的節點之前,我們需要考慮的是這個新插入的節點是什麼顏色的?
- 由於紅黑樹的性質二 ,根節點始終爲黑色
那麼當插入節點爲根節點的時候,就是黑色的節點
- 紅黑樹的性質四,對於每一個結點,從該節點到葉子節點的所有路徑中,均包含相同數目的黑色節點
如果我們新插入一個黑色的節點,那麼很容易就會破壞這個特性,所以說我們就讓新插入的節點爲紅色節點
,這樣對整體結構的影響比較小,然後再進行向上調整。
同AVL樹,我們在插入的時候,先要找到待插入的這個節點的位置。
//找到插入節點
if (_root == nullptr)
{
_root = new node(data);
_root->_col = BLACK;
return make_pair(iterator(_root),true);
}
KOfT koft;//比較的函數
node* parent = nullptr;
node* cur = _root;
while (cur)
{
if (koft(cur->_data) == koft(data))
return make_pair(iterator(cur),false);
parent = cur;
cur = (koft(cur->_data) < koft(data)) ? cur->_right : cur->_left;
}
//此時cur爲插入節點的位置,cur即爲nullptr
cur = new node(data);
node* newNode = cur;
if (koft(parent->_data) < koft(cur->_data))
{
parent->_right = cur;
cur->_parent = parent;
}
else if (koft(parent->_data) > koft(cur->_data))
{
parent->_left = cur;
cur->_parent = parent;
}
接下來就是調節的過程了,對於調節而言,因爲我們的新插入的節點爲紅色,那麼當他的父親節點也是紅色的時候,就需要進行調節了。
//調節完畢的條件
while (parent && parent->_col == RED)
對於調整,我們需要根據叔叔節點來判斷調整的方式。爲了找到叔叔節點(u)
,我們就需要祖父節點的幫助(g)
先判斷,新插入的節點在祖父節點的左子樹中的情況
- 叔叔節點存在,並且爲紅色節點
// 1. uncle 存在,並且該節點爲紅色
if (uncle != nullptr && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
//繼續向上處理
cur = grandfather;
parent = cur->_parent;
}
繼續向上處理時,我們需要保證cur爲紅色節點,也就是祖父節點(g)。
- 叔叔節點不存在,那麼cur節點一定是新增的節點(因爲在插入這個節點之前,這棵樹就是一個紅黑樹)
如果叔叔節點存在,那麼在這個分支中,叔叔節點一定是一個黑色節點,我們同樣也進行一次右旋操作
直接對祖父節點進行右旋,然後變色就可以了,這個時候就可以退出循環了。
Left_Left(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
- 當前節點(cur)是 parent 節點的右子樹節點
//3.cur 是parent的右子樹節點
if (cur == parent->_right)
{
Right_Right(parent);
swap(parent, cur);//單純的交換兩個指針的位置,讓他變成第二種情況
}
//2.uncle節點爲nullptr,說明cur結點一定是新增節點,而不是向上調整後的節點
//uncle節點存在,那麼此時他一定是黑色節點
//對grandfather進行一次右旋
Left_Left(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
break;
同理,當新插入的節點是祖父節點的右子樹時
當需要旋轉的時候,
else if (grandfather->_right = parent)
{
node* uncle = grandfather->_left;
if (uncle != nullptr && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
else
{
if (cur == parent->_left)
{
Left_Left(parent);
swap(parent, cur);
}
Right_Right(grandfather);
grandfather->_col = RED;
parent->_col = BLACK;
break;
}
}
右旋
//右旋
void Left_Left(node* parent)
{
node* pParent = parent->_parent;
node* pLeft = parent->_left;
node* pLRight = pLeft->_right;
parent->_left = pLRight;
if (pLRight) pLRight->_parent = parent;
pLeft->_right = parent;
parent->_parent = pLeft;
if (pParent == nullptr)
_root = pLeft;
else
(parent == pParent->_left) ? pParent->_left = pLeft: pParent->_right = pLeft;
pLeft->_parent = pParent;
}
左旋
//左旋
void Right_Right(node* parent)
{
node* pParent = parent->_parent;
node* pRight = parent->_right;
node* pRLeft = pRight->_left;
parent->_right = pRLeft;
if(pRLeft) pRLeft->_parent = parent;
pRight->_left = parent;
parent->_parent = pRight;
if (pParent == nullptr)
_root = pRight;
else
(pParent->_left == parent) ? pParent->_left = pRight : pParent->_right = pRight;
pRight->_parent = pParent;
}
紅黑樹的檢測
對於紅黑樹的檢測,我們需要嚴格根據紅黑樹的性質進行判斷
- 根節點爲黑色
- 對於每一個結點,從該節點到葉子節點的所有路徑中,均包含相同數目的黑色節點
- 沒有相連的紅色節點
//判斷是否是紅黑樹
bool IsRBTree()
{
node* proot = _root;
//空樹也是紅黑樹
if (nullptr == proot)
return true;
//根節點必須爲黑色
if (BLACK != proot->_col)
{
cout << "根節點不爲黑色" << endl;
return false;
}
//獲取任意一條路徑中,黑色節點的數量
size_t blackNum = 0;
node* cur = proot;
while (cur != nullptr)
{
if (BLACK == cur->_col)
blackNum++;
cur = cur->_left;
}
size_t k = 0;
return _IsRBTree(proot, k, blackNum);
}
我們在第一個函數中,先找到一條路徑上的黑色節點數量,然後在判斷這棵樹是否滿足所有路徑的黑色節點數量相等
private:
bool _IsRBTree(node* root, size_t k, size_t blackNum)
{
//走到了葉子節點,判斷黑色節點是否相等
if (nullptr == root)
{
if (k != blackNum)
{
cout << "路徑中黑色節點數量不相等" << endl;
return false;
}
return true;
}
// 統計黑色節點的個數
if (BLACK == root->_col)
k++;
// 檢測當前節點與其雙親是否都爲紅色
node* pParent = root->_parent;
if (pParent != nullptr && RED == pParent->_col && RED == root->_col)
{
cout << "有兩個相連的紅色節點" << endl;
return false;
}
return _IsRBTree(root->_left, k, blackNum) && _IsRBTree(root->_right, k, blackNum);
}
紅黑樹中的迭代器
跟list容器一樣,我們對於迭代器的功能進行了一下封裝,組成一個迭代器的類(這個類在紅黑樹類中得是public的,因爲map和set的底層還需要使用這個迭代器)
- 迭代器的框架
Ref爲數據的引用類型,Ptr爲數據的指針類型
template<class T,class Ref,class Ptr>
struct __TreeIterator
{
typedef RBTreeNode<T> Node;
typedef __TreeIterator<T, Ref, Ptr> Self;
Node* _node;
__TreeIterator(Node* node)
:_node(node)
{}
};
- 自增操作的重載(++)
首先,我們需要知道,自增操作的每一步都是找一個比當前節點數據大的節點,這就被分成了兩步
第一步,如果當前節點存在右子樹,那麼自增的下一步就是右子樹中最小的節點
//如果存在右子樹,那麼下一個節點就是右子樹中最小的那個節點
if (_node->_right)
{
Node* cur = _node->_right;
while (cur->_left)
cur = cur->_left;
_node = cur;
}
第二步,如果當前節點的右子樹不存在,那麼就需要向上找第一個沒有被訪問的節點
else
{
//右子樹不存在,
Node* cur = _node;
Node* parent = cur->_parent;
while (parent && cur == parent->_right)
{
cur = cur->_parent;
parent = parent->_parent;
}
_node = parent;
}
- 自減操作的重載(- -)
同理,只是在自減的操作中,需要找的是下一個比自己小的節點,那麼就需要先判斷左子樹,其他步驟同自增
Self& operator--()
{
if (_node->_left != nullptr)
{
//找到左子樹中最大的一個節點
Node* cur = _node->_left;
while (cur->_right)
cur = cur->_right;
_node = cur;
}
else
{
Node* cur = _node;
Node* parent = cur->_parent;
while (parent && cur == parent->_left)
{
cur = parent;
parent = parent->_parent;
}
_node = parent;
}
return *this;
}
- *,->,==,!=,運算符重載
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
bool operator!=(const Self& s)
{
return _node != s._node;
}
bool operator==(const Self& s)
{
return _node == s._node;
}
- 紅黑樹中的begin,end函數
//迭代器
iterator begin()//返回最小的節點的iterator迭代器
{
node* cur = _root;
while (cur && cur->_left)
cur = cur->_left;
return iterator(cur);
}
iterator end()//返回最大元素的下一個元素,即爲空
{
return iterator(nullptr);
}
STL中 map的實現
map類的框架
template<class K,class V>
class map
{
//比較的函數
struct MapKOfV
{
const K& operator()(const pair<K, V>& data)
{
return data.first;
}
};
typedef typename RBTree<K, pair<K, V>, MapKOfV> RB_type;
typedef typename RB_type::iterator iterator;
private:
RB_type _t;//紅黑樹
};
在map的底層,其實是調用的紅黑樹的接口,所使用的數據都是通過紅黑樹來存儲的。
- []運算符重載
在[]
運算符中,他的內部其實是使用了一次插入的操作,不管該節點是否存在,我們直接對他進行插入操作。
只是在插入的時候,我們只知道鍵值,而不知道所存儲的數據,所以我們對於pair結構體的第二個數據,直接調用這個數據類型的構造函數,創建一個匿名對象。
V& operator[](const K& k)
{
pair<iterator, bool> ans = _t.Insert(make_pair(k, V()));
return ans.first->second;
}
對於他的返回值問題,我們需要返回的是,這個紅黑樹節點(ans.first)的第二個數據(ans.first->second)的引用。
其他的函數沒有什麼可說的,就是調用的紅黑樹的接口。