文章目录
关联容器和顺序容器的区别
关联容器和顺序容器有着根本的不同:关联容器中的元素是按关键字来保存和访问的。与之相对的,顺序容器中的元素是按它们在容器中的位置顺序保存和访问的。
虽然关联容器的很多行为与顺序容器相同,但其不同之处反映了关键字的作用。
引言
两个主要的关联容器:map和set。
map中的元素是一些关键字-值对:关键字起到索引的作用,值则表示与索引相关联的数据。
set中每个元素只包含一个关键字:set支持高效的关键字查询操作-检查一个给定关键字是否在set中。
标准库提供8个关联容器:
关联容器类型(按关键字有序保存元素) | 描述 |
---|---|
map | 关联数组:保存关键字-值对 |
set | 关键字即值,即只保存关键字的容器 |
multimap | 关键字可重复出现的map |
multiset | 关键字可重复出现的set |
关联容器类型(无序集合) | 描述 |
---|---|
unorder_map | 用哈希函数组织的map |
unordered_set | 用哈希函数组织的set |
unordered_multimap | 哈希组织的map,关键字可重复出现 |
unordered_multiset | 哈希组织的set,关键字可重复出现 |
这八个容器的不同体现在三个维度:
- 或者是一个set,或者是一个map;
- 或者要求不重复的关键字,或者允许重复关键字;
- 按顺序保存元素,或无序保存。
类型map和multimap定义在头文件map中;set和multiset定义在头文件set中;
无序容器定义在头文件unordered_map和unordered_set中。
使用关联容器
map通常被称为关联数组。关联数组与“正常”数组类似,不同之处在于其下标不必是整数。
与之相对,set是关键字的简单集合。只想知道一个值是否存在时,set是最有用的。
使用map
#include <iostream>
#include <map>
#include <string>
// 单词计数程序
using namespace std;
int main(){
map<string,size_t> word_count;
string word;
while (cin >> word)
++word_count[word];
for (const auto& w : word_count)
cout << w.first << " occurs " << w.second << ((w.second > 1) ? " times" : " time") << endl;
return 0;
}
map保存的每个元素中,关键字是string类型,值是size_t类型。(定义)
当对word_count进行下标操作时,使用string作为下标,获得size_t类型的计数器。(索引)
当word没在map中就进行索引的话,下标运算符会创建一个新元素,其关键字是word,值为0;(创建新元素)
采用范围for语句遍历map时,会得到一个pair类型的对象。pair是一个模板类型,保存名为first和second的共有数据成员。
map所使用的pair用first成员保存关键字,用second成员保存对应的值。(键值对的获取)
使用set
// 单词计数程序,只统计不再exclude中的单词
void count_word_update() {
map<string, size_t> word_count;
set<string> exclude = { "The","But","a" }; // 列表初始化
string word;
while (cin >> word) {
// find调用返回一个迭代器。如果给定关键字在set中,迭代器指向该关键字。否则返回尾后迭代器
if (exclude.find(word) == exclude.end())
++word_count[word];
}
}
关联容器概述
支持顺序容器-容器库概览中的所有操作(初始化、赋值、交换、迭代器、大小)。
关联容器不支持顺序容器的位置相关的操作,如插入(push_back、push_front)、删除(pop_back…)。这是因为关联容器中的元素是根据关键字存储的,这些操作对关联容器没有意义。
关联容器并不支持构造函数或插入操作这些接受一个元素值和一个数量值的操作。
关联容器的迭代器都是双向的。我们这里可以回顾一下顺序容器的迭代器有单向的、双向的、随机的。
定义关联容器
初始化set和map
// 空容器
map<string,size_t> word_count;
// 列表初始化
set<string> exclude = {"the", "but"};
map<string,string> = authors = {{"Joyce", "James"}, {"Austen", "Jane"}};
// 拷贝初始化与迭代器初始化也是可行的;
// 可以同类型的其他容器来初始化该容器
初始化multimap和multiset
一个map或set中的关键字必须是唯一的,multimap或multiset则没有这样的限制。
vector<int> ivec;
for (int i = 0; i < 10; ++i) {
ivec.push_back(i);
ivec.push_back(i);
}
set<int> iset(ivec.cbegin(), ivec.cend());
multiset<int> imset(ivec.cbegin(), ivec.cend());
cout << ivec.size() << endl; // 20
cout << iset.size() << endl; // 10
cout << imset.size() << endl; // 20
关键字类型要求
对于有序容器,关键字类型必须定义元素比较的方法。
集合类型中,关键字类型就是元素类型,在映射类型中关键字类型是元素的第一部分类型。
有序容器的关键字类型
可以向算法提供一个自定义的比较操作。所提供的操作必须在关键字类型上定义一个严格弱序(小于等于)。
其必须具备一下性质:
- 两个关键字不能同时小于等于对方;
- 如果 k 1 ≤ k 2 , k 2 ≤ k 3 k1\le k2,k2\le k3 k1≤k2,k2≤k3,那么 k 1 ≤ k 3 k1\le k3 k1≤k3;
- 如果存在两个关键字,任何一个都不小于等于另一个,那么称这两个关键字是等价的。
使用关键字类型的比较函数
我们不能直接定义一个Sales_data的multiset,因为Sales_data没有<运算符。
我们给出比较函数
bool compareIsbn(const Sales_data& lhs, const Sales_data& rhs)
return lhs.isbn < rhs.isbn;
为了使用自定义的操作,在定义multiset时,我们必须提供两个类型:关键字类型Sales_data以及比较操作类型(函数指针类型)
multiset<Sales_data,decltype(compareIsbn)*>
boiokstore(compareIsbn);
使用decltype来指出自定义操作的类型。当使用decltype来获得一个函数指针时,必须加上*来指出我们要使用一个给定函数类型的指针。
另外,我们还需要用compareIsbn来初始化bookstore对象。
pair类型
名为pair的标准库类型定义在头文件utility中。
一个pair类型保存两个数据成员。
pair是一个用来生成特定类型的模板,当创建一个pair时,必须提供两个类型名,pair的数据成员将具有对应的类型。
比如
pair<string, string> anon;
pair<string, vector<int>>;
pair的默认构造函数对数据成员进行值初始化(像Sales_data中一样,在默认给数据成员0、0.0这样的赋值,且在默认构造函数中也是这么写的)。
与其他标准库类型不同,pair的数据成员是public的,两个成员分别命名为first和second。
我们用普通成员的访问符号来访问他们。
pair上的操作 | 含义 |
---|---|
pair<T1,T2> p; | p是一个pair,两个类型分别为T1、T2的成员都进行了值初始化 |
pair<T1,T2> p(v1,v2); | - |
pair<T1,T2> p={v1,v2}; | - |
make_pair(v1,v2); | 返回一个用v1和v2初始化的pair。pair的类型从v1和v2的类型推断出来 |
p.first; | 返回p的名为first的数据成员 |
p.second; | 返回p的名为second的数据成员 |
p1 relop p2; | 关系运算符,按字典序定义:比如,当p1.first < p2.first 或 !(p2.first < p1.first)&& p1.second < p2.second成立时,p1<p2为true。 |
p1 == p2; | - |
p1 != p2; | - |
关联容器操作
除了顺序容器-容器类型成员,关联容器还定义了这些类型:
关联容器额外的类型别名 | 含义 |
---|---|
key_type | 此容器类型的关键字类型 |
mapped_type | 每个关键字关联的类型:只适用于map类型 |
value_type | 对于set类型,与key_type相同;对于map类型,为pair<const ket_type, mapped_type> |
关联容器迭代器
迭代器是容器普遍支持的操作。
解引用一个关联容器迭代器时,会得到一个value_type类型的值的引用。
对于map而言,是一个pair类型,其first成员保存const关键字,second成员保存值。
set中的关键字也是const的,iterator和const_iterator都只允许只读访问set中的元素。
添加元素
关联容器insert操作 | 含义 |
---|---|
c.insert(v) | v是value_type类型的对象;args用来构造一个元素。对于map和set,只有当元素的关键字不在c中时才插入元素。函数返回一个pair,包含一个迭代器,指向具有关键字的元素,以及一个指示是否成功的bool值 |
c.emplace(args) | - |
c.insert(b,e) | b和e是迭代器,表示一个c::value_type类型值得范围;i1是一个花括号列表。函数返回void。 |
c.insert(i1) | - |
c.insert(p.v) | 类似insert(v),但将迭代器p作为一个提示,指出从哪里开始搜索新元素应该存储的位置。返回一个迭代器,指向具有给定关键字的元素。 |
c.emplace(p,args) | - |
如果关键字已在容器中,则insert什么也不做,且返回值中的bool部分为false。如果关键字不存在,元素被插入容器中,且bool值为true。
对允许重复关键字的容器,接受单个元素的insert操作返回一个指向新元素的迭代器,无需返回bool值,因为insert总是向这个类容器中添加一个新元素。
删除元素
从关联容器删除元素 | 含义 |
---|---|
c.erase(k) | 从c中删除每个关键字为k的元素。返回一个size_type值,指出删除的元素的数量 |
c.erase(p) | 从c中删除迭代器p指定的元素。p必须指向c中一个真实元素,不能等于c.end()。返回一个指向p之后元素的迭代器,若p指向c中的尾元素,则返回c.end()。 |
c.erase(b,e) | 删除迭代器对b和e所表示的范围中的元素。返回e。 |
对允许重复关键字的容器,删除元素的数量可能大于1。
map的下标操作
map和unordered_map容器提供了下标运算符和一个对应的at函数。
下标操作 | 含义 |
---|---|
c[k] | 返回关键字为k的元素;如果k不在c中,添加一个关键字为k的元素,并对其进行值初始化 |
c.at(k) | 访问关键字为k的元素,带参数检查;若k不在c中,抛出一个out_of_range异常 |
set类型不支持下标,因为set中没有关键字相关联的“值”,元素本身就是关键字;
不能对multimap或unorderer_multimap进行下标操作,因为这些容器可能有多个值与一个关键字相关联。
如word_count["Anna"]=1
:
- 在word_count中搜索关键字为Anna的元素,未找到;
- 将一个新关键字插入到word_count中,值进行值初始化为0;
- 提取出新插入的元素,并将值1赋予它。
使用下标操作的返回值
与其他下标运算符相同的是,map的下标运算符返回一个左值,所以既可以读也可以写元素。
如果我们只想直到这个元素在不在里面,不想往里面加东西,就不能用下标操作。
访问元素
如果我们所关系的不过是一个特定的元素是或已经在容器中,可能find是最佳的选择。对于不允许重复关键字的容器,可能使用find还是count没什么区别。但允许重复关键字的容器,count还会做更多的工作:如果元素在容器中 ,它还会统计有多少个元素有相同的关键字。如果不需要计数,最好使用find。
在一个关联容器中查找元素的操作 | 含义 |
---|---|
c.find(k) | 返回一个迭代器,指向第一个关键字为k的元素,若k不在容器中,则返回尾后迭代器 |
c.count(k) | 返回关键字等于k的元素的数量。对于不允许重复关键字的容器,返回值永远是0或1 |
c.lower_bound(k) | 返回一个迭代器,指向第一个关键字不小于k的元素 |
c.upper_bound(k) | 返回一个迭代器,指向第一个关键字大于k的元素 |
c.equal_range(k) | 返回一个迭代器pair,表示关键字等于k的元素的返回。若k不存在,pair的两个成员均等于c.end() |
lower_bound & upper_bound不适用于无序容器。
在multimap或multiset中查找元素
如果一个multimap或multiset中有多个元素具有给定关键字,则这些元素在容器中会相邻存储。
给定一个从作者到著作题目的映射,我们想打印一个特定作者的所有著作。有三种不同方法来解决这个问题。
法一:find和count
string search_item("Alain de Botton");
auto entries = authors.count(search_item);
auto iter = authors.find(search_item);
while(entries){
cout << iter -> second << endl;
++iter;
--entries;
}
法二:面向迭代器的解决方法
可以用lower_bound和upper_bound来解决问题。
lower_bound返回的迭代器将指向第一个具有给定关键字的元素;upper_bound返回的迭代器则指向最后一个匹配给定关键字的元素之后的位置。
如果元素不再multimap中,lower_bound和upper_bound会返回相等的迭代器。
综上所示,相同关键字调用lower_bound和upper_bound会得到一个迭代器范围。
我们由此也可以知道有序关联容器,map和set类型按照key的顺序来存储key和value。
for (
auto beg = author.lower_bound(search_item),end = author.upper_bound(search_item);
beg != end;
++beg)
cout << beg->second << endl;
法三:equal_range函数
equal_range函数相当于lower_bound和upper_bound的组合。此函数接收一个关键字,返回一个迭代器pair。
若关键字存在,则第一个迭代器指向第一个关键字匹配的元素,第二个迭代器指向最后一个匹配的元素之后的位置。如果未找到匹配元素,则两个迭代器都指向关键字可以插入的位置。
for (auto pos = authors.equal_range(search_item);
pos.first != pos.second;
++pos.first)
cout << pos.first->second << endl;
无序容器
无序容器不是用比较运算符来组织元素的,而是使用一个哈希函数和关键字类型的==运算符。
如果关键字类型固有就是无序的,或者性能测试发现问题可以用哈希技术解决,就可以使用无序容器。
使用无序容器
除了哈希管理操作之外,无序容器害提供了与有序容器相同的操作(find、insert等)。这意味着我们曾用于map和set的操作也可以用于unordered_map和unordered_set。类似的,无序容器也有允许重复关键字的版本。
由于元素未按顺序存储,所以无序容器的程序的输出通常会与使用有序容器的版本不同。
unordered_map<string,size_t> word_count;
string word;
while (cin>>word)
++word_count[word];
for (const auto &w:word_count)
cout << ...
管理桶
无序容器在存储上组织为一组桶,每个桶保存0个或多个元素。
无序容器使用一个哈希函数将元素映射到桶。
为了访问一个元素,容器首先计算元素的哈希值,它指出应该搜索哪个桶。
容器将具有一个特定哈希值的所有元素都保存在桶中。
因此,无序容器的性能依赖于哈希函数的质量和桶的数量大小。
理想的情况下,哈希函数还能将每个特定的值映射到唯一的桶。但是,将不同的关键字的元素映射到相同的桶也是允许的。每个桶保存多个元素时,需要顺序搜索这些元素来查照我们想要哪个。
无序容器提供了一组管理桶的函数。
无序容器管理操作 | 所属 | 含义 |
---|---|---|
c.bucket_count() | 桶接口 | 正在使用的桶的数目 |
c.max_bucket_count() | 桶接口 | 容器能容纳的最多的桶的数量 |
c.bucket_size(n) | 桶接口 | 第n个桶中有多少个元素 |
c.bucket(k) | 桶接口 | 关键字为k的元素在哪个桶中 |
local_iterator | 桶迭代 | 可以用来访问桶中元素的迭代器类型 |
const_local_iterator | 桶迭代 | 桶迭代器的const版本 |
c.begin(n),c.end(n) | 桶迭代 | 桶n的首元素迭代器和尾后迭代器 |
c.cbegin(n),c.cend(n) | 桶迭代 | 与前两个函数类似,但返回const_local_iterator |
c.load_factor | 哈希策略 | 每个桶的平均元素数量,返回float值 |
c.max_load_factor | 哈希策略 | c试图维护的平均桶大小,返回float值。c会在需要时添加新的桶,以使得 l o a d _ f a c t o r ≤ m a x _ l o a d _ f a c t o r load\_factor\le max\_load\_factor load_factor≤max_load_factor |
c.rehash(n) | 哈希策略 | 重组存储,使得 b u c k e t _ c o u n t ≥ n bucket\_count\ge n bucket_count≥n且 b u c k e t _ c o u n t > s i z e / m a x _ l o a d _ f a c t o r bucket\_count\gt size/max\_load\_factor bucket_count>size/max_load_factor |
c.reverse(n) | 哈希策略 | 重组存储,使得c可以保存n个元素且不必rehash |
无序容器对关键字类型的要求
默认情况下,无序容器使用关键字类型的==运算符来比较元素,还是用hash<key_type>类型的对象来生成每个元素的哈希值。
标准库为内置类型(包括指针)提供了hash模板。
我们不能自定义关键字类型为自定义类类型的无序容器。与容器不同,不能直接使用哈希模板,我们必须提供自己的hash模板版本。
哈希函数:
size_t hasher(const Sales_data &sd)
return hash<string>()(sd.isbn());
bool eq0p(const Sales_data &lhs, const Sales_data &rhs)
return lhs.isbn() == rhs.isbn();
我们的hasher函数使用一个标准库hash类型对象来计算ISBN成员的哈希值,该hash类型建立在string类型之上。
using SD_multiset = unordered_multiset<Sales_data,decltype(hasher)*,decltype(eq0p)*>;
SD_multiset bookstore(42,hasher,eq0p);