关联容器和顺序容器有着根本的不同:关联容器中的元素是按关键字来保存和访问的
,而顺序容器中的元素是按它们在容器中的位置来顺序保存和访问的
。
关联容器支持高效的关键字查找和访问,两个主要的关联容器类型是map
和set
。map中的元素是一些键-值(key-value)对:关键字起到索引的作用,值则表示与索引相关联的数据。set中每个元素值只包含一个关键字:set支持高效的关键字查询操作——检查一个给定关键字是否在set中。
标准库一共提供了8个关联容器
:允许关键字重复的容器名字中都包含单词multi
,不保持关键字按顺序存储的容器的名字都以单词unordered开头
。因此unordered_multi_set
是一个允许重复关键字、元素无序保存
的集合,而set
则是一个不允许重复关键字、有序存储
的集合,无序容器使用哈希函数
来组织元素。类型map和multimap定义在头文件map中,set和multiset定义在头文件set中,无序容器定义在头文件unordered_map和unordered_set中。
关联容器不支持顺序容器的位置相关的操作
,例如push_front或push_back
,而且关联容器也不支持构造函数或插入操作这些接受一个元素值和一个数量值的操作
。关联容器还支持一些顺序容器不支持的操作和类型别名
,此外关联容器的迭代器都是双向的
。
对于有序容器
——map、multumap、set或multiset,关键字类型必须定义元素比较的方法,默认情况下,标准库使用关键字类型的<运算符来比较两个关键字
。此外也可以提供自定义的操作来代替关键字上的<运算符,所提供的操作必须在关键字类型上定义一个严格弱序(即“小于等于”)
。为了使用自己定义的操作,在定义有序容器时必须提供指向该比较操作的函数指针。
bool compareIsbn(const Sales_data &lhs,const Sales_data &rhs)
{
return lhs.isbn() < rhs.isbn();
}
multiset<Sales_data,decltype(compaerIsbn)*> bookstore(compareIsbn); //这里使用&compareIsbn效果也一样
map的元素是pair类型
的,它定义在头文件utility中,一个pair保存两个数据成员,当创建一个pair时,我们必须提供两个类型名。此外pair的数据成员是public的
,两个成员分别命名为first
和second
,可以直接通过普通的成员访问符号来访问。
pair<T1,T2> p; //两个成员进行值初始化
pair<T1,T2> p(v1,v2); //first和second分别用v1和v2初始化
pair<T1,T2> p = {
v1,v2}; //等价于p(v1,v2)
make_pair(v1,v2); //返回一个v1和v2初始化的pair,pair的类型从v1和v2的类型推断出来
p.first,p.scond; //返回p的first和second成员
关联容器还定义了几种特有的类型别名:
类型别名 | 说明 |
---|---|
key_type |
容器的关键字类型 |
mapped_type |
与关键字关联的类型,只适用于map |
value_type |
对于set,与key_type相同;对于map,为pair |
set中的元素是const的,map中的元素是pair,其第一个成员是const的
。我们通常不能对关联容器使用泛型算法,关键字是const这一特性意味着不能将关联容器传递给修改或重排容器元素的算法,只能用于只读取元素的算法
,但是这类算法都要搜素序列,由于关联容器中的元素不能通过它们的关键字进行(快速)查找,因此对其使用泛型算法不是一个好主意。例如,关联容器定义了一个名为find的成员,它通过一个给定的关键字直接获取元素,如果使用泛型find算法来查找,则会进行顺序搜索,因此使用关联容器自定义的find成员会比调用泛型find快得多
。在实际编程中,如果我们真要对一个关联容器使用算法,要么将它当做一个源序列,要么当做一个目的位置
。
关联容器的insert成员
向容器中添加一个元素或一个元素范围,对于包含不重复关键字的容器来讲(例如map和set),只有第一个带此关键字的元素才会被成功插入到容器中
。insert的返回值依赖于容器类型和参数,对于不包含重复关键字的容器,添加单一元素的insert和emplace版本返回一个pair,告诉我们插入是否成功
,pair的first成员是一个迭代器,指向具有给定关键字的元素,second成员是一个bool值,指出元素是插入成功还是已经存在于容器中。对允许重复关键字的容器,接受单个元素的insert操作只返回一个指向新元素的迭代器
。
vector<int> ivec = {
2,4,6,8,2,4,6,8}; //ivec有8个元素
set<int> set2;
set2.insert(ivec.cbegin(),ivec.cend()); //set2有4个元素
关联容器除了提供普通版本erase操作外,还提供了一个接受key_type参数的erase版本,此版本删除所有匹配给定关键字的元素,返回实际删除的元素的数量
。
map和unordered_map提供了下标运算符和一个对应的at函数
,set类型不支持下标
,因为set中没有与关键字相关联的“值“””。我们不能对一个multimap或一个unordered_multimap进行下标操作,因为可能有多个值与一个关键字对应
。map下标运算符接受一个索引(即一个关键字),获取与此关键字相关联的值,但与其他下标运算符不同的是,如果关键字并不在map中,则会为它创建一个元素并插入到map中,关联值将进行值初始化
。由于下标运算符可能插入一个新元素,因此我们只可以对非const的map使用下标操作和at操作
。map的下标运算符返回的是一个左值,因此我们既可以读也可以写元素
。
map<string,size_t> word_count; //空map
word_count["Anna"] = 1; //未找到“Anna”关键字,则插入“Anna”,并对值进行值初始化,然后提取出新插入的元素,并将值1赋予它
cout << word_count["Anna"]; //打印1
++word_count["Anna"]; //提取元素,将其增1
cout << word_count["Anna"]; //打印2
关联容器提供了一系列用于查找元素的操作:
操作 | 说明 |
---|---|
c.find(k) | 返回一个迭代器,指向第一个关键字为k的元素 ,若k不在容器中,则返回尾后迭代器 | |
c.count(k) | 返回关键字等于k的元素的数量 |
c.lower_bound(k) | 返回一个迭代器,指向第一个关键字不小于k的元素 | |
c.upper_bound(k) | 返回一个迭代器,指向第一个关键字大于k的元素 | |
c.equal_range(k) | 返回一个迭代器pair,表示关键字等于k的元素的范围 ,若k不存在,pair的两个成员均等于c,end() |
如果我们只是想知道一个给定的关键字是否是在map中,而不是想改变map,应该使用find,因为如果关键字不存在,下标运算符会插入一个新元素。如果一个multimap或multiset中有多个元素具有相同的关键字,那么这些元素在容器中会相邻存储
。
新标准定义了4个无序关联容器,这些无序容器不是使用比较运算符来组织元素,而是使用一个哈希函数和关键字类型的==运算符
。在关键字类型的元素没有明显的序关系的情况下,无序容器是非常有用。在某些应用中,维护元素出的序代价非常高昂,此时无序容器也很有用。
虽然理论上哈希技术能获得更好的平均性能,但在实际中要想到达很好的效果还需进行一些性能测试和调优工作,因此使用无序容器通常更为简单(通常也会有更好的性能)。如果关键字类型固有就是无序的,或者性能测试发现问题可以用哈希技术来解决,就可以使用无序容器
。无序容器的性能依赖于哈希函数的质量和桶的数量和大小
,无序容器提供了一组管理桶的函数,这些成员函数允许我们查询容器的转态以及在必要时强制容器进行重组。
管理操作 | 说明 |
---|---|
桶接口 | |
c.bucket_count() | 正在使用的桶的数目 |
c.max_bucket_count() | 容器能容纳的最多的桶的数量 |
c.bucket_size(n) |
第n个桶中有多少个元素 |
c.bucket(k) |
关键字为k的元素在哪个桶中 |
桶迭代 | |
local_iterator | 可以用来访问桶中元素的迭代器类型 |
constlocal_iterator | const版本 |
c.begin(n),c.end(n) | 桶n的首元素迭代器和尾后迭代器 |
c.cbegin(n),c.cend(n) | const版本 |
哈希策略 | |
c.load_factor() |
每个桶平均元素数量 ,返回float值 |
c.max_load_factor() |
c试图维护的平均桶大小,返回float值 。c会在需要时添加新的桶,以使得load_factor<=max_load_factor |
c.rehash(n) | 重组存储,使得buck_count>=n且bucket_count > size/max_load_factor |
c.reserve(n) | 重组存储,使得c可以保存n个元素且不必rehash |
默认情况下,无序容器使用关键字类型的相等运算符来比较元素
,此外它们还使用一个hash
,标准库为内置类型(包括指针)、string和智能指针类型都定义了hash
,但是对于自定义类型的关键字,我们必须提供自己的hash模板版本以及==运算符
。
size_t hasher(const Sales_data &sd)
{
return hash<string>()(sd.isbn()); //自定义hash函数
}
bool eqOp(const Sales_data &lhs,const Sales_data &rhs)
{
return lhs.isbn() == rhs.isbn(); //自定义函数替代==运算符
}
using SD_multiset unordered_mulitset<Sales_data,decltype(hasher)*,decltype(eqOp)*>;
SD_multiset bookstore(42,hasher,eqOp);
无论在有序容器还是无序容器中,具有相同关键字的元素都是相邻存储的
。