两个主要的关联容器,类型是 mapset。map中的元素是一些关键字—值(key-value)对,set中每个元素只包含一个关键字(key) , 可以理解为set中的 key 和 value 是相同的。

标准库提供8个关联容器,这8个容器的不同体现在三个维度上:

  • 每个容器或者是一个set,或者是一个map
  • 或者要求不重复的关键字,或者允许重复关键字
  • 按顺序保存元素,或无序保存

允许重复关键字的容器的名字中都包含单词 multi ; 不保存关键字按顺序存储的容器的名字都以单词 unordered 开头。因此一个 unordered_multi_set 是一个允许重复关键字,元素无序保存的集合,而一个 set 则是一个要求不重复关键字,有序存储的集合。 map 和 set 以及相关的容器分别定义在 map 和 set 头文件中。

image-20221201121151479

1. 使用关联容器

使用map

一个经典的使用关联数组的例子,单词计数程序:

map<string, size_t> word_count;		// string 到 size_t的空map
string word;
while(cin >> word)
    ++word_count[word];		// 提取word的计数器并将其加1
for (const auto &w : word_count)	// 对map中的每个元素 打印结果
    cout << w.first << " occurs " << w.second
    	 << ((w.second > 1) ? " times" : " time") << endl;

以上代码使用的输入示例:

Although most programmers are familiar with data structures such as vectors and lists, many have never used an associative data structure. Before we look at the details of how the library supports these types, it will be helpful to start with examples of how we can use these containers.

在C++中是通过 [] 运算符来获取对应 key 的 value, map[key] 返回的就是 key-value 对中对应的value,该操作是通过重载 [] 运算符实现的。

使用set

上一个示例的一个扩展,忽略常见单词, 如 “the” 、“and” 、“or” 等。我们可以使用 set 保存想忽略的单词,只对不在集合中的单词统计出现次数:

// 统计输入单词从每个单词出现的次数
map<string, size_t> word_count;		// string 到 size_t 的空 map
set<string> exclude = {"The", "But", "And", "Or", "An", "A",
                      "the", "but", "and", "or", "an", "a"};
string word;
while (cin >> word) 
    // 只统计不在exclude中的单词
    if (exclude.find(word) == exclude.end())
        ++word_count[word];		// 获取并递增word的计数器

上面代码中的 if判断语句,其中 exclude.find(word) 返回的是一个迭代器,如果查找不到对应的 word 返回的就是尾后迭代器。

2. 关联容器概述

关联容器支持9.2节中介绍的普通容器操作,不支持顺序容器的位置相关的操作,因为关联容器中元素是根据关键字存储的。除了与顺序容器相同的操作之外,关联容器还支持一些顺序容器不支持的操作和类型别名。

2.1 定义关联容器

定义一个 map 时 ,需要指明关键字(key)类型和值(value)类型;定义 set 时只需要指定关键字(key)类型。

map<string, size_t> word_count;		// 空容器
// 列表初始化
set<string> exclude = {"The", "But", "And", "Or", "An", "A",
                      "the", "but", "and", "or", "an", "a"};
// 三个元素; authors 将姓映射为名
map<string, string> authors = { {"Joyce", "James"},
                               	{"Austen", "Jane"},
                               	{"Dickens", "Charles"} };

初始化 multimapmultiset

multimapmultiset 的key可以重复的,而 mapset 的key必须是唯一的。

下面是一个例子,创建一个名为 ivec 的保存 int 的 vector, 它包含 20 个元素: 0 到 9 每个整数有两个拷贝。我们将使用 vector 初始化一个set和一个multiset

// 定义一个有20个元素的vector,保存 0 到 9 每个整数的两个拷贝
vector<int> ivec;
for (vector<int>::size_type i = 0; i != 10; ++i) {
    ivec.push_back(i);
    ivec.push_back(i);  // 每个数重复保存一次
}
// iset 包含来自ivec的不重复的元素; miset 包含所有20个元素
set<int> iset(ivec.cbegin(), ivec.cend());
multiset<int> miset(ivec.cbegin(), ivec.cend());
cout << ivec.size() << endl;	// 打印出 20
cout << iset.size() << endl;	// 打印出 10
cout << miset.size() << endl;	// 打印出 20

2.2 关键字类型的要求

关联容器对其key类型有一些限制。对于无序容器中key的要求在后面的章节介绍。对于有序容器– mapmultimapset 以及 multiset ,key的类型必须定义元素比较的方法。默认情况下,默认库使用key类型的 < 运算符来比较两个 key。

有序容器的关键字类型

可以向一个算法提供我们自己定义的比较操作,与之类似,也可以提供自己定义的操作来替代key上 < 的运算符。无论我们怎样定义比较函数,它必须具备如下基本性质:

  • 两个key不能同时“小于等于”对方;如果 k1 “小于等于” k2, 那么 k2 绝不能“小于等于” k1。
  • 如果 k1 “小于等于” k2, 且k2 “小于等于” k3, 那么 k1 必须等于 “小于等于” k3。
  • 如果存在两个key, 任何一个都不 “小于等于” 另一个,那么我们称这个两个关键字是“等价”的。如果k1 “等价于” k2, 且k2 “等价于” k3, 那么 k1 必须“等价于”k3。

使用关键字类型的比较函数

对于提供的自定以的操作也是容器的类型的一部分,用尖括号指出要定义哪种类型的容器,自定义的操作类型必须在尖括号中紧跟着元素类型给出。在尖括号中出现的每个类型,就仅仅是一个类型而已。当我们创建一个容器(对象)时,才会以构造函数参数的形式提供真正的比较操作(其类型必须与在尖括号中指定的类型相吻合)。

例如,我们不能直接定义一个 Sales_datamultiset ,因为 Sales_data 没有 < 运算符。但是可以用 compareIsbn 函数来定义一个 multiset。该函数是10.3.1节的练习中的一个函数

bool compareIsbn(const Sales_data& lhs, const Sales_data& rhs) {
    return lhs.isbn() < rhs.isbn();
}

对应的自定义操作的 multiset 的定义形式如下

// bookstore 中多条记录可以相同的ISBN
// bookstore 中的元素以ISBN的顺序进行排序
multiset<Sales_data, decltype(compareIsbn)*> bookstore(compareIsbn);

此处,使用 decltype 来指出自定义操作的类型。当用 decltype 来获得一个函数指针类型时,必须加上一个 * 来只指出我们要使用一个给定函数类型的指针。用 compareIsbn 来初始化 bookstore 对象,这表示我们向 bookstore 添加元素时,通过调用 compareIsbn 来为这些元素排序。

2.3 pair 类型

pair 定义在头文件 utility 中,一个pair 保存两个数据成员,分别是 fistsecond ,这两个成员是公有的,我们可以直接访问。pair 是一个用来生成特定的模板。当创建一个 pair 时,我们必须体统两个类型名,pair 的数据成员将具有对应的类型:

pair<string, string> anon;		// 保存两个 string
pair<string, size_t> word_count;	// 保存一个 string 和一个 size_t
pair<string, vector<int>> line;		// 保存string 和 vector<int>

pair 的默认构造函数对数据成员进行值初始化,我们也可以为每个成员提供初始化器:

pair<string, string> author{"James", "Joyce"};

pair 的成员是公有的,对于之前的单词计数程序的输出语句我们可以这样做:

// 打印结果
cout << w.first << " occurs " << w.second 
    << ((w.second > 1) ? " times" : " time") << endl;

此处, w 是指向 map 中某个元素的引用。map的元素是 pair。first成员是保存的 key, second 保存的是 value。下面是 pair 支持的一些操作

image-20221217205721045

创建 pair 对象的函数

一个函数需要返回一个 pair,在新标准下,我们可以对返回值进行列表初始化

pair<string, int>
process(vector<string>& v) {
    // 处理 v
    
    if (!v.empty()) 
        return {v.back(), v.bcak().size()};		// 列表初始化
    else
        return pair<string, int>();		// 隐式构造返回值
}

在较早的 C++ 版本中,不允许花括号包围的初始化来返回pair,必须显示构造返回值

if (!v.empty()) 
    return pair<string, int>(v.back(), v.back.size());
// 另一种方式
if (!v.empty()) 
    return make_pair(v.back(), v.back.size());

3. 关联容器操作

除了表9.2中列出的类型, 关联容器还定义了下表中列出的类型

image-20221219125521105

对于set类型,key_typevalue_type 是一样的; set 中保存的value就是 key。在一个 map中,元素是key-value 对。由于我们不能改变一个元素的key,因此这些key部分是const 的:

set<string>::value_type vl;			// v1 是一个string
set<string>::key_type v2;			// v2 是一个 string
map<string, int>::value_type v3;	// v3 是一个pair<const string, int>
map<string, int>::key_type v4;		// v4 是一个 string
map<string, int>::mapped_type v5;	// v5 是一个 int

只有 map 类型(unordered_map、unordered_multimap、multimap 和 map)才定义 mapped_type

3.1 关联迭代器

当解引用一个关联容器迭代器时,我们会得到一个类型为容器器 value_type 的值的引用,(map的value_type 是 pair,set 的是 key(value) 的类型):

// 获得指向 word_count 中一个元素的迭代器
auto map_it = word_count.begin();
// *map_it 是一个 pair<const string, size_t> 对像的引用
cout << map_it->first;		// 打印此元素的key
cout << " " << map_it -> second;	// 打印此元素的value
map_it->first = "new key";		// 错误,key 是const的
++map_it-second;				// 正确,

set 的迭代器是const 的

set<int> iset = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
set<int>::iterator set_it = iset.begin();
if (set_it != iset.end()) {
    *set_it = 42;		// 错误, set 中的key(value) 是只读的
	cout << *set_it < endl;		// 正确
}

关联容器和算法

我们通常不对关联容器使用泛型算法。关键字是const 这一特性意味着不能将关联容器传递给修改或重排容器元素的算法。

3.2 添加元素

关联容器的添加元素使用成员方法 insert

set 插入元素

vector<int> ivec = {2, 4, 6, 8, 2, 4, 6, 8}; 	// ivec 有8个元素
set<int> set2;		// 空 set
set2.insert(ivec.cbegin(), ivec.cend());		// set2 有 4个元素
set2.insert({1, 3, 5, 7, 1, 3, 5, 7});			// set2 有 8个元素

insert 有两个版本,分别是接受一对迭代器,或是初始化器列表,这两个版本的行为类似对应的构造函数——对于一个给定的关键字,只有一个带此关键字的元素才被插入到容器中。

向map添加元素

// 向 word_count 插入word的4种方法
word_count.insert({word, 1});
word_count.insert(make_pair(word, 1));
word_count.insert(pair<string, size_t>(word, 1));
word_count.insert(map<string. size_t>::value_type(word, 1));

map 的insert 的方法的四个版本都是需要传递一个 pair 类型的参数。

image-20221219135403709

检测 insert 的返回值

insert (或 emplace) 返回的值依赖于容器类型和参数。对于不含重复key的容器,添加一个元素的 insert 和 emplace 版本返回一个 pair,pair的 first 成员是一个迭代器,指向具有给定key的元素;second 成员是一个bool 值,指出元素是插入成功合适已经存在于容器中。如果key已在容器中,则 insert 什么事情也不做,且返回值的 bool 部分为false ,否则为 true

使用 insert 重写单词计数的程序

// 统计每个单词在输入中出现次数的一种更繁琐的写法
map<string, size_t> word_count> word_count;	
string word;
while(cin >> word) {
    // 插入一个元素, key 等于 word, value 为1
    auto ret = word_count.insert({word, 1});
    if (!ret.second) {		// word 已在 word_count 中
        ++ret.first->second;	// 递增计数器
    }
}

++ret.first->second; 展开

//  等价表达式
++((ret.first)->second);
// 展开解释
// ret   保存insert返回的值,是一个 pair 类型
// ret.first   是pair的第一个成员,是一个map的迭代器,指向具有给定key的元素
// ret.fist->  解引用此迭代器,提取map中的元素,元素是一个pair
// ret.first->second   map中元素的value部分
// ++ret.first->second;  递增此值

// ret 的声明和初始化,不使用auto
pair<map<string, size_t>::iterator, bool> ret =
     word_count.insert({word, 1});

multisetmultimap 添加元素

这两个容器的key是可以重复的,因此调用insert 总会插入一个元素,返回的是指向新元素的迭代器。

multimap<string, string> authors;
// 插入第一个元素, 关键字为 Barth, John
authors.insert({"Barth, John", "sot-Weed Factor"});
// 正确: 添加第二元素,关键字也是 Barth, John
authors.insert({"Barth, John", "Lost in the Funhouse"});

3.3 删除元素

关联容器定义了三个版本的 erase ,如下表。除了传递迭代器和迭代器对,还支持接受一个 key_type 的参数。传递迭代器的版本指定元素被删除,返回void。

image-20221220151706980

从 word_count 中删除一个特定的单词

// 删除一个关键字,返回删除元素的数量
if (word_count.erase(removal_word))
    cout << "ok: " << removal_word << " removed\n";
else cout << "oops: " << removal_word << " not found!\n"

3.4 map 的下标操作

mapunordered_map 容器提供了下标运算符和一个对应的 at 函数。set 不支持下标,因为 set 没有与关键字相关联的 value。不能对一个 multimap 或一个 unordered_multimap 进行下标操作,因为这些容器中可能有多个 value 与一个 key 相关联。

map <string, size_t> word_count;	// 空 map
// 插入一个key为Anna的元素,关联值进行值初始化; 然后将1 赋予它
word_count["Anna"] = 1;

将会执行如下操作:

  • 在 word_count 中搜索key 为 Anna 的元素,未找到
  • 将一个新的key-value 对插入就到 word_count 中。key 是 一个 const string,将保存 Anna。值进行值初始化,在本例中意味着值为 0
  • 提取出新的插入的元素,并将值 1 赋予它。

对一个map使用小标操作,其行为与数组或 vector 上的下标操作很不相同:使用一个不在容器中的关键字作为下标,会添加一个具有此关键字的元素到map中。

image-20221220152857223

使用下标操作的返回值

map 的下标运算符与我们用过的其他下标运算符的另一个不同之处是其返回类型。通常情况下,解引用一个车迭代器所返回的类型与下标运算符返回的类型是一样的。但对 map 则不然: 当对一个map进行下标操作是,会获得一个mapped_type 对象,即 value 类型的对象;但当解引用一个 map 的迭代器时,会得到一个 value_type 对象,即 pair 对象。

map 的下标运算符返回一个左值,所以可以读写

cout << word_count["Anna"];   // 用 Anna 作为下标提取元素的value
++word_count["Anna"];		  // 提取元素,将其增1
cout << word_count["Anna"];	 

与 vector 与 string 不同, map的下标运算符返回的类型与解引用map迭代器返回得到的类型不同。

3.5 访问元素

关联容器提供多种查找一个指定元素的方法,如下表

image-20221220155218012

image-20221220155234659

set<int> iset = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
iset.find(1);		// 返回一个迭代器,指向 key==1 的元素
iset.find(11);		// 返回一个迭代器,其值等于 iset.end()
iset.count(1);		// 返回1
iset.count(11);		// 返回0

对 map 使用 find 代替下标操作

对 map 和 unordered_map 类型,使用下标操作提取元素会有一个副作用:如果 key 不在map中,下标操作会插入一个给定 key 的元素。所以在我们的需求只是想知道对应元素是否存在map中的时候,应该使用 find

if (word_count.find("foobar") == word_count.end())
    cout << "foobar is not in the map" << endl;

在 multimap 或 multiset中查找元素

一个示例,给定一个从作者到著作题目的映射,我们可能想打印打印一个特定作者的所有著作。

string search_iterm("Alain de Botton");		// 要查找的作者
auto entries = authors.count(search_iterm);	//元素的数量
auto iter = authors.find(search_iterm);		// 此作者的第一本书
// 用一个循环查找此作者的所有著作
while(entries) {
    cout << iter->second << endl;
    ++iter;
    --entries;
}

一种不同的,面向迭代器的解决方法

我们还可以用 lower_boundupper_bound 来解决此问题。这两个操作接受一个关键字,返回一个迭代器。如果关键字在容器中, lower_bound 返回的迭代器将指向第一个具有给定关键字的元素,而 upper_bound 返回的迭代器则指向最后一个匹配给定关键字的元素之后的位置。如果元素不在 multimap 中,则 lower_boundupper_bound 会返回相等的迭代器——指向一个不影响排序的关键字插入位置。

// authors 和 search_iterm 的定义, 与前面的程序一样
// beg 和 end 表示对应此作者的元素的范围
for (auto beg = authors.lower_bound(search_item), 
    	  end = authors.upper_bound(search_item);
    beg != end; ++beg)
    cout << beg->second << endl;  // 打印每个题目

如果lower_bound 和 upper_bound 返回相同的迭代器,则给定关键字不在容器中。

equal_range 函数

此函数接受一个关键字,返回一个迭代器 pair,若关键字存在,则第一个迭代器指向第一个与关键字匹配的元素,第二个迭代器指向最后一个匹配元素之后的位置。若未找到匹配元素,则两个迭代器都指向关键字可以插入的位置。

// authors 和 search_item 的定义, 与前面的程序一样
// pos 保存迭代器对,表示与关键字匹配的元素范围
for (auto pos = authors.equal_range(search_item);
     pos.first != pos.second; ++pos.first)
    cout << pos.first->second << endl;  

3.6 一个单词转换的map

一个map 的实例

  • TODO

4. 无序容器

C++11 定义了4个无序关联容器,这些容器不是使用比较运算符来组织元素,而是使用哈希函数和关键字类型的 == 运算符。

使用无序容器

使用 unordered_map 重写最初的单词计数程序

unordered_map<string, size_t> word_count;		// string 到 size_t的空map
string word;
while(cin >> word)
    ++word_count[word];		// 提取word的计数器并将其加1
for (const auto &w : word_count)	// 对map中的每个元素 打印结果
    cout << w.first << " occurs " << w.second
    	 << ((w.second > 1) ? " times" : " time") << endl;

此程序与原程序的唯一区别就是 word_count 的类型。如果在相同的输入数据上运行此版本,会得到这样的输出:

containers. occurs 1 time
use occurs 1 time
can occurs 1 time
examples occurs 1 time

对于每个单词,我们将得到相同的计数结果。但单词不太可能按字典顺序输出。

管理桶

  • TODO,关于hash 这方面的知识还不了解,这部分内容后续在补全