C++ Primer 第十一章 关联容器

两个主要的关联容器:set 和 map
mapmultimap 定义在头文件 map 中;
setmultiset 定义在头文件 set 中;
无序容器定义在 unordered_mapunordered_set

按关键字有序保存元素
map 关联数组;保存关键字-值对
set 关键字即值,即只保存关键字的容器
multimap 关键字可重复出现的 map
multiset 关键字可重复出现的 set
无序集合
unordered_map 用哈希函数组织的 map
unordered_set 用哈希函数组织的 set
unordered_multimap 哈希组织的 map;关键字可以重复出现
unordered_multiset 哈希组织的 set;关键字可以重复出现

使用关联容器

  • map 类型通常被称为关联数组。关联数组与“正常”数组类似,不同之处在于其下标不必是整数。我们通过一个关键字而不是位置来查找值。
  • set 就是关键字的简单集合

使用 map

  • 当从 map 中提取一个元素时,会得到一个 pair 类型对象。
int main(int argc, char *argv[]) {
// 统计每个单词在输入中出现的次数
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;
}

使用 set

find 调用返回一个迭代器。如果给定关键字在 set 中,迭代器指向该关键字。否则,find 返回尾后迭代器。

int main(int argc, char *argv[]) {
// 统计每个单词在输入中出现的次数
map<string, size_t> word_count;
set<string> exclude = {"The", "But", "And", "Or", "An", "A",
"the", "but", "and", "or", "an", "a"};
string word;
while (cin >> word) {
if (exclude.find(word) == exclude.end())
++ 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; // 空容器
// 列表初始化
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"} };

初始化 multimap 或 multiset

  • 允许多个元素具有同样的关键字
int main(int argc, char *argv[]) {
vector<int> ivec;
for (vector<int>::size_type i = 0; i != 10; ++ i) {
ivec.push_back(i);
ivec.push_back(i);
}
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
return 0;
}

关键字类型的要求

  • 对于有序容器,关键字类型必须定义元素比较的方法
  • 默认情况下,标准库使用关键字类型的 < 运算符来比较两个关键字

有序容器的关键字类型

可以提供自己自定义的操作来代替关键字上的 < 运算符。所提供的操作必须在关键字类型上定义了一个严格弱序。

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

如果两个关键字是等价的,那么容器将它们视作相等来处理。
当用作 map 的关键字时,只能有一个元素与这两个关键字关联,我们可以用两者中任意一个来访问对应的值

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

  • 为了指定使用自定义的操作,必须在定义关联容器类型时提供此操作的类型。如前所述,用尖括号指出要定义哪种类型的容器,自定义的操作类型必须在尖括号中紧跟着元素类型给出。

例:

  • 为了使用自己定义的操作,在定义 multiset 时我们必须提供两个类型:关键字类型 Sales_data,以及比较操作类型——应该是一种函数指针类型。
bool compareIsbn(const Sales_data &lhs, const Salse_data &rhs) {
return lhs.isbn() < rhs.isbn();
}
// bookstore 中多条记录可以有相同的 ISBN
// bookstore 中的元素以 ISBN 的顺序进行排列
multiset<Sales_data, decltype(compareIsbn)*> bookstore(compareIsbn);

使用 compareIsbn 来初始化 bookstore 对象,这表示当我们向 bookstore 添加元素时,通过调用 compareIsbn 来为这些元素排序。
即,bookstore 中的元素将按它们的 ISBN 成员的值排序。可以用 compareIsbn 代替 &compareIsbn 作为构造函数的参数,因为我们使用一个函数的名字时,在需要的情况下它会自动转化为一个指针。当然,使用 &compareIsbn 的效果也是一样的。

pair 类型

  • 它定义在头文件中 utility
  • pair 的默认构造函数对数据成员进行值初始化。
pair<string, string> anon; // 保存两个 string, 空 空
pair<string, size_t> word_count; // 保存一个 string 和一个 size_t, 空 0
pair<string, vector<int>> line; // 保存 string 和 vector<int> 空 空
  • 我们也可以为每个成员提供初始化器
pair<string, string> author{"James", "Joyce"};
  • pair 的数据成员是 public 的。两个成员分别命名为 first 和 second。我们用普通的成员访问符号来访问它们
pair<T1, T2> p; p 是一个 pair,两个类型分别为 T1 和 T2 的成员都进行了值初始化
pair<T1, T2> p(v1, v2); p 是一个 成员类型为 T1 和 T2 的 pair;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 的名为 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 当 first 和 second 成员分别相等时,两个 pair 相等。相等性判断利用元素的 == 运算符实现
p1 != p2

创建 pair 对象的函数

pair<string, int> process(vector<string> &v) {
if (!v.empty()) return {v.back(), v.back().size()}; // 列表初始化
else return pair<string, int> (); // 隐式构造返回值
// 在较早的 C++ 版本中,不允许用花括号包围的初始化器来返回 pair 这种类型的对象,必须显式构造返回值
if (!v.empty()) return pair<string, int> (v.back(), v.back().size());
// 我们还可以用 make_pair 来生成 pair 对象,pair 的两个类型来自于 make_pair 的参数
if (!v.empty()) return make_pair(v.back(), v.back().size());
}

关联容器操作

关联容器额外的类型别名

key_type 此容器类型的关键字类型
mapped_type 每个关键字关联的类型;只适用于 map
value_type 对于 set,与 key_type 相同
对于 map,为 pair<const key_type, mapped_type>
  • 对于 map,由于我们不能改变一个元素的关键字,因此这些 pair 的关键字部分是 const 的
  • 只有 map 类型(unordered_map, unordered_multimap, multimap 和 map)才定义了 mapped_type
set<string>::value_type v1; // 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

关联容器迭代器

  • 当解引用一个关联容器迭代器时,我们会得到一个类型为容器的 value_type 的值的引用。
auto map_it = word_count.begin();
// map_it 是指向一个 pair<const string, size_t> 对象的引用
cout << map_it -> first;
cout << " " << map_it -> second;
map_it -> first = "new 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; // 错误:sest 中的关键字是只读的
cout << *set_it << endl;
}

遍历关联容器

  • 当使用一个迭代器遍历一个 map、multimap、set 或 multiset 时,迭代器按关键字升序遍历元素
auto map_it = word_count.cbegin();
while (map_it != word_count.cend()) {
cout << map_it -> first << " occurs "
<< map_it -> second << " times" << endl;
++ map_it;
}

关联容器和算法

添加元素

  • insert 有两个版本,分别接受一对迭代器,或是一个初始化列表
vector<int> ivec = {2, 4, 6, 8, 2, 4, 6, 8};
set<int> set2;
set2.insert(ivec.begin(), ivec.end());
set2.insert({1, 3, 5, 7, 1, 3, 5, 7});

向 map 添加元素

  • 对一个 map 进行 insert 操作时,必须记住元素类型是 pair。
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));

关联容器 insert 操作

c.insert(v) v 是 value_type 类型的对象;args 用来构造一个元素
c.emplace(args) 对于 map 和 set,只有当元素的关键字不在 c 中时才插入(或构造)
元素。函数返回一个 pair,包含一个迭代器,指向具有指定关键字
的元素,以及一个指示插入是否成功的 bool 值。
对于 multimap 和 multiset,总会插入(或构造)给定元素,并
返回一个指向新元素的迭代器。
c.insert(b, e) b 和 e 是迭代器,表示一个 c::value_type 类型值的范围;il 是
这种值的花括号列表。函数返回 void
对于 map 和 set,只插入关键字不在 c 中的元素。对于 multimap
和 multiset,则会插入范围中的每个元素
c.insert(p, v) 类似 insert(v)(或 emplace(args)),但将迭代器 p 作为一个提
示。指出从哪里开始搜索新元素应该存储的位置。返回一个迭代器,
指向具有给定关键字的元素

检测 insert 的返回值

while (cin >> word) {
auto ret = word_count.insert({word, 1});
if (!ret.second) // 如果元素已经在容器中,会返回 false
++ ret.first -> second;
}

展开递增语句 (++ ret.first -> second)

添加一些括号后

++ ((ret.first) -> second);

一步一步解释此表达式

ret 保存 insert 返回的值,是一个 pair
ret.first 是 pair 的第一个成员,是一个 map 迭代器,指向具有给定关键字的元素
ret.first -> 解引用此迭代器,提取 map 中的元素,元素也是一个 pair
ret.first -> second map 中元素的值部分
++ ret.first -> second 递增此值

如果使用旧版本的编译器,ret 的声明和初始化会很复杂

pair<map<string, size_t>::iterator, bool> ret = word_count.insert(make_pair(word, 1));

向 multiset 或 multimap 添加元素

场景:建立作者到他所著书籍题目得到映射
由于一个 multi 容器中的关键字不必唯一,在这些类型上调用 insert 总会插入一个元素:

multimap<string, string> authors;
// 插入第一个元素,关键字为 "Barth, John"
authors.insert({"Barth, John", "Sot-Weed Factor"});
// 正确:添加第二个元素,关键字也是 Barth, John
authors.insert({"Barth, John", "Lost in the Funhouse"});
  • 这里无须返回一个 bool 值,因为 insert 总是向这类容器中加入一个新元素

删除元素

  • 关联容器定义了三个版本的 erase:传递迭代器、迭代器对(返回 void)和 接受 key_type 参数(返回删除元素数量)
  • 接受 key_type 的版本删除所有匹配给定关键字的元素(如果存在的话)
// 删除一个关键字,返回删除的元素数量
if (word_count.erase(removal_word)) cout << "ok: " << removal_word << " removed\n";
else cout << "oops: " << removal_word << " not found!\n";
// 对允许重复关键字的容器,删除元素的数量可能大于 1
auto cnt = authors.erase("Barth, John");

从关联容器删除元素

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

map 的下标操作

  • map 和 unordered_map 容器提供了下标运算符和一个对应的 at 函数

  • set 类型不支持下标,因为 set 中没有与关键字相关联的“值”

  • 不能对 multimap 或一个 unordered_multimap 进行下标操作,因为这些容器中可能有多个值与一个关键字相关联

  • 与下标运算符不同的是,如果关键字并不在 map 中,会为其创建一个元素并插入到 map 中,关联值将进行值初始化

map<string, size_t> word_count; // empty map
word_count["Anna"] = 1;

由于下标运算符可能插入一个新元素,我们只可以对非 const 的 map 使用下标操作

c[k] 返回关键字为 k 的元素:如果 k 不在 c 中,添加一个关键字为 k 的元素,对其进行值初始化
c.at(k) 访问关键字为 k 的元素,带参数检查;若 k 不在 c 中,抛出一个 out_of_range 异常

使用下标操作的返回值

  • 当对一个 map 进行下标操作时,会获得一个 mapped_type 对象;但当解引用一个 map 迭代器时,会得到一个 value_type 对象
  • map 的下标运算符返回一个左值,因此我们可以读也可以写元素
cout << word_count["Anna"]; // 1
++ word_count["Anna"];
cout << word_count["Anna"]; // 2

有时只是想知道一个元素是否已在 map 中,但在不存在时并不想添加元素。在这种情况下,就不能使用下标运算符

访问元素

在一个关联容器中查找元素的操作

lower_bound 和 upper_bound 不适用于无序容器
下标和 at 操作只适用于非 const 的 map 和 unordered_map。
c.find(k) 返回一个迭代器,指向第一个关键字为 k 的元素,若 k 不在容器中,则返回尾后迭代器
c.count(k) 返回关键字等于 k 的元素的数量。对于不允许重复关键字的容器,返回值永远是 01
c.lower_bound(k) 返回一个迭代器,指向第一个关键字不小于 k 的元素
c.upper_bound(k) 返回一个迭代器,指向第一个关键字大于 k 的元素
c.equal_range(k) 返回一个迭代器 pair,表示关键字等于 k 的元素的范围。若 k 不存在,pair 的两个成员均等于 c.end()

对 map 使用 find 代替下标操作

  • 有时,我们只是想知道一个给定关键字是否在 map 中,而不想改变 map。这种情况下,应该使用 find
if (word_count.find("foobar") == word_count.end()) cout << "foobar is not int the map" << endl;

在 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;
}

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

for (auto beg = authors.lower_bound(search_item), end = authors.upper_bound(search_item); beg != end; ++ beg) {
cout << beg -> second << endl;
}

equal_range 函数

for (auto pos = authors.equal_bound(search_item); pos.first != pos.second; ++ pos.first) {
cout << pos.first -> second << endl;
}

一个单词转换的 map

单词转换文件

brb be right back
k okay?
y why
r are
u you
pic picture
thk thanks!
l8r later

转换的文本

where r u
y dont u send me a pic
k thk l8r

整体代码

//#define NDEBUG
#include <iostream>
#include <string>
#include <vector>
#include <cstring>
#include <cstddef>
#include <iterator>
#include <stdexcept>
#include <initializer_list>
#include <cstdlib>
#include <cassert>
#include <fstream>
#include <sstream>
#include <list>
#include <deque>
#include <forward_list>
#include <array>
#include <stack>
#include <numeric>
#include <algorithm>
#include <functional>
#include <map>
#include <set>
#include <unordered_map>
#include <unordered_set>
#include <utility>
//#include "Chapter6.h"
//#include "Sales_data.h"
//#include "Person.h"
//#include "Screen.h"
//#include "Debug.h"
//#include "Account.h"
//#include "Sales_item.h"
using std::cin; using std::cout; using std::endl; using std::cerr;
using std::vector; using std::string;
using std::begin; using std::end;
using std::ends; using std::flush;
using std::ifstream; using std::ofstream; using std::istringstream; using std::ostringstream;
using std::list; using std::deque; using std::forward_list; using std::array; using std::stack;
using std::map; using std::set; using std::multimap; using std::multiset;
using std::unordered_map; using std::unordered_set; using std::unordered_multimap; using std::unordered_multiset;
using std::pair;
using namespace std::placeholders;
map<string, string> buildMap(ifstream &map_file) {
map<string, string> trans_map;
string key;
string value;
while (map_file >> key && getline(map_file, value)) {
if (value.size() > 1) trans_map[key] = value.substr(1); // 跳过前导空格
else throw std::runtime_error("no rule for " + key);
}
return trans_map;
}
const string &transform(const string &s, const map<string, string> &m) {
auto map_it = m.find(s);
if (map_it != m.end()) return map_it -> second;
else return s;
}
void word_transform(ifstream &map_file, ifstream &input) {
auto trans_map = buildMap(map_file);
string text;
while (getline(input, text)) {
istringstream stream(text);
string word;
bool firstword = true;
while (stream >> word) {
if (firstword) firstword = false;
else cout << " ";
cout << transform(word, trans_map);
}
cout << endl;
}
}
int main(int argc, char *argv[]) {
ifstream map_file("file1"), input("file2");
word_transform(map_file, input);
return 0;
}

无序容器

  • 使用一个哈希函数和关键字类型的 == 运算符

使用无序容器

除了哈希管理操作之外,无序容器还提供了与有序容器相同的操作(find、insert等)。
由于元素未按顺序存储,一个使用无序容器的程序的输出(通常)会与使用有序容器的版本不同。

管理桶

无序容器在存储上组织为一组桶,每个桶保存零个或多个元素。无序容器使用一个哈希函数将元素映射到桶(类似拉链法)

桶接口
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 会在需要时添
加新的桶,以使得 load_factor <= max_load_factor
c.rehash(n) 重组存储,使得 bucket_count >= n 且 bucket_count > size / max_load_factor
c.reserve(n) 重组存储,使得 c 可以保存 n 个元素且不必 rehash

无序容器对关键字类型的要求

  • 我们可以直接定义关键字是内置类型(包括指针类型)、string 还是智能指针类型的无序容器
  • 不能直接定义关键字类型为自定义类类型的无序容器。与容器不同,不能直接使用哈希模板,而必须提供我们自己的 hash 模板版本

我们不使用默认的 hash,而是使用另一种方法,类似于为有序容器重载关键字类型的默认比较函数。为了能将 Sales_data 用作关键字,我们需要提供函数来替代 == 运算符和哈希值计算函数。

size_t hasher(const Sales_data &sd) {
return std::hash<string>()(sd.isbn());
}
bool eqOp(const Sales_data &lhs, const Sales_data &rhs) {
return lhs.isbn() == rhs.isbn();
}
using SD_multiset = unordered_multiset<Sales_data, decltype(hasher)*, decltype(eqOp)*>;
SD_multiset bookstore(42, hasher, eqOp);

如果我们的类定义了 == 运算符,则可以只重载哈希函数

unordered_set<Foo, decltype(FooHash)*> fooSet(10, FooHash);
posted @   HuiPuKui  阅读(7)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
点击右上角即可分享
微信分享提示