C++ rb_tree红黑树

在 STL 编程中,容器是我们经常会用到的一种数据结构,容器分为序列式容器和关联式容器。

两者的本质区别在于:序列式容器是通过元素在容器中的位置顺序存储和访问元素,而关联容器则是通过键 (key) 存储和读取元素。

本篇着重剖析关联式容器相关背后的知识点,来一张思维导图
C++ rb_tree红黑树_第1张图片
容器分类
前面提到了,根据元素存储方式的不同,容器可分为序列式和关联式,那具体的又有哪些分类呢,这里我画了一张图来看一下。
C++ rb_tree红黑树_第2张图片
关联式容器比序列式容器更好理解,从底层实现来分的话,可以分为 RB_tree 还是 hash_table,所有暴露给用户使用的关联式容器都绕不过底层这两种实现。

RB-tree 介绍与应用

首先来介绍红黑树,RB Tree 全称是 Red-Black Tree,又称为“红黑树”,它一种特殊的二叉查找树。红黑树的每个节点上都有存储位表示节点的颜色,可以是红 (Red) 或黑 (Black)。

红黑树的特性:

  1. 每个节点或者是黑色,或者是红色。
  2. 根节点是黑色。
  3. 每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
  4. 如果一个节点是红色的,则它的子节点必须是黑色的。
  5. 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

C++ rb_tree红黑树_第3张图片
红黑树保证了最坏情形下在 O(logn) 时间复杂度内完成查找、插入及删除操作;效率非常之高。

因此红黑树可用于很多场景,比如下图。

C++ rb_tree红黑树_第4张图片

stl中的rb_tree类

迭代器的begin指向红黑树根节点,也就是header的父亲,而end指向header节点。
C++ rb_tree红黑树_第5张图片
图中省略号表示节点没有画完,还有其他节点,所以省略。

红黑树节点基类

红黑树基类,非常简单,在文件开头定义了颜色标记。

基类中包含了指向自己的指针,分别定义了left、right、parent,同时包含了一个颜色标记常量,而里面有两个核心函数,目的是获取红黑树中最小节点与最大节点。 我们知道对于二分搜索树获取最小节点就是左子树一直往下搜,最大节点就是右子树一直往下搜即可。

//标记红黑树的颜色
typedef bool __rb_tree_color_type;
//红色
const __rb_tree_color_type __rb_tree_red = false;
//黑色
const __rb_tree_color_type __rb_tree_black = true;

//节点基类
struct __rb_tree_node_base
{
  typedef __rb_tree_color_type color_type;
  typedef __rb_tree_node_base* base_ptr;
  //颜色
  color_type color; 
  //父节点指针
  base_ptr parent;
  //左子节点指针
  base_ptr left;
  //右子节点指针
  base_ptr right;

  //遍历红黑色的最小值,一直遍历左子节点
  static base_ptr minimum(base_ptr x)
  {
    while (x->left != 0) x = x->left;
    return x;
  }
  //遍历红黑色的最大值,一直遍历右子节点
  static base_ptr maximum(base_ptr x)
  {
    while (x->right != 0) x = x->right;
    return x;
  }
};

红黑树节点

红黑树节点继承自红黑树基类。

//红黑树节点
template <class Value>
struct __rb_tree_node : public __rb_tree_node_base
{
  typedef __rb_tree_node<Value>* link_type;
  //存放节点的值
  Value value_field;
};

红黑树迭代器

红黑树迭代器里面有一个红黑树基类成员,然后通过该成员进行迭代器的相关操作。 同时,我们可以知道该迭代器属于bidirectional_iterator_tag。

里面也包含了萃取机相关需要的typedef。

//红黑树迭代器基类
struct __rb_tree_base_iterator
{
  //萃取机
  typedef __rb_tree_node_base::base_ptr base_ptr;
  typedef bidirectional_iterator_tag iterator_category;
  typedef ptrdiff_t difference_type;

  //节点指针
  base_ptr node;

  //将node指向下一个节点(按照排序规则)
  void increment()
  {
    if (node->right != 0) {//存在右子树,那么下一个节点为右子树的最小节点
      node = node->right;
      while (node->left != 0) 
        node = node->left;
    }
    else { 
      /* 不存在右子树,那么分为两种情况:自底往上搜索,当前节点为父节点的左孩子的时候,父节点就是后继节点; */
      /* 第二种情况:node为header节点了,那么node就是最后的后继节点. 简言之node为最小节点且往上回溯,一直为父节点的右孩子,直到node变为父节点,y为其右孩子 */
      base_ptr y = node->parent;
      while (node == y->right) {
        node = y;
        y = y->parent;
      }
      if (node->right != y)
        node = y;
    }
  }

  //将node指向上一个节点(按照排序规则)
  void decrement()
  {
    if (node->color == __rb_tree_red &&
        node->parent->parent == node)//如果是end()节点
      node = node->right;
    else if (node->left != 0) { /* 左节点不为空,返回左子树中最大的节点 */
      base_ptr y = node->left;
      while (y->right != 0)
        y = y->right;
      node = y;
    }
    else {
      base_ptr y = node->parent; /* 自底向上找到当前节点为其父节点的右孩子,那么父节点就是前驱节点 */
      while (node == y->left) {
        node = y;
        y = y->parent;
      }
      node = y;
    }
  }
};

获取数据

template <class Value, class Ref, class Ptr>
struct __rb_tree_iterator : public __rb_tree_base_iterator
{
  //萃取机
  typedef Value value_type;
  typedef Ref reference;
  typedef Ptr pointer;
  typedef __rb_tree_iterator<Value, Value&, Value*>             iterator;
  typedef __rb_tree_iterator<Value, const Value&, const Value*> const_iterator;
  typedef __rb_tree_iterator<Value, Ref, Ptr>                   self;
  typedef __rb_tree_node<Value>* link_type;

  //构造函数
  __rb_tree_iterator() {}
  __rb_tree_iterator(link_type x) { node = x; }
  __rb_tree_iterator(const iterator& it) { node = it.node; }

  //获取节点里面的数据
  reference operator*() const { return link_type(node)->value_field; }
}

重载++操作符

template <class Value, class Ref, class Ptr>
struct __rb_tree_iterator : public __rb_tree_base_iterator
{
  self& operator++() { increment(); return *this; }
  
  self operator++(int) {
    self tmp = *this;
    increment();
    return tmp;
  }
}

调用了基类的increment

  //将node指向下一个节点(按照排序规则)
  void increment()
  {
    if (node->right != 0) {//存在右子树,那么下一个节点为右子树的最小节点
      node = node->right;
      while (node->left != 0) 
        node = node->left;
    }
    else { 
      /* 不存在右子树,那么分为两种情况:自底往上搜索,当前节点为父节点的左孩子的时候,父节点就是后继节点; */
      /* 第二种情况:node为header节点了,那么node就是最后的后继节点. 简言之node为最小节点且往上回溯,一直为父节点的右孩子,直到node变为父节点,y为其右孩子 */
      base_ptr y = node->parent;
      while (node == y->right) {
        node = y;
        y = y->parent;
      }
      if (node->right != y)
        node = y;
    }
  }

重载–操作符:

  self& operator--() { decrement(); return *this; }
  self operator--(int) {
    self tmp = *this;
    decrement();
    return tmp;
  }

基类decrement的实现

  //将node指向上一个节点(按照排序规则)
  void decrement()
  {
    if (node->color == __rb_tree_red &&
        node->parent->parent == node)//如果是end()节点
      node = node->right;
    else if (node->left != 0) { /* 左节点不为空,返回左子树中最大的节点 */
      base_ptr y = node->left;
      while (y->right != 0)
        y = y->right;
      node = y;
    }
    else {
      base_ptr y = node->parent; /* 自底向上找到当前节点为其父节点的右孩子,那么父节点就是前驱节点 */
      while (node == y->left) {
        node = y;
        y = y->parent;
      }
      node = y;
    }
  }

重载==与!=操作符,直接判断节点指针是否相等

inline bool operator==(const __rb_tree_base_iterator& x,
                       const __rb_tree_base_iterator& y) {
  //直接判断节点指针是否相等
  return x.node == y.node;
}

inline bool operator!=(const __rb_tree_base_iterator& x,
                       const __rb_tree_base_iterator& y) {
  //直接判断节点指针是否不相等
  return x.node != y.node;
}

红黑树操作

回到一开始的图
C++ rb_tree红黑树_第6张图片
红黑树类的成员变量

template <class Key, class Value, class KeyOfValue, class Compare,
          class Alloc = alloc>
class rb_tree {
protected:
  //定义自用类型
  typedef void* void_pointer;
  typedef __rb_tree_node_base* base_ptr;
  typedef __rb_tree_node<Value> rb_tree_node;
  typedef simple_alloc<rb_tree_node, Alloc> rb_tree_node_allocator;
  typedef __rb_tree_color_type color_type;
public:

  typedef Key key_type;
  typedef Value value_type;
  typedef value_type* pointer;
  typedef const value_type* const_pointer;
  typedef value_type& reference;
  typedef const value_type& const_reference;
  typedef rb_tree_node* link_type;
  typedef size_t size_type;
  typedef ptrdiff_t difference_type;
protected:
  //节点个数
  size_type node_count; // keeps track of size of tree
  //end节点
  link_type header;  
  //比较函数
  Compare key_compare;

  //获取红黑树的根节点
  link_type& root() const { return (link_type&) header->parent; }
  //heaer的左节点保存的是红黑树的最小值
  link_type& leftmost() const { return (link_type&) header->left; }
  //heaer的右节点保存的是红黑树的最大值
  link_type& rightmost() const { return (link_type&) header->right; }
  
  //获取最小节点
  iterator begin() { return leftmost(); }
  const_iterator begin() const { return leftmost(); }
  //获取最大节点
  iterator end() { return header; }
  const_iterator end() const { return header; }
}

红黑树插入

红黑树的基本操作包括 添加、删除。

在对红黑树进行添加或删除之后,都会用到旋转方法。原因在于添加或删除红黑树中的节点之后,红黑树就发生了变化,可能不满足红黑树的 5 条性质,也就说不再是一颗红黑树了,而是一颗普通的树。

而通过旋转,可以使这颗树重新成为红黑树。简单点说,旋转的目的是让树保持红黑树的特性。

在红黑树里的旋转包括两种:左旋和右旋。

左旋: 对节点 X 进行左旋,也就说让节点 X 成为左节点。

右旋: 对节点 X 进行右旋,也就说让节点 X 成为右节点。

C++ rb_tree红黑树_第7张图片
C++ rb_tree红黑树_第8张图片
说完了旋转,我们再来看一下它的插入,有两种插入方式:

//不允许键值重复插入
pair<iterator, bool> insert_unique(const value_type& x);

//允许键值重复插入
iterator insert_equal(const value_type& x);

RB-tree 里面分两种插入方式,一种是允许键值重复插入,一种不允许。可以简单的理解,如果调用 insert_unique 插入重复的元素,在 RB-tree 里面其实是无效的。

其实在 RB-tree 源码里面,上面两个函数走到最底层,调用的是同一个 __insert() 函数。

知道了数据的操作方式,我们再来看 RB-tree 的构造方式:内部调用 rb_tree_node_allocator ,每次恰恰配置一个节点,会调用 simple_alloc 空间配置器来配置节点。

并且分别调用四个节点函数来进行初始化和构造化。

template <class Key, class Value, class KeyOfValue, class Compare,
          class Alloc = alloc>
class rb_tree {
  //申请一个节点空间
  link_type get_node() { return rb_tree_node_allocator::allocate(); }

  //释放一个节点空间
  void put_node(link_type p) { rb_tree_node_allocator::deallocate(p); }

  //创建一个节点
  link_type create_node(const value_type& x) {
    //申请空间
    link_type tmp = get_node();
    __STL_TRY {
      //构造值
      construct(&tmp->value_field, x);
    }
    __STL_UNWIND(put_node(tmp));//异常处理
    return tmp;
  }

  //克隆节点
  link_type clone_node(link_type x) {
    link_type tmp = create_node(x->value_field);
    tmp->color = x->color;
    tmp->left = 0;
    tmp->right = 0;
    return tmp;
  }

  //析构节点并释放内存
  void destroy_node(link_type p) {
    destroy(&p->value_field);
    put_node(p);
  }
}

在讨论红黑树的插入操作之前必须要明白,任何一个即将插入的新结点的初始颜色都为红色。这一点很容易理解,因为插入黑点会增加某条路径上黑结点的数目,从而导致整棵树黑高度的不平衡。但如果新结点的父结点为红色时(如下图所示),将会违反红黑树的性质:一条路径上不能出现相邻的两个红色结点。这时就需要通过一系列操作来使红黑树保持平衡。
C++ rb_tree红黑树_第9张图片
为了清楚地表示插入操作以下在结点中使用“新”字表示一个新插入的结点;使用“父”字表示新插入点的父结点;使用“叔”字表示“父”结点的兄弟结点;使用“祖”字表示“父”结点的父结点。插入操作分为以下几种情况:

  1. 黑父
    如下图所示,如果新节点的父结点为黑色结点,那么插入一个红点将不会影响红黑树的平衡,此时插入操作完成。红黑树比AVL树优秀的地方之一在于黑父的情况比较常见,从而使红黑树需要旋转的几率相对AVL树来说会少一些。
    在这里插入图片描述
  2. 红父
    如果新节点的父结点为红色,这时就需要进行一系列操作以保证整棵树红黑性质。如下图所示,由于父结点为红色,此时可以判定,祖父结点必定为黑色。这时需要根据叔父结点的颜色来决定做什么样的操作。青色结点表示颜色未知。由于有可能需要根结点到新点的路径上进行多次旋转操作,而每次进行不平衡判断的起始点(我们可将其视为新点)都不一样。所以我们在此使用一个蓝色箭头指向这个起始点,并称之为判定点。
    C++ rb_tree红黑树_第10张图片
    2.1 红叔
    当叔父结点为红色时,如下图所示,无需进行旋转操作,只要将父和叔结点变为黑色,将祖父结点变为红色即可。但由于祖父结点的父结点有可能为红色,从而违反红黑树性质。此时必须将祖父结点作为新的判定点继续向上(迭代)进行平衡操作。
    C++ rb_tree红黑树_第11张图片
    需要注意的是,无论“父节点”在“叔节点”的左边还是右边,无论“新节点”是“父节点”的左孩子还是右孩子,它们的操作都是完全一样的(其实这种情况包括4种,只需调整颜色,不需要旋转树形)。

2.2 黑叔
当叔父结点为黑色时,需要进行旋转,以下图示了所有的旋转可能:
Case 1:
C++ rb_tree红黑树_第12张图片
Case 2:
C++ rb_tree红黑树_第13张图片
Case 3:
C++ rb_tree红黑树_第14张图片
Case 4:
C++ rb_tree红黑树_第15张图片
可以观察到,当旋转完成后,新的旋转根全部为黑色,此时不需要再向上回溯进行平衡操作,插入操作完成。需要注意,上面四张图的“叔”、“1”、“2”、“3”结点有可能为黑哨兵结点。

其实红黑树的插入操作不是很难,甚至比AVL树的插入操作还更简单些。红黑树的插入操作源代码如下:

// 元素插入操作  insert_unique()
// 插入新值:节点键值不允许重复,若重复则插入无效
// 注意,返回值是个pair,第一个元素是个红黑树迭代器,指向新增节点
// 第二个元素表示插入成功与否
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)
{
    rb_tree_node* y = header;    // 根节点root的父节点
    rb_tree_node* 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 , z) , true);
 else // 否则(插入点之父节点不为最左节点)
            --j;   // 调整j,回头准备测试
    }
 if(key_compare(key(j.node) , KeyOfValue()(v) ))
 // 新键值不与既有节点之键值重复,于是以下执行安插操作
 return pair<iterator , bool>(_insert(x , y , z) , true);
 // 以上,x为新值插入点,y为插入点之父节点,v为新值
 
 // 进行至此,表示新值一定与树中键值重复,那么就不应该插入新值
 return pair<iterator , bool>(j , false);
}
 
// 真正地插入执行程序 _insert()
template<class Key , class Value , class KeyOfValue , class Compare , class Alloc>
typename<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) ))
    {
        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()设定(并调整)
 _rb_tree_rebalance(z , header->parent);      // 参数一为新增节点,参数二为根节点root
    ++node_count;       // 节点数累加
 return iterator(z);  // 返回一个迭代器,指向新增节点
}

// 全局函数

// 重新令树形平衡(改变颜色及旋转树形)

// 参数一为新增节点,参数二为根节点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;
}

你可能感兴趣的:(C++)