MyTinySTL的rb_tree源码分析

mystl项目地址为:https://github.com/Alinshans/MyTinySTL

原STL库十分庞大,父子类关系十分复杂。故借这个项目来研究原STL库的代码逻辑,对代码的理解都以注释的形式写在了注释里

参考博客: http://blog.csdn.net/v_JULY_v/article/details/6105630

红黑树

疑问:红黑树插入节点的时候,有两种形式。一种是允许重复节点的插入,另外一种是不允许重复节点的插入。
判断节点是否重复是一个我没有看明白的地方。具体函数体现在get_insert_unique_pos()。

性质

性质1. 结点是红色或黑色。
性质2. 根结点是黑色。
性质3. 所有叶子都是黑色。(叶子是NIL结点)
性质4. 每个红色结点的两个子结点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色结点)
性质5. 从任一节结点到其每个叶子的所有路径都包含相同数目的黑色结点。

节点

//rb_tree_node_base中有如下数据
base_ptr   parent;  // 父节点    typedef rb_tree_node_base* base_ptr;也就是指针类型
base_ptr   left;    // 左子节点
base_ptr   right;   // 右子节点
color_type color;   // 节点颜色
//真正的节点是rb_tree_node,继承自rb_tree_node_base
struct rb_tree_node :public rb_tree_node_base
{
    ......
    T value;  // 节点值
    ......
}
//到这里,真正的节点内总共五个变量

迭代器

rb_tree_iterator_base继承自bidirectional_iterator_tag,是可向前向后的读写和输入的迭代器。

 //此函数主要是有一个变量,两个函数,用来被继承
struct rb_tree_iterator_base 
		:public mystl::iterator
{ //STL的迭代器有五种类型。  
  base_ptr node;  // 指向节点本身
  // 使迭代器前进,也就是下一个节点
  /*可以理解为:寻找节点的后继节点,我们先将整棵树进行一次中序遍历,得到一个序列。在此序列中,node的后继节点就是序列的下一个节点。可是在在原树中位置不是如此,需要分多种情况*/
  void inc()
  {
    if (node->right != nullptr){ //存在右子树,那么后继就是右子树的最左下节点 
      node = rb_tree_min(node->right);//此函数来寻找本树的最左下节点
    }
    else{  // 如果没有右子节点
      auto y = node->parent;
      while (y->right == node){//进行上溯查找node的父节点,也就是找最高层的父节点
        node = y;
        y = y->parent;
      }
      if (node->right != y)  // 应对“寻找根节点的下一节点,而根节点没有右子节点”的特殊情况
        node = y;
    }
  }
  // 使迭代器后退
  void dec(){
      //同inc逻辑
  }
}

真正的迭代器rb_tree_iterator继承自rb_tree_iterator_base

struct rb_tree_iterator :public rb_tree_iterator_base
{
	//主要内容是构造函数和重载运算符,如++(重载是调用了inc()函数)
}

tree algorithm

rb_tree_min(NodePtr x)来寻找x的最左下节点
rb_tree_max(NodePtr x)来寻找x的最右下节点
还有一些变色的函数,很简单

template 
NodePtr rb_tree_next(NodePtr node) noexcept //找前驱节点,有点像inc()函数
{
  if (node->right != nullptr) 
    return rb_tree_min(node->right);
  while (!rb_tree_is_lchild(node))//此函数判断本节点是不是某个父节点的左孩子,不知道特殊情况是什么
    node = node->parent;
  return node->parent;
}

旋转

树在经过左旋右旋之后,树的搜索性质保持不变,但树的红黑性质则被破坏了

/*---------------------------------------*\
|       p                         p       |
|      / \                       / \      |
|     x   d    rotate left      y   d     |
|    / \       ===========>    / \        |
|   a   y                     x   c       |
|      / \                   / \          |
|     b   c                 a   b         |
\*---------------------------------------*/
// 左旋,参数一为左旋点,参数二为根节点
/* 如上图,x-a和y-c是不需要变动的。
   首先将x的右孩子修改为b,然后将b的父节点修改为x,最后修改x,y的父节点。
   修改顺序可能造成成员丢失 */
template 
void rb_tree_rotate_left(NodePtr x, NodePtr& root) noexcept
{
  auto y = x->right;  // y 为 x 的右子节点
  x->right = y->left;
  if (y->left != nullptr)/*y的左孩子如果为空,x的右孩子也为空。感觉可以不写这一句,不写这个if的只是重新给空指针又赋值一次空指针*/
    y->left->parent = x;
  y->parent = x->parent;

  if (x == root){ // 如果 x 为根节点,让 y 顶替 x 成为根节点
    root = y;
  }
  else if (rb_tree_is_lchild(x)){ // 如果 x 是左子节点
    x->parent->left = y;
  }
  else{ // 如果 x 是右子节点
    x->parent->right = y;
  }
  // 调整 x 与 y 的关系
  y->left = x;  
  x->parent = y;
}

右旋同理。

插入节点

插入节点共有五种情况:
1:根节点
2:父黑
3:父红叔红
4:父红叔黑NULL,父旋
5:父红叔黑NULL,祖父旋

在旋转的过程中,处理3后,情况变为4,处理4后情况变为5,处理5后,达到平衡。

具体解释:

// 插入节点后使 rb tree 重新平衡,参数一为新增节点,参数二为根节点
//
// case 1: 新增节点位于根节点,令新增节点为黑
// case 2: 新增节点的父节点为黑,没有破坏平衡,直接返回
// case 3: 父节点和叔叔;节点都为红,令父节点和叔叔节点为黑,祖父节点为红,
//         然后令祖父节点为当前节点,继续处理
// case 4: 父节点为红,叔叔节点为 NIL 或黑色,父节点为左(右)孩子,当前节点为右(左)孩子,
//         让父节点成为当前节点,再以当前节点为支点左(右)旋
// case 5: 父节点为红,叔叔节点为 NIL 或黑色,父节点为左(右)孩子,当前节点为左(右)孩子,
//         让父节点变为黑色,祖父节点变为红色,以祖父节点为支点右(左)旋
// https://blog.csdn.net/v_JULY_v/article/details/6105630 参考地址
template 
void rb_tree_insert_rebalance(NodePtr x, NodePtr& root) noexcept
{
  rb_tree_set_red(x);  // 新增节点规定为红色
  while (x != root && rb_tree_is_red(x->parent)) 
  {
    if (rb_tree_is_lchild(x->parent)){ // 如果父节点是左子节点
      auto uncle = x->parent->parent->right; //叔叔节点
      if (uncle != nullptr && rb_tree_is_red(uncle)) { // case 3: 父节点和叔叔节点都为红
        rb_tree_set_black(x->parent);
        rb_tree_set_black(uncle);//父节点和叔节点都变为黑色
        x = x->parent->parent;
        rb_tree_set_red(x);//祖父节点为红色。继续处理体现在:x已经变为祖父节点且继续执行while上
      }
      else{ // 无叔叔节点或叔叔节点为黑
        if (!rb_tree_is_lchild(x)){ // case 4: 当前节点 x 为右子节点
          x = x->parent;
          rb_tree_rotate_left(x, root); //以x的父节点为支点左旋
        }
        // 都转换成 case 5: 当前节点为左子节点
        rb_tree_set_black(x->parent);
        rb_tree_set_red(x->parent->parent);
        rb_tree_rotate_right(x->parent->parent, root);
        break;
      }
    }
    else  {// 如果父节点是右子节点,对称处理 
      auto uncle = x->parent->parent->left;
      if (uncle != nullptr && rb_tree_is_red(uncle)) { // case 3: 父节点和叔叔节点都为红
        rb_tree_set_black(x->parent);
        rb_tree_set_black(uncle);
        x = x->parent->parent;
        rb_tree_set_red(x);
        // 此时祖父节点为红,可能会破坏红黑树的性质,令当前节点为祖父节点,继续处理
      }
      else { // 无叔叔节点或叔叔节点为黑
        if (rb_tree_is_lchild(x)) { // case 4: 当前节点 x 为左子节点
          x = x->parent;
          rb_tree_rotate_right(x, root);
        }
        // 都转换成 case 5: 当前节点为左子节点
        rb_tree_set_black(x->parent);
        rb_tree_set_red(x->parent->parent);
        rb_tree_rotate_left(x->parent->parent, root);
        break;//父节点变为黑色,祖父节点变为红色,以祖父节点为支点右旋
      }
    }
  }
  rb_tree_set_black(root);  // 根节点永远为黑
}

删除节点

删除节点分两个步骤进行:一是直接删除,二是调整红黑树(因为删除节点后,原红黑树可能发生改变)
删除z:
1,z是叶节点:直接删除
2,z只有一个儿子:那么将z的儿子取代z的位置
3,z左右孩子都有:左孩子的最右下节点(前驱)替代z,或者右孩子的最左下(后继)节点替代z。

调整:
1,如果删除的是红色节点,那么原红黑树的性质依旧保持,此时不用做修正操作。删红不变
2,如果删除的节点是黑色节点,原红黑树的性质可能会被改变,我们要对其做修正操作。 如果只是根节点,什么都不用做。
2.1,如果删除节点不是树唯一节点,那么删除节点的那一个支的到各叶节点的黑色节点数会发生变化,此时性质5被破坏。
2.2,如果被删节点的唯一非空子节点是红色,而被删节点的父节点也是红色,那么性质4被破坏。
2.3,如果被删节点是根节点,而它的唯一非空子节点是红色,则删除后新根节点将变成红色,违背性质2。

我们从被删节点后来顶替它的那个节点开始调整(即代码中的x节点)

此区域信息收集于博客,有较大修改
case 1:兄红。当前节点是黑且兄弟节点为红色。
解法:把父节点染成红色,把兄弟结点染成黑色,之后重新进入算法。
此变换后原红黑树性质5不变,而把问题转化为兄弟节点为黑色的情况
case 2:兄黑兄两儿黑。当前节点是黑且兄弟是黑色且兄弟节点的两个子节点全为黑色。
解法:令兄弟节点为红,父节点成为当前节点,重新进入算法。
case 3:兄黑兄左子红右子黑。
解法:把兄弟结点染红,兄弟左子节点染黑,之后再在兄弟节点为支点解右旋,之后重新进入算法。
case 4:兄黑兄左子任意,右子红。
解法:把兄弟节点染成当前节点父节点的颜色,把当前节点父节点染成黑色,兄弟节点右子染成黑色,之后以当前节点的父节点为支点进行左旋,此时算法结束,红黑树所有性质调整正确。
// 删除节点后使 rb tree重新平衡,参数一为要删除的节点,参数二为根节点,参数三为最小节点,参数四为最大节点

template 
NodePtr rb_tree_erase_rebalance(NodePtr z, NodePtr& root, NodePtr& leftmost, 
                                NodePtr&rightmost)
{
  // y 是可能的替换节点,指向最终要删除的节点
  auto y = (z->left == nullptr || z->right == nullptr) ? z : rb_tree_next(z);
  // x 是 y 的一个独子节点或 NIL 节点
  auto x = y->left != nullptr ? y->left : y->right;
  // xp 为 x 的父节点
  NodePtr xp = nullptr;
  ......//这里主要是删除节点z
      
  // 此时,y 指向要删除的节点,x 为替代节点,从 x 节点开始调整。
  // 如果删除的节点为红色,树的性质没有被破坏,否则按照以下情况调整(x 为左子节点为例):
  // case 1: 兄弟节点为红色,令父节点为红,兄弟节点为黑,以父亲作为支点进行左(右)旋,继续处理
  // case 2: 兄弟节点为黑色,且两个子节点都为黑色或 NIL,令兄弟节点为红,父节点成为当前节点,继续处理
  // case 3: 兄弟节点为黑色,左子节点为红色或 NIL,右子节点为黑色或 NIL,
  //         令兄弟节点为红,兄弟节点的左子节点为黑,以兄弟节点为支点右(左)旋,继续处理
  // case 4: 兄弟节点为黑色,右子节点为红色,令兄弟节点为父节点的颜色,父节点为黑色,兄弟节点的右子节点
  //         为黑色,以父节点为支点左(右)旋,树的性质调整完成,算法结束
  if (!rb_tree_is_red(y))
  { // x 为黑色时,调整,否则直接将 x 变为黑色即可
    while (x != root && (x == nullptr || !rb_tree_is_red(x)))
    {
      if (x == xp->left)
      { // 如果 x 为左子节点
        auto brother = xp->right;
        if (rb_tree_is_red(brother))
        { // case 1
          rb_tree_set_black(brother);
          rb_tree_set_red(xp);
          rb_tree_rotate_left(xp, root);
          brother = xp->right;
        }
        // case 1 转为为了 case 2、3、4 中的一种
        if ((brother->left == nullptr || !rb_tree_is_red(brother->left)) &&
            (brother->right == nullptr || !rb_tree_is_red(brother->right)))
        { // case 2
          rb_tree_set_red(brother);
          x = xp;
          xp = xp->parent;
        }
        else
        { 
          if (brother->right == nullptr || !rb_tree_is_red(brother->right))
          { // case 3
            if (brother->left != nullptr)
              rb_tree_set_black(brother->left);
            rb_tree_set_red(brother);
            rb_tree_rotate_right(brother, root);
            brother = xp->right;
          }
          // 转为 case 4
          brother->color = xp->color;
          rb_tree_set_black(xp);
          if (brother->right != nullptr)  
            rb_tree_set_black(brother->right);
          rb_tree_rotate_left(xp, root);
          break;
        }
      }
      else  {// x 为右子节点,对称处理
        ......
      }
    }
    if (x != nullptr)
      rb_tree_set_black(x);
  }
  return y;
}

rb_tree

在类rb_tree中,有三个数据

private:
  // 用以下三个数据表现 rb tree
  base_ptr    header_;      // 特殊节点,与根节点互为对方的父节点
  size_type   node_count_;  // 节点数,在empty()和size()起到重要作用
  key_compare key_comp_;    // 节点键值比较的准则
  // 以下三个函数用于取得根节点,最小节点和最大节点
  base_ptr& root()      const { return header_->parent; }
  base_ptr& leftmost()  const { return header_->left; }
  base_ptr& rightmost() const { return header_->right; }
//这里的header_的左孩子是最小节点,右孩子是最大节点,父节点指向root

析构函数:

 ~rb_tree() { clear(); } //调用clear(),clear()中调用erase_since()递归的删除每个节点

查找函数:

// 查找键值为 k 的节点,返回指向它的迭代器
find(const key_type& key)
{
  // key 小于等于 x 键值,向左走
  // key 大于 x 键值,向右走
}

插入函数:

插入的函数有emplace,insert相关的函数,insert又是调用的emplace,所以说,实际上是emplace。
插入有两种类型的函数,分别是emplace_unique(不允许键值重复),emplace_multi(允许键值重复)

// 就地插入元素,键值允许重复
//get_insert_multi_pos()返回值是个pair:第一个元素是rb-tree迭代器指向新增结点;第二个表示成功与否
emplace_multi(Args&& ...args)
{
  THROW_LENGTH_ERROR_IF(node_count_ > max_size() - 1, "rb_tree's size too big");
  node_ptr np = create_node(mystl::forward(args)...); //创建一个节点
  auto res = get_insert_multi_pos(value_traits::get_key(np->value));//返回应该插入的位置
  return insert_node_at(res.first, np, res.second); //插入节点
}
// 在 x 节点处插入新的节点
// x 为插入点的父节点, node 为要插入的节点,add_to_left 表示是否在左边插入
insert_node_at(base_ptr x, node_ptr node, bool add_to_left) //1560
{
  ...... // 加入节点,维护header_等的三个数据
  rb_tree_insert_rebalance(base_node, root());  //调整结构
  ++node_count_;
  return iterator(node);
}
//不允许键值重复
emplace_unique(Args&& ...args)
{
  ......
  auto res = get_insert_unique_pos(value_traits::get_key(np->value));
    /*不允许键值的重复,主要是靠此函数来实现的*/
  ......
}
// get_insert_unique_pos 函数
rb_tree::get_insert_unique_pos(const key_type& key)
{ /** 返回一个pair,嵌套结构
  /*  第一个值为一个 pair,包含插入点的父节点和一个 bool 表示是否在左边插入,
  /*  第二个值为一个 bool,表示是否插入成功
  */
  ......
  //x是根节点,y是head_,add_to_left = true
  while (x != nullptr) {  //此循环寻找插入节点的位置
	y = x;
    add_to_left = key_comp_(key, value_traits::get_key(x->get_node_ptr()->value));
    x = add_to_left ? x->left : x->right;
  }
  iterator j = iterator(y);  // 此时 y 为插入点的父节点
  if (add_to_left){ //在左边添加
    if (y == header_ || j == begin()){ // 如果树为空树或插入点在最左节点处,肯定可以插入新的节点
      return mystl::make_pair(mystl::make_pair(y, true), true);
    }
    else{ // 否则,如果存在重复节点,那么 --j 就是重复的值
/* 没看懂,大概意思是说:迭代器已经重载了--,那么--j就是j的直接前驱。如果存在一样的数值,那么这个数值就是直接前驱。具体为什么不知道  */
      --j;
    }
  }
  if (key_comp_(value_traits::get_key(*j), key))  { // 表明新节点没有重复
    return mystl::make_pair(mystl::make_pair(y, add_to_left), true);
  }
  // 进行至此,表示新节点与现有节点键值重复
  return mystl::make_pair(mystl::make_pair(y, add_to_left), false);
}

另外,
emplace_multi_use_hint()就地插入元素,键值允许重复,当 hint 位置与插入位置接近时,插入操作的时间复杂度可以降低;
emplace_unique_use_hint()就地插入元素,键值不允许重复,当 hint 位置与插入位置接近时,插入操作的时间复杂度可以降低

键值:

底层使用pair

本文只是自己的理解,仅供学习。

如有错误,请指出。

你可能感兴趣的:(c++,算法,排序算法)