1. STL红黑树
1.1 基础结构
-
_Rb_tree_node_base
中包含_M_color
、三个指针_M_parent
、_M_left
和_M_right
_Rb_tree_node
继承自上述类,额外存储值。 -
头节点
_Rb_tree_header
,红色,_M_node_count
维护节点数量,其_M_parent
指向root
结点。
左孩子为红黑树最左侧节点(begin()
迭代器位置);右孩子为最右侧节点(最大的节点)
头结点本身作为end()
迭代器指向位置。 -
实际的实现中
_M_impl
负责节点分配,并继承了头结点
1.2 迭代器遍历
(1)在increment
函数中
其正常范围为 begin() ~ end()
,对end()
自增会回退到最大节点,再自增又会回到end()
- 1.若有右子树,则其后继为 右子树中最小的节点
- 2.若无右子树,则其后继为 祖先节点(该节点作为其左子树出现)
- 处理特殊情况,根节点无右子树才不会执行下述情况,保证了最大节点的后继为 header节点
if ( __x->_M_right != __y ) __x = __y;
- 处理特殊情况,根节点无右子树才不会执行下述情况,保证了最大节点的后继为 header节点
注意:在上述函数中,若当前节点为header节点,则对其自增,下一个节点为最大的节点,尤其是在使用int
的情况下,很容易混淆普通节点和header节点。验证代码如下
set<int> mset;
mset.insert(4);
mset.insert(2);
mset.insert(6);
set<int>::iterator iter = mset.end();
iter++;
cout << *mset.end() << endl; // 3 此处end()指向header节点,其代表的是红黑树中节点的数量
cout << *iter << endl; // 6
(2)在decrement
函数中
- 1.若当前节点为header,则其前驱为 最大节点
- 2.若当前节点有左子树,则其前驱为 左子树中最大节点
- 3.若无左子树,则其前驱为 祖先节点(该节点作为其右子树出现)
其正常范围为 end() ~ begin()
,对begin()
自减根据root节点是否有左子树,会出现两种情况
若root有左子树,对begin()
自减会到header节点
若root无左子树,对begin()
自减仍然会到root节点(即begin()
所指向位置)
// 1.root有左子树的情况
set<int> mset;
mset.insert(4);
mset.insert(2);
mset.insert(6);
set<int>::iterator iter = mset.begin();
iter--;
cout << *mset.begin() << endl; // 输出 2
cout << *iter << endl; //输出3, 指向header节点
// 2.root无左子树的情况
set<int> mset;
mset.insert(4);
mset.insert(6);
set<int>::iterator iter = mset.begin();
iter--;
cout << *mset.begin() << endl; // 输出4
cout << *iter << endl; // 输出4
感觉此处也应该像increment
在第3种情况下加一个特殊情况的判断,以统一上述两种情况。
1.3 插入函数
_M_begin
代表的root节点,可能为空(只有header节点情况)
_M_end
代表的是header节点
这部分主要关注下_M_insert_unique
函数
map
和set
插入时都会调用_M_insert_unique
1.3.1 判断是否出现过关键字
插入函数首先调用_M_get_insert_unique_pos
获取插入位置, __x
初始为root节点,__y
始终是其父节点
在该函数中,首先按照二叉搜索树的性质找到叶子节点,同时不断维护其父节点__y
。正常情况下,__x == 0
即找到插入位置。
但为了保证插入值是唯一的(即之前未出现在红黑树中),还需要做一些额外的判断。
iterator __j = iterator(__y);
if (__comp) // 如果是往左子树插入。 说明 __k < __y.key
{
if (__j == begin()) // 特殊情况,此情况下当前节点没有前驱节点(即已经是最小的节点);没有root节点,或root节点无左子树的情况
return _Res(__x, __y);
else
--__j; // 需要找到其父节点前驱结点,确保 父节点的前驱 < __k,才能保证不出现相同key值
// 即确保 (--__y).key < __k < __y.key
}
// 如果是往右子树插入,可以确定的是 __k >= __y.key, 下述条件将其等号去掉,即__k > __y.key
// 此处为何不需要找到 父节点的后继节点进行上述双边判断?
if (_M_impl._M_key_compare(_S_key(__j._M_node), __k))
return _Res(__x, __y);
return _Res(__j._M_node, 0); // 插入失败的情况
(1)插入左子树情况需要进行双边界判断的情况,举例说明:对于下述树,如果插入2,插入位置在结点3的左子树。因此必须进行双边界判断。
4
/ \
2 5
/ \
1 3
(2)为何插入右子树不需要找到 父节点的后继节点进行上述双边界判断?
-
假设插入结点 x x x已经在树中,如果是插入到某个 y y y结点的右子树,那么在遍历过程中,必定经过了 x x x结点,根据比较规则,需要向右子树遍历,那么会有两种情况:
- y y y结点是 x x x结点右子树,那么 y > x y > x y>x,又由于要插入到 y y y结点的右子树,有 x ≥ y x \ge y x≥y,矛盾(如果是插入 y y y结点的左子树,则这种情况也不矛盾,需要进行额外判断)
- y y y结点就是 x x x结点,这种情况就是上述代码的判断的情况。
(3)这里的__x
是否有必要返回?经过循环条件可以确定其值为0,猜测这里可能是兼容性考虑?
(4)_M_get_insert_equal_pos
则省去了上述判断过程,根据搜索树的性质找到插入位置即可。
开卷有益: STL不愧是经过时间和实践考验出来的经典开源库,里面很多涉及细节还是值得我们学习的。
1.3.2 实际插入与平衡
这里确保了红黑树中未出现过该关键值__v
(1)判断是否插入到__p
的左子树中。如果__p
为header节点,则认为插入到其左子树中。
若是,在插入过程中需要维护header结点的_M_left
若不是,在插入过程中需要维护header结点的_M_right
(2)插入与平衡过程
- 因为红黑树要求根到所有叶子节点路径上的黑节点数量一致。因此,若要增加黑节点数量,必须是从根结点处增加(有点类似B+树的思想,整体树高的增加是通过root结点向上分裂实现的)。
为了尽可能红黑树的性质,新创建的结点颜色为红色。 - 插入一个新的红色节点可能会打破红黑树的性质:红节点的孩子必须为黑节点。插入红节点,其父节点也可能为红节点,因此需要进行操作。祖父
__xpp
肯定为黑色节点。
- 2.1、如果堂叔节点存在且为红色,祖父
__xpp
染成红色,其父节点和堂叔节点设置为黑色(黑色下移),设置__x = __xpp
,接着进行循环判断情况2 - 2.2、如果堂叔节点不存在或为黑色,需要进行改变颜色、并右旋(如果为右子树,需要先左旋统一形式);(第一次插入时不会出现其堂叔存在且为黑色的情况,但通过2.1可能导致该情况出现)
- 根节点设置为黑色(可能会增加黑高)
(3)维护红黑树结点数量。
返回迭代器和是否插入成功。
1.4 删除
1.4.1 实际删除和平衡
__x
是来替换被删除节点__z
的。
- 找到删除节点
__z
的替换节点__y
(可能__y == __z
),找到删除节点__y
的替换节点__x
如果无左子树,则__x
为其右孩子(此处__x
可能为空)
如果无右子树,则__x
为其左孩子
两者都有,__y
为其右子树中最小的节点,即__z
的后继节点用来替换__z
;__x
为__y
的右子树,即__y
的替代节点。
之所以区分两种情况,是因为当__y == __z
时,即只有,可能会影响__leftmost
或__rightmost
- 如果删除的是黑色节点。