STL红黑树

1. STL红黑树

1.1 基础结构

  1. _Rb_tree_node_base中包含_M_color、三个指针_M_parent_M_left_M_right
    _Rb_tree_node继承自上述类,额外存储值。

  2. 头节点_Rb_tree_header红色_M_node_count维护节点数量,其_M_parent指向root结点。
    左孩子为红黑树最左侧节点(begin()迭代器位置);右孩子为最右侧节点(最大的节点)
    头结点本身作为end()迭代器指向位置。

  3. 实际的实现中_M_impl负责节点分配,并继承了头结点

1.2 迭代器遍历

(1)在increment函数中
其正常范围为 begin() ~ end(),对end()自增会回退到最大节点,再自增又会回到end()

  • 1.若有右子树,则其后继为 右子树中最小的节点
  • 2.若无右子树,则其后继为 祖先节点(该节点作为其左子树出现)
    • 处理特殊情况,根节点无右子树才不会执行下述情况,保证了最大节点的后继为 header节点
      	if ( __x->_M_right != __y ) 
      	      __x = __y;
      

注意:在上述函数中,若当前节点为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函数

mapset插入时都会调用_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 xy矛盾(如果是插入 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)插入与平衡过程

  1. 因为红黑树要求根到所有叶子节点路径上的黑节点数量一致。因此,若要增加黑节点数量,必须是从根结点处增加(有点类似B+树的思想,整体树高的增加是通过root结点向上分裂实现的)。
    为了尽可能红黑树的性质,新创建的结点颜色为红色。
  2. 插入一个新的红色节点可能会打破红黑树的性质:红节点的孩子必须为黑节点。插入红节点,其父节点也可能为红节点,因此需要进行操作。祖父__xpp肯定为黑色节点。
  • 2.1、如果堂叔节点存在且为红色,祖父__xpp染成红色,其父节点和堂叔节点设置为黑色(黑色下移),设置__x = __xpp,接着进行循环判断情况2
  • 2.2、如果堂叔节点不存在或为黑色,需要进行改变颜色、并右旋(如果为右子树,需要先左旋统一形式);(第一次插入时不会出现其堂叔存在且为黑色的情况,但通过2.1可能导致该情况出现)
  1. 根节点设置为黑色(可能会增加黑高)

(3)维护红黑树结点数量。

返回迭代器和是否插入成功。

1.4 删除

1.4.1 实际删除和平衡

__x是来替换被删除节点__z的。

  1. 找到删除节点__z的替换节点__y(可能__y == __z),找到删除节点__y的替换节点__x
    如果无左子树,则__x为其右孩子(此处__x可能为空)
    如果无右子树,则__x为其左孩子
    两者都有,__y为其右子树中最小的节点,即__z的后继节点用来替换__z__x__y的右子树,即__y的替代节点。

之所以区分两种情况,是因为当__y == __z时,即只有,可能会影响__leftmost__rightmost

  1. 如果删除的是黑色节点。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值