STL源码剖析(三)红黑树原理

红黑树的大名以及关于关联式容器用的比较少,想一探究竟,于是先跳过了序列式容器。本博客主要记载比较重要以及自己理解的部分。

红黑树

红黑树是二叉搜索树,且满足一下规则
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节点,和新增的节点的左右子节点

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值