STL源码剖析之五:关联式容器
关联式容器
所谓关联式容器,观念上类似于关联式数据库:每笔数据都有一个键值(key)和一个实际值(value)。当元素被插入容器时,内部机制根据键值,按着一定的规则将元素置于特定的位置。关联式容器没有所谓头尾的概念(只有最大元素,最小元素),所以不会有类似push_back(),push_front()这样的操作。
标准的stl关联式容器分为set和map两大类,以及这两大类的衍生体multiset和multimap,这些容器的底层机制均以RB-tree来实现,RB-tree也是一个独立容器,并不开放给外界使用。此外,SGI STL还提供了一个不在标准之内的关联式容器hash-table,以及以之为底层机制而完成的hash-set,hash-map,hash-multiset,hash-multimap。
二叉搜索树
一般而言,关联式容器的内部结构是二叉搜索树(binary balanced tree),BBT有很多种类,包括RB-Tree,AVL-Tree,AA-Tree,其中最被广泛运用的是RB-tree。
二叉搜索树的的规则是:任何结点的键值都大于其左子树中每个结点的键值,而小于其右子树中每一个结点的键值。二叉搜索树支持的操作时间复杂度与树的高度成正比,如果树是平衡的(极端是完全二叉树),其操作效率就高,不平衡(极端是单链),其操作效率就低。为了保证操作的效率,因此有所谓平衡二叉树的概念。
平衡二叉树是施加了某些条件来保证平衡性的二叉搜索树,关于AVL树和RB-TREE的定义和如何保证平衡性,可以参考《算法导论》或其他介绍数据结构的书籍。
AVLtree的平衡性要求比RB-Tree要严格,至于STL为何选择RB-Tree作为关联式容器的底层机制,而不是AVL-Tree,我认为应该是由于保证平衡性本身要增加一定的算法复杂度,要求越严格,操作过程中对树的调整就越频繁。RB-tree在两个指标之间达到了比较好的折中,实际效果要好于AVL-Tree。
Set和MutiSet
set是集合,它的元素的键值就是实值,实值就是键值,不允许两个元素有相同的值。
我们不可以通过set的iterator来改变元素的值,因为set的元素值就是键值,改变键值会违反元素排列的规则。
在客户端对set进行插入或删除操作后,之前的迭代器依然有效(当然被删除那个元素的迭代器除外)。
STL还提供了一些集合算法包括交际、并集等。
set底层机制是RB-tree,所有的操作都只是转掉RB-tree的操作行为而已。
MultiSet和set几乎一样,唯一的区别是,multiset允许键值重复。
Map和MultiMap
Map的元素都是pair,第一个值是键值,第二个是实值。
我们可以通过map的迭代器来改变元素的实值。
MultiMap和Map几乎一样,唯一的区别是允许键值重复。
HashTable
Hash table可以提供对任意有名项的存取和删除操作,这种结构的用意在于提供常数时间的的基本操作。
stl hash table采用的hash方式是开链法。
hash table的存储结构分成两级,第一级是连续空间的桶子(buckets),每个桶子里面是一个结点链表。桶子列表是用stl vector来实现的,而链表结构和list或slist没有关系。
hash table的迭代器没有定义后退操作。
stl hash table的模板参数非常多:
Value:实值类型;
Key:键值类型;
HashFun:将键值转化为hash值的函数(注意这个不是通常所说的映射地址的哈希函数)。
ExtractKey:从结点中取出键值的方法
EqualKey:判断键值是否相等的函数
Alloc:空间配置器,默认std::alloc
虽然开链法并不要求表格大小为质数,但stl任然使用质数。并先将28个质数准备好,以备随时访问。当需要初始化或扩张表格时至n时,就设定表格实际大小为最接近且不小于n的质数。
stl hash table扩张表格的触发条件是:当元素的数目大于或等于表格的大小。(这个条件应该是为了保证常数操作时间,在统计基础上得出的)。
Hash_Map,Hash_Set,Hash_MultiSet,Hash_MultiMap
这些容器和前面介绍的一一对应,只不过这些是以hash_tabel为底层实现机制的。
底层机制决定了这两组容器的区别:
RB-tree组对元素实现排序,而hash_map组没有;
RB-tree的查找时间复杂度为lg(n),而hash_map组为常数时间;
RB-tree组在空间利用上,不会浪费结点,而hash_map组可能会有一些空置桶。