在 STL 编程中,容器是我们经常会用到的一种数据结构,容器分为序列式容器和关联式容器。
两者的本质区别在于:序列式容器是通过元素在容器中的位置顺序存储和访问元素,而关联容器则是通过键 (key) 存储和读取元素。
本篇着重剖析关联式容器相关背后的知识点,来一张思维导图
容器分类
前面提到了,根据元素存储方式的不同,容器可分为序列式和关联式,那具体的又有哪些分类呢,这里我画了一张图来看一下。
关联式容器比序列式容器更好理解,从底层实现来分的话,可以分为 RB_tree 还是 hash_table,所有暴露给用户使用的关联式容器都绕不过底层这两种实现。
首先来介绍红黑树,RB Tree 全称是 Red-Black Tree,又称为“红黑树”,它一种特殊的二叉查找树。红黑树的每个节点上都有存储位表示节点的颜色,可以是红 (Red) 或黑 (Black)。
红黑树的特性:
红黑树保证了最坏情形下在 O(logn) 时间复杂度内完成查找、插入及删除操作;效率非常之高。
因此红黑树可用于很多场景,比如下图。
迭代器的begin指向红黑树根节点,也就是header的父亲,而end指向header节点。
图中省略号表示节点没有画完,还有其他节点,所以省略。
红黑树节点基类
红黑树基类,非常简单,在文件开头定义了颜色标记。
基类中包含了指向自己的指针,分别定义了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;
}
红黑树操作
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 成为右节点。
//不允许键值重复插入
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);
}
}
在讨论红黑树的插入操作之前必须要明白,任何一个即将插入的新结点的初始颜色都为红色。这一点很容易理解,因为插入黑点会增加某条路径上黑结点的数目,从而导致整棵树黑高度的不平衡。但如果新结点的父结点为红色时(如下图所示),将会违反红黑树的性质:一条路径上不能出现相邻的两个红色结点。这时就需要通过一系列操作来使红黑树保持平衡。
为了清楚地表示插入操作以下在结点中使用“新”字表示一个新插入的结点;使用“父”字表示新插入点的父结点;使用“叔”字表示“父”结点的兄弟结点;使用“祖”字表示“父”结点的父结点。插入操作分为以下几种情况:
2.2 黑叔
当叔父结点为黑色时,需要进行旋转,以下图示了所有的旋转可能:
Case 1:
Case 2:
Case 3:
Case 4:
可以观察到,当旋转完成后,新的旋转根全部为黑色,此时不需要再向上回溯进行平衡操作,插入操作完成。需要注意,上面四张图的“叔”、“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;
}