STL源码剖析——关联式容器

来源:互联网 发布:linux boot 大小 编辑:程序博客网 时间:2024/05/21 01:32

        所谓关联式容器,观念上类似于关联式数据库:每笔数据都有一个键值(key)和一个实际值(value)。当元素被插入容器时,内部机制根据键值,按着一定的规则将元素置于特定的位置。关联式容器没有所谓头尾的概念(只有最大元素,最小元素),所以不会有类似push_back(),push_front()这样的操作。

       标准的stl关联式容器分为setmap两大类,以及这两大类的衍生体multisetmultimap,这些容器的底层机制均以RB-tree来实现,RB-tree也是一个独立容器,并不开放给外界使用。此外,SGI STL还提供了一个不在标准之内的关联式容器hash-table,以及以之为底层机制而完成的hash-set,hash-map,hash-multiset,hash-multimap

       二叉搜索树(binary search tree),可提供对数时间的元素插入和访问。其节点放置规则是:任何节点的键值一定大于其左子树的每一个节点的键值,并小于其右子树中的每一个节点的键值。因为输入值可能不够随机,或者经过某些插入或删除操作,二叉搜索树会失去平衡,造成搜寻效率较低。AVL-treeRB-treeAA-tree均可实现出平衡二叉搜索树,其中最被广泛运用于STL的是RB-tree(红黑树)

       AVL-tree的平衡条件:任何节点的左右子树高度相差最多1。这是一个较弱的条件,但仍能保证“对数深度”平衡状态。

       RB-tree的平衡条件:1.每个节点不是红色就是黑色;2.根节点为黑色;3.如果节点为红,其子节点必须为黑;4.任一节点到NULL(树尾端)的任何路径,所含黑节点数必须相同。

       AVLtree的平衡性要求比RB-Tree要严格,应该是由于保证平衡性本身要增加一定的算法复杂度,要求越严格,操作过程中对树的调整就越频繁。RB-tree在两个指标之间达到了比较好的折中,实际效果要好于AVL-Tree。它的旋转规则是:

       1 单旋转:将违反AVL tree规则的子树的插入元素边的第一个节点提起来,变成跟节点。再调整。

         2 双旋转:从下往上,单旋转两次。

RB-TREE红黑树

1每个节点不是红色就是黑色

2根节点为黑色

3叶子节点是黑色。

4如果节点为红色,其子节点必须为黑色

5任一节点至NULL(树尾端)的任何路径,所含黑节点数必须相同

       由规则5 => 新增节点必须为红色。规则4 => 新增节点父节点为黑色。如果插入不满足上述要求,就需要调整RB-tree

具体插入操作看书即可。

       这些约束强制了红黑树的关键性质: 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长(注意到性质4导致了路径不能有两个毗连的红色节点就足够了。最短的可能路径都是黑色节点,最长的可能路径有交替的红色和黑色节点。)。结果是这个树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的。

       RB-tree的迭代器属于迭代器,但不具备随机定位能力。它的前进操作调用了基层迭代器的increment(),后退操作调用了基层迭代器的decrement()

SeMutiSet

set是集合,它的元素的键值就是实值,实值就是键值,不允许两个元素有相同的值。不可以通过setiterator来改变元素的值,因为set的元素值就是键值,改变键值会违反元素排列的规则。在客户端对set进行插入或删除操作后,之前的迭代器依然有效。STL还提供了一些集合算法包括交际、并集等。MultiSetset几乎一样,唯一的区别是,multiset允许键值重复。它的底层机制是RB-tree

MapMultiMap

Map的元素都是pair,第一个值是键值,第二个是实值。可以通过map的迭代器来改变元素的实值,但不可以改变键值,会违反元素的排列的规则。它的底层机制同样也是RB-tree

Hash table

这种数据结构,在插入,删除,查找也有常数平均时间,而不依赖于插入元素的随机性,是以统计为基础的。Hash table可以提供对任何named item的存取和删除操作,因为所有被操作的对象都是named,所以hash table可以看做是一个dictionary。如何避免使用一个大得换谬的array呢,办法之一就是使用某种映射函数,将大数映射为小数,这样一来,array就不需要特别的大,但是可能会造成冲突。而解决这种冲突的做法则是使用 线性探测,二次探测,开放寻址等。

开链:一个键后面跟着一个list。STL中的hashtable就是这种方式。STL中的hash迭代器,是一种forward迭代器,只能+。有指向当前节点的指针和指向对应的vector的指针,没有后退操作,也就是没有所谓的逆向迭代器。

hashtable结构:

hashtable以质数来设计表格大小,预先计算好了28个质数,大约都是两倍的关系递增,查询28个质数中,“最接近且大于元素数目”的数字作为vector的长度,如果需要重新分配,则分配下一个质数长度的vector。Bucket所维护的linked list不采用STL的list或者slist(直接指针操作),而至于bucket则使用vector来完成。

hashtable设计的容量和buckets vector的大小一样。因此如果数目超过了,就需要重新建立:新建一个temp,将原来hashtable中的元素一个一个切割出来插入到新的temp的适当位置,全部插完后交换hashtable和temp。同时释放temp的内存。

insert分为insert_unique和insert_equal操作,前者保证插入的数不能有重复,后者可以插入键值相同的数。可以先用unique之后再用equal。insert_unique:先调用resize函数,看是否需要增大vector,然后插入,vector的索引通过取余得到。resize:如果已有元素的个数大于vector的size,需要根据得到的最新质数,分配新的空间,将在旧空间的元素,重新计算hash,复制到新的空间,最后旧空间与新空间swap一下即可。insert_equal:也是先调用resize,遍历找到和他相同的节点,在该节点的前面插入。

hashtable有一些无法处理的型别,比如string,double,float。除非用户为那些型别写了相应的hash function。


Hash_MapHash_SetHash_MultiSet,Hash_MultiMap

这些容器和前面介绍的一一对应,只不过这些是以hash_tabel为底层实现机制的。

底层机制决定了这两组容器的区别:

RB-tree组对元素实现排序,而hash_map组没有;

RB-tree的查找时间复杂度为lg(n),而hash_map组为常数时间;

RB-tree组在空间利用上,不会浪费结点,而hash_map组可能会有一些空置桶。

Hash_multiset和Hash_multimap插入使用的是inset_equal。其它操作与multiset和multimap相同。