1、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;// 2
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;
}
2、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()
。
3、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便采用的是开链法。
2、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相同。