红黑树的大名以及关于关联式容器用的比较少,想一探究竟,于是先跳过了序列式容器。本博客主要记载比较重要以及自己理解的部分。
红黑树
红黑树是二叉搜索树,且满足一下规则
1 每个节点不是红色就是黑色(在代码上用bool类型0/1分别表示)
2 根节点为黑色
3 如果节点为红色,其子结点必须为黑。(看过一些说法,就是不能子结点和父节点同时为红)
4 任一节点至NULL(树尾端)的任何路径,所含之黑节点数必相同(意味着新增节点必须为红)
在插入数据进入红黑树时,会导致不平衡或者颜色不满足红黑树规则,于是要作出调整,通常调整有以下几种
其实从以上的旋转可以发现,和之前学过的平衡树的旋转很像,分别有单旋和双旋,根据在树的左侧还是右侧,就有右旋转和向左旋转。还有双旋的情况,由于这和之前学过的比较类似,所以不记载了。
但是,如果按照状况4的方式,父子结点皆为红色的情况持续向RB-tree的上层结构发展,会造成处理时效的瓶颈,于是可以采用一个由上而下的程序:假设新增节点为A,那么沿着A的路径,只要看到某节点X的两个子节点皆为红色,就把X改为红色,并把两个子节点改为黑色
再对上图进行旋转,使得符合红黑树规则,
下图为旋转后,并插入了35节点的红黑树:
红黑树中的每个节点数据结构有颜色、父节点(必须要知道)、左子节点、右子节点、节点值
调整RB-tree(旋转以及改变颜色)
这个树形调整,就是根据上面说的自上而下的程序设计的,这个程序设计我觉得巧妙的地方就在于可以做到有时只需单旋转,有时做双旋转,只是根据一个判断条件单选转的程序可被双选转的第二个旋转复用,原因在于没有伯父节点时,x=x->parent了,以下的函数的原理都是上面讲过的,因此可以用来学习怎么对照原理进行写代码,学习编码习惯以及技巧
/ 全局函数
// 重新令树形平衡(改变颜色及旋转树形)
// 参数一为新增节点,参数二为根节点root
inline void _rb_tree_rebalance(_rb_tree_node_base* x , _rb_tree_node_base*& root)
{
x->color = _rb_tree_red; //新节点必为红
while(x != root && x->parent->color == _rb_tree_red) // 父节点为红
{
if(x->parent == x->parent->parent->left) // 父节点为祖父节点之左子节点
{
_rb_tree_node_base* y = x->parent->parent->right; // 令y为伯父节点
if(y && y->color == _rb_tree_red) // 伯父节点存在,且为红
{
x->parent->color = _rb_tree_black; // 更改父节点为黑色
y->color = _rb_tree_black; // 更改伯父节点为黑色
x->parent->parent->color = _rb_tree_red; // 更改祖父节点为红色
x = x->parent->parent;
}
else // 无伯父节点,或伯父节点为黑色
{
if(x == x->parent->right) // 如果新节点为父节点之右子节点
{
x = x->parent;
_rb_tree_rotate_left(x , root); // 第一个参数为左旋点
}
x->parent->color = _rb_tree_black; // 改变颜色
x->parent->parent->color = _rb_tree_red;
_rb_tree_rotate_right(x->parent->parent , root); // 第一个参数为右旋点
}
}
else // 父节点为祖父节点之右子节点
{
_rb_tree_node_base* y = x->parent->parent->left; // 令y为伯父节点
if(y && y->color == _rb_tree_red) // 有伯父节点,且为红
{
x->parent->color = _rb_tree_black; // 更改父节点为黑色
y->color = _rb_tree_black; // 更改伯父节点为黑色
x->parent->parent->color = _rb_tree_red; // 更改祖父节点为红色
x = x->parent->parent; // 准备继续往上层检查
}
else // 无伯父节点,或伯父节点为黑色
{
if(x == x->parent->left) // 如果新节点为父节点之左子节点
{
x = x->parent;
_rb_tree_rotate_right(x , root); // 第一个参数为右旋点
}
x->parent->color = _rb_tree_black; // 改变颜色
x->parent->parent->color = _rb_tree_red;
_rb_tree_rotate_left(x->parent->parent , root); // 第一个参数为左旋点
}
}
}//while
root->color = _rb_tree_black; // 根节点永远为黑色
}
// 左旋函数
inline void _rb_tree_rotate_left(_rb_tree_node_base* x , _rb_tree_node_base*& root)
{
// x 为旋转点
_rb_tree_node_base* y = x->right; // 令y为旋转点的右子节点
x->right = y->left;
if(y->left != 0)
y->left->parent = x; // 别忘了回马枪设定父节点
y->parent = x->parent;
// 令y完全顶替x的地位(必须将x对其父节点的关系完全接收过来)
if(x == root) // x为根节点
root = y;
else if(x == x->parent->left) // x为其父节点的左子节点
x->parent->left = y;
else // x为其父节点的右子节点
x->parent->right = y;
y->left = x;
x->parent = y;
}
// 右旋函数
inline void _rb_tree_rotate_right(_rb_tree_node_base* x , _rb_tree_node_base*& root)
{
// x 为旋转点
_rb_tree_node_base* y = x->left; // 令y为旋转点的左子节点
x->left = y->right;
if(y->right != 0)
y->right->parent = x; // 别忘了回马枪设定父节点
y->parent = x->parent;
// 令y完全顶替x的地位(必须将x对其父节点的关系完全接收过来)
if(x == root)
root = y;
else if(x == x->parent->right) // x为其父节点的右子节点
x->parent->right = y;
else // x为其父节点的左子节点
x->parent->left = y;
y->right = x;
x->parent = y;
}
红黑树中的一个operator++其实是调用increment函数,这个函数是找接下来的大的值,我觉得程序挺有意思的,可以学习一下
void increment()
{
/*当有右子节点时,先向右走再一直往左子树走到底,即为答案*/
if (node->right!=0)
{
node=node->right;
while(node->left!=0)
node=node->left;
}
/*没有右子节点,就要找出父节点,如果出现的节点本身是个右子节点就一直向上追溯,直到不是*/
else
{
base_ptr y=node->parent;
while(node==y->right)
{
node=y;
y=y->parent;
}
if(node->right!=y)
node=y; /*这个写法,即可以得出上面状况2的答案,同时也能解决特殊情况,就是寻找根节点的下一节点,但是恰巧没有右子节点的情况,要配合红黑树根节点与特殊的header的关系*/
}
}
decrement()代码思路类似,就是向左向右相反。
RB树的起头为最左节点处,RB树的终点为header所指处
header和root互为parent,一开始初始化时,header父指针为0.,在节点插入时,都要维护header节点,使得其左子节点指向最小节点,右子节点指向最大节点,父节点指向根节点。
RB-tree的元素操作
RB-tree提供两种插入操作,分别是insert_unique()和insert_equal(),前者表示被插入节点的键值在整棵树中必须独一无二,因此在插入函数中如果已经存在相同的值,就不会执行插入操作。后者表示被插入的节点的键值在整棵树可以重复。这两个函数都会数个版本,在书中只以单个参数的作为说明对象。
因为insert_equal()比较容易,在阅读的时候没有障碍,而insert_unique()函数在阅读时有点障碍,现在记录下来
// 安插新值;節點鍵值不允許重複,若重複則安插無效。
// 注意,傳回值是個pair,第一元素是個 RB-tree 迭代器,指向新增節點,
// 第二元素表示安插成功與否。
template <class Key, class Value, class KeyOfValue, class Compare, class Alloc>
pair<typename rb_tree<Key, Value, KeyOfValue, Compare, Alloc>::iterator, bool>
rb_tree<Key, Value, KeyOfValue, Compare, Alloc>::insert_unique(const Value& v)
{
link_type y = header;
link_type x = root(); // 從根節點開始
bool comp = true;
while (x != 0) { // 從根節點開始,往下尋找適當的安插點
y = x;
comp = key_compare(KeyOfValue()(v), key(x)); // v 鍵值小於目前節點之鍵值?
x = comp ? left(x) : right(x); // 遇「大」則往左,遇「小於或等於」則往右
}
// 離開 while 迴圈之後,y 所指即安插點之父節點(此時的它必為葉節點)
iterator j = iterator(y); // 令迭代器j指向安插點之父節點 y
if (comp) // 如果離開 while 迴圈時 comp 為真(表示遇「大」,將安插於左側)
if (j == begin()) // 如果安插點之父節點為最左節點
return pair<iterator,bool>(__insert(x, y, v), true);
// 以上,x 為安插點,y 為安插點之父節點,v 為新值。
else // 否則(安插點之父節點不為最左節點)
--j; // 調整 j,回頭準備測試...
if (key_compare(key(j.node), KeyOfValue()(v)))
// 小於新值(表示遇「小」,將安插於右側)
return pair<iterator,bool>(__insert(x, y, v), true);
// 進行至此,表示新值一定與樹中鍵值重複,那麼就不該插入新值。
return pair<iterator,bool>(j, false);
}
以上代码中,根据我的理解如下
1 while循环是为了找出适当的插入点,离开while循环的y即为插入点的父节点,此时必为叶节点 。
2,离开while循环后马上令iterator j=iterator(y)是有两个作用的: (1) 当comp为ture,表示遇大值,需要插入左侧。插入左侧这时就要考虑,是否是重复的数字,怎么判断重复呢,很简单,就是判断待插入的值和父节点–后(红黑树的–)的数值情况。因为begin()–没有意义,所以先单独判断J==begin(),如果是则可以直接插入。如果不是,令J–,再通过if (key_compare(key(j.node), KeyOfValue()(v))) ,如果j.node的值小于V的值,意味着没有重复,则可以插入,if内的判断为ture,意味着数字重复,则直接返回 return pair<iterator,bool>(j, false);。 (2)当comp为false时,意味着插入右侧,但是这个值有可能与待插入的父节点的值相同,因为“遇小于等于都往右”,如果一开始comp是false的,j就是iterator(y)的值,所以只要父节点的值小于V的值就表示可以插入,如果为false,意味着有重复,则直接返回 return pair<iterator,bool>(j, false);
这里之所以让人难以理解一点,有可能是因为这个key_compare(x,y),在这里可以理解为当x<y时为true,x>=y时返回false;
注意return里用的是y返回,不是j,j在这就是辅助判断作用。而节点插入父节点的左侧还是右侧,由真正执行插入程序__insert()负责。
template <class Key, class Value, class KeyOfValue, class Compare, class Alloc>
typename rb_tree<Key, Value, KeyOfValue, Compare, Alloc>::iterator
rb_tree<Key, Value, KeyOfValue, Compare, Alloc>::
__insert(base_ptr x_, base_ptr y_, const Value& v) {
// 參數x_ 為新值安插點,參數y_ 為安插點之父節點,參數v 為新值。
link_type x = (link_type) x_;
link_type y = (link_type) y_;
link_type z;
// key_compare 是鍵值大小比較準則。應該會是個 function object。
if (y == header || x != 0 || key_compare(KeyOfValue()(v), key(y))) { //x != 0时,直接插到y的左儿子处
z = create_node(v); // 產生一個新節點
left(y) = z; // 這使得當 y 即為 header時,leftmost() = z
if (y == header) {
root() = z;
rightmost() = z;
}
else if (y == leftmost()) // 如果y為最左節點
leftmost() = z; // 維護leftmost(),使它永遠指向最左節點
}
else {
z = create_node(v); // 產生一個新節點
right(y) = z; // 令新節點成為安插點之父節點 y 的右子節點
if (y == rightmost())
rightmost() = z; // 維護rightmost(),使它永遠指向最右節點
}
parent(z) = y; // 設定新節點的父節點
left(z) = 0;
right(z) = 0;
__rb_tree_rebalance(z, header->parent); // 參數一為新增節點,參數二為 root
++node_count; // 節點數累加
return iterator(z); // 傳回一個迭代器,指向新增節點
}
在判断条件中 if (y == header || x != 0 || key_compare(KeyOfValue()(v), key(y))) , 最后一个判定条件为true 意味着 v小于Y 所以插入左侧!!!正是这就能判断插入左侧还是要右侧,之前纠结了一会,没认真看到。 这里x!=0 感觉基本不会用到 因为从调用它的函数中,都是从while循环中出来的 那时x已经要等于0 了,
在插入函数中要记得维护hearder节点,和新增的节点的左右子节点