首页 > 技术文章 > STL——关联式容器

yyxt 2015-11-22 07:56 原文

一、关联式容器

标准的STL关联式容器分为set(集合)/map(映射表)两大类,以及这两大类的衍生体multiset(多键集合)和 multimap(多键映射表)。这些容器的底层机制均以RB-tree(红黑树)完成。RB-tree也是一个独立容器,但并不开放给外界使用。

此外,SGI STL 还提供了一个不在标准规格之列的关联式容器:hash table (散列表),以及以此hash table 为底层机制而完成的hash_set(散列集合)、hash_map(散列映射表)、hash_multiset(散列多键集合)、hash_multimap(散列多键映射表)。

所谓的关联式容器,观念上类似关联式数据库:每笔数据(每个元素)都有一个键值(key)和一个实值(value)。当元素被插入到关联式容器中时,容器内部结构(可能是RB-tree ,也可能是hash-table)便依照其键值大小,以某种特定规则将这个元素放置于适当位置。关联式容器没有所谓的头尾(只有最大元素和最小元素),所以不会有所谓push_back()/push_front()/pop_back()/pop_front()/begin()/end() 这样的操作行为

一般而言,关联式容器的内部结构是一个balanced binary tree(平衡二叉树),以便获得良好的搜寻效率。balanced binary tree 有许多种类型,包括AVL-tree、RB-tree、AA-tree ,其中最被广泛运用于STL的是RB-tree(红黑树)。

二、树

有关树以及红黑树的相关内容,参见 红黑树详解 博文。 红黑树及其迭代器源码请参考《STL源码剖析》5.2节。

三、set

set 的特性是,所有元素都会根据元素的键值自动被排序。set 的元素不像map那样可以同时拥有实值和键值,set元素的键值就是实值,实值就是键值。set不允许两个元素有相同的键值

务必注意,我们不能通过set的迭代器改变set的元素值。因为set元素值就是其键值,关系到set元素的排序规则。如果任意改变set元素值,会严重破坏set组织。在set的源码中,set<T>::iterator被定义为底层RB-tree 的const_iterator,杜绝写入操作。换句话说,set iterators 是一种constant iterators(相对于mutable iterators)。

STL特别提供了一组set、multiset 相关算法,包括交集set_intersection、并集set_union、差集set_difference、对称差集set_symmetric_difference。

由于RB-tree 是一种平衡二叉搜索树,自动排序的效果很不错,所以标准的STL set即以RB-tree为底层机制。又由于set 所开放的各种操作接口,RB-tree也都提供了,所以几乎所有的set操作行为,都只是转调用RB-tree 的操作行为而已。

参见相关源码;

四、map

map的特性是,所有元素都会根据元素的键值自动被排序。map的所有元素都是pair,同时拥有实值和键值。pair 的第一元素被视为键值,第二元素被视为实值。map不允许两个元素拥有相同的键值(但两个元素的实值可以相同)。同样,我们不能通过map的迭代器改变map的键值,但我们可以改变其实值。因此,map iterators 既不是一种constant iterators,也不是一种mutable iterators。map也是也是以RB-tree为底层机制,通过转调RB-tree的操作行为来实现自己的接口功能。

参见相关源码;

五、multiset

multiset 的特性以及用法和set 完全相同,唯一的差别在于它允许键值重复,因此它的插入操作采用的是底层机制RB-tree的insert_equal()而非insert_unique()——insert_equal()表示被插入节点的键值在整棵树中可以重复,因此,无论如何插入都会成功(除非空间不足导致配置失败)。insert_unique()表示被插入节点的键值key在整棵树中必须独一无二(因此,如果树中已存在相同的键值,插入操作就不会真正进行)。

参见相关源码;

六、multimap

multimap 的特性以及用法与map完全相同,唯一的差别在于它允许键值重复,因此它的插入操作采用的是底层机制RB-tree的insert_equal()而非insert_unique()。 参见相关源码;

参见附录1 SGI STL RB-tree的insert_equal()和insert_unique()插入源码

七、hashtable

参见 hashtable详解

八、hash_set

虽然STL 只规范复杂度与接口,并不规范实现方法,但STL set 多半以RB-tree 为底层机制。SGI 则是在STL 标准规格之外另又提供了一个所谓的hash_set,以hashtable 为底层机制。由于hash_set所供应的操作接口,hashtable都提供了,所以几乎所有的hash_set 操作行为,都只是转调用hashtable的操作行为而已。

运用set,为的是能够快速搜寻元素。这一点,不论其底层是RB-tree 或是hashtable,都可以达成任务。但是请注意,RB-tree 有自动排序功能而hashtable 没有,反映出来的结果就是,set的元素有自动排序功能而hash_set 没有。 hash_set 的使用方式,与set完全相同。

参见相关源码;

九、hash_map

SGI 在STL 标准规格之外,另提供了一个所谓的hash_map,以hashtable 为底层机制。由于hash_map 所供应的操作接口,hashtable 都提供了,所以几乎所有的hash_map操作行为,都只是转调用hashtable 的操作行为而已。

RB-tree 有自动排序功能而hashtable 没有,所以map 的元素有自动排序功能而hash_map没有

参见相关源码;

十、hash_multiset

hash_multiset 的特性于multiset 完全相同。唯一的差别在于它的底层机制是hashtable。也因此,hash_multiset 的元素并不会被自动排序。

hash_multiset 和 hash_set 实现上的唯一差别在于,前者的元素插入操作采用底层机制hashtable的insert_equal(),后者则是采用 insert_unique()。

十一、hash_multimap

hash_multimap 的特性于multimap 完全相同。唯一的差别在于它的底层机制是hashtable。也因此,hash_multimap 的元素并不会被自动排序。

hash_multimap 和 hash_map 实现上的唯一差别在于,前者的元素插入操作采用底层机制hashtable的insert_equal(),后者则是采用 insert_unique()。

附录1:SGI STL RB-tree的insert_equal()和insert_unique()插入源码

// 插入新值:节点键值允许重复
// 注意,返回值是一个RB-Tree迭代器,指向新增节点
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_equal(const _Value& __v)
{
  _Link_type __y = _M_header;
  _Link_type __x = _M_root();   // 从根节点开始
  while (__x != 0) {    // 从根节点开始,往下寻找适当的插入点
    __y = __x;
    // _M_key_compare(v, x) =意思是=> (x > v ? true : false)
    __x = _M_key_compare(_KeyOfValue()(__v), _S_key(__x)) ? 
            _S_left(__x) : _S_right(__x);
    // 新值遇到比它“大”的节点则往左,否则(遇到比它“小于或等于”的节点)则往右
  }
  return _M_insert(__x, __y, __v);
  // 以上,x为新值插入点,y为插入点之父节点,v为新值
}

// 插入新值:节点键值不允许重复,若重复则插入无效
// 注意,返回值是个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 = _M_header;
  _Link_type __x = _M_root();   // 从根节点开始
  bool __comp = true;
  while (__x != 0) {        // 从根节点开始,往下寻找适当的插入点
    __y = __x;
    __comp = _M_key_compare(_KeyOfValue()(__v), _S_key(__x));  
    // 如果节点值比新值大则往左;否则往右(注意,_KeyOfValue()(__v)为第1参数)
    __x = __comp ? _S_left(__x) : _S_right(__x);
  } //离开while循环之后,__y所指即插入点之父节点(此时的它必为叶节点)

  iterator __j = iterator(__y);  // 令迭代器__j指向插入点之父节点__y 
  if (__comp)   // 如果离开while循环时comp为真(表示插入节点值“大于”新值,将在左侧插入)
    if (__j == begin())     // 如果插入点之父节点为最左节点
      return pair<iterator,bool>(_M_insert(__x, __y, __v), true);
      // 以上,x为插入点,y为插入点之父节点,v为新值
    else    // 否则(插入点之父节点不为最左节点)
      --__j;    // 调整__j (重新确定插入点,见附录2图

// (1)__comp == false => 插入节点值“小于或等于”新值,右侧插入 // (2)__comp == true && 插入点之父节点不为最左节点 => 调整__j(重新确定插入点,见附录2图 if (_M_key_compare(_S_key(__j._M_node), _KeyOfValue()(__v))) // 如果新值“大于”插入节点值((注意,_KeyOfValue()(__v)为第2参数)) return pair<iterator,bool>(_M_insert(__x, __y, __v), true); // 以上,__x为新值插入点,__y为插入点之父节点,__v为新值 // 至此,表示新值一定与树中键值重复,那么就不该插入新值 return pair<iterator,bool>(__j, false); }

附录2图:

注:准备插入v=20的值,此时RB-Tree里面已经有一个值为20的节点了。

遍历步骤已经插入流程如下(如图,红色箭头表示遍历路径):

1. 40节点比新值大,往左走;

2. 20节点不比新值大(“小于或等于”),往右走;

3. 30节点比新值大,往左走;同时,达到叶子节点;此时j指向30节点;

4. 30节点不是最左节点,执行“--j”,j新指向20节点;

5. 执行后续判断(也即进入上面代码(1)、(2)情况的语句):

  a. 新值是否大于j节点,如果是,插入;

  b. 否则(本例子的情况,“小于或等于”),不插入。

注:步骤2(20节点“小于或等于”新值)和步骤5(新值“小于或等于”20节点),由这两个判断可以推断:新值“等于”20节点。进而说明了原RB-Tree中已有相同的值,故而不再插入。 

 

推荐阅读