1、关联式容器概述
所谓关联式容器,观念上类似于关联式数据库:每笔数据都有一个键值(key)和一个实际值(value)。当元素被插入容器时,内部机制根据键值,按着一定的规则将元素置于特定的位置。关联式容器没有所谓头尾的概念(只有最大元素,最小元素),所以不会有类似push_back(),push_front(),pop_back(),pop_front,begin(),end()这样的操作。
标准的stl关联式容器分为set(集合)和map(映射表)两大类,以及这两大类的衍生体multiset(多键集合)和multimap(多键映射表),这些容器的底层机制均以RB-tree来实现,RB-tree也是一个独立容器,并不开放给外界使用。
此外,SGI STL还提供了一个不在标准之内的关联式容器hash table,以及以此为底层机制而完成的hash_set(散列集合),hash_map(散列映射表),hash_multiset(散列多键集合),hash_multimap(散列多键映射表)。
2、AVL树
AVL树属于二叉查找树,二叉查找树的查找和插入操作在最坏情况下复杂度为O(N),而AVL树最坏时仍然为O(lgN)。
AVL树的插入和删除操作需要借助于节点的旋转来保持树的高度平衡。AVL树平衡被破坏时采用的调整规则:
(1)单旋转:用来修正外侧插入导致的不平衡。
(2)双旋转:用来修正内侧插入导致的不平衡。可以利用两次单旋完成。
节点的插入结果可以分为四种情况。节点插入之后如果AVL树的平衡遭到破坏,那么,令X为平衡状态被破坏的节点中最深(下方)的节点。
插入点位于X的左子结点的左子树——左左;(外侧插入,单旋)
插入点位于X的左子结点的右子树——左右;(内测插入,双旋)
插入点位于X的右子结点的左子树——右左;(内测插入,双旋)
插入点位于X的右子结点的右子树——右右;(外侧插入,单旋)
3、红黑树(RB-tree)
由红黑树规则知:新增节点必须为红色;新增节点之父节点必须为黑色。
当新增节点根据二叉搜索树的规则到达其插入点,却未能符合红黑树的规则时,就必须调整颜色并旋转树形。
为了更大的弹性,SGI将RB-tree迭代器实现为两层。RB-tree迭代器,但不具备随机定位功能,其提领操作和成员访问操作与list十分近似,较为特殊的是其前进和后退操作。前进操作调用了基层迭代器的increment(),后退操作调用了基层迭代器的decrement()。
RB-tree有定义专属的空间配置器,每次配置一个节点。RB-tree的构造方式有两种,一种是以现有RB-tree复制一个新的RB-tree,另一种是产生一颗空空如也的树。
set和multiset
set的特性:
(1)所有元素都会根据元素的键值自动被排序。
(2)set是集合,它的元素的键值就是实值,实值就是键值,不允许两个元素有相同的值。
(3)不可以通过set的iterator来改变元素的值,因为set的元素值就是键值,改变键值会违反元素排列的规则。
(4)在客户端对set进行插入或删除操作后,之前的迭代器依然有效。当然,被删除的元素的迭代器是个例外。
(5)它的底层机制是RB-tree。几乎所有的操作都只是转调用RB-tree的操作行为而已。
multiset和set几乎一样,唯一的区别是,multiset允许键值重复。因此set使用底层RB-tree的insert_unique()实现插入,而multiset插入采用的是RB-tree的insert_equal()而非insert_unique()。
测试实例:
#include <set>
#include <iostream>
using namespace std;
int main()
{
int ia[5] = {0,1,2,3,4};
int n = sizeof(ia)/sizeof(ia[0]);
cout<<n<<endl;// 5
set<int> iset(ia, ia+n);
cout<<"size="<<iset.size() << endl;// size=5
cout<<iset.count(3)<<endl;// 1
iset.insert(3);//插入无效,元素不允许重复
cout<<"size="<<iset.size() << endl;// size=5
cout<<iset.count(3)<<endl;// 1
iset.erase(1);
cout<<iset.count(1)<<endl;// 0
set<int>::iterator ite1=iset.begin();
set<int>::iterator ite2=iset.end();
for(; ite1 != ite2; ++ite1)
cout<<*ite1<<" ";
cout<<endl;//0 2 3 4
//关联式容器,应采用其所提供的find函数搜寻元素,会比使用
//STL算法find()更优效率!因STL find()只是循序搜寻。
ite1 = iset.find(3);//使用自身提供的find函数
if(ite1 != iset.end())
cout<<"3 found!"<<endl;
else
cout<<"3 not found!"<<endl;
//企图通过迭代器改变set元素,是不被允许的
//*ite1 = 9;//error !
return 0;
}
map和multimap
map的特性:
(1)所有元素都会根据元素的键值自动被排序。
(2)map的所有元素都是pair,第一个值是键值,第二个是实值。
(3)map不允许两个元素拥有相同的键值。
(4)可以通过map的迭代器来改变元素的实值,但不可以改变键值,那样会违反元素的排列规则。
(5)在客户端对map进行插入或删除操作后,之前的迭代器依然有效。当然,被删除的元素的迭代器是个例外。
(6)它的底层机制是RB-tree。几乎所有的操作都只是转调用RB-tree的操作行为而已。
multimap和map几乎一样,唯一的区别是,multimap允许键值重复。因此map使用底层RB-tree的insert_unique()实现插入,而multimap插入采用的是RB-tree的insert_equal()而非insert_unique()。
hashtable
1、hashtable概述
hashtable可以提供对任意有名项的存取和删除操作,这种结构的用意在于提供常数时间的的基本操作,而不依赖于插入元素的随机性,是以统计为基础的。
散列函数(hash function):负责将某一元素映射为一个”大小可接受之索引”。简而言之,就是将大数映射为小数。
使用hash function带来的问题:可能有不同元素映射到相同的位置(相同索引)。这便是碰撞或冲突问题。解决碰撞问题的方法:线性探测(linear probing),二次探测(quadratic probing),开链(separate chaining)等。stl hashtable采用的hash方式是开链法。
(1)线性探测:当hash function计算出某个元素的插入位置,而该位置空间不再可用时,怎么做?最简单的办法就是循序往下一一寻找,知道找到一个可用空间为止。
需要两个假设:a.表格足够大。b.每个元素都能够独立。
线性探测会造成主集团(primary clustering)问题:平均插入成本的成长幅度,远高于负载系数的成长幅度。
(2)二次探测:主要用来解决主集团问题。解决碰撞的方程式为F(i) = i^2。如果hash function计算出新元素的位置为H,而该位置实际上已被使用,那么就依次尝试H+1^2,H+2^2,H+3^2,H+4^2,….,H+i^2,而不像线性探测尝试的是H+1,H+2,H+3,H+4,….,H+i。
二次探测可以消除主集团,却可能造成次集团(secondary clustering):两个元素经hash function计算出来的位置若相同,则插入时所探测的位置也相同,形成某种浪费。消除次集团的方法如复式散列。
(3)开链:这种做法是在每一个表格元素中维护一个list。hash function为我们分配某一个list,然后我们在哪个list身上执行元素的插入、搜寻、删除等操作。若list够短,速度还是够快。
使用开链法,表格的负载系数将大于1。SGI STL hashtable便采用的是开链法。
hashtable结构
hashtable表格内的每个单元,涵盖的不只是个节点(元素),而可能是一桶节点,因此称为bucket。
SGI STL中hash table使用的是开链法进行的冲突处理,其结构如图所示:
bucket所维护的linked list不采用STL的list或者slist,而是自行维护hash table node。而至于buckets聚合体,则使用vector来完成,以便有动态扩容能力。
STL中的hash迭代器,是一种forward迭代器,只能+。有指向当前节点的指针和指向对应的vector的指针,没有后退操作,也就是没有所谓的逆向迭代器。
hashtable以质数来设计表格大小,预先计算好了28个质数,以备随时访问,大约都是两倍的关系递增,同时提供一个函数,查询28个质数中,“最接近某数且大于某数”的质数作为vector的长度,如果需要重新分配,则分配下一个质数长度的vector。
stl hash table扩张表格的触发条件是:当元素的数目大于或等于表格的大小。(这个条件应该是为了保证常数操作时间,在统计基础上得出的)。
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。
4、hash_set,hash_map,hash_multiset,hash_multimap
这些容器和前面介绍的一一对应,只不过这些都是以hash_tabel为底层实现机制的。
底层机制决定了这两组容器的区别:
RB-tree组对元素实现排序,而hashtable组没有;
RB-tree组的查找时间复杂度为lg(n),而hashtable组为常数时间;
RB-tree组在空间利用上,不会浪费结点,而hashtable组可能会有一些空置桶。
hash_multiset和hash_multimap插入使用的是inset_equal。其它操作与multiset和multimap相同。