两个主要的关联容器:set 和 map
map
和 multimap
定义在头文件 map
中;
set
和 multiset
定义在头文件 set
中;
无序容器定义在 unordered_map
和 unordered_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"}; |
| |
| 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; |
| cout << iset.size() << endl; |
| cout << miset.size() << endl; |
| |
| 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(); |
| } |
| |
| |
| |
| multiset<Sales_data, decltype(compareIsbn)*> bookstore(compareIsbn); |
使用 compareIsbn 来初始化 bookstore 对象,这表示当我们向 bookstore 添加元素时,通过调用 compareIsbn 来为这些元素排序。
即,bookstore 中的元素将按它们的 ISBN 成员的值排序。可以用 compareIsbn 代替 &compareIsbn 作为构造函数的参数,因为我们使用一个函数的名字时,在需要的情况下它会自动转化为一个指针。当然,使用 &compareIsbn 的效果也是一样的。
pair 类型
- 它定义在头文件中
utility
中
- pair 的默认构造函数对数据成员进行值初始化。
| pair<string, string> anon; |
| pair<string, size_t> word_count; |
| pair<string, vector<int>> line; |
| 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> (); |
| |
| |
| if (!v.empty()) return pair<string, int> (v.back(), v.back().size()); |
| |
| 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; |
| set<string>::key_type v2; |
| map<string, int>::value_type v3; |
| map<string, int>::key_type v4; |
| map<string, int>::mapped_type v5; |
关联容器迭代器
- 当解引用一个关联容器迭代器时,我们会得到一个类型为容器的 value_type 的值的引用。
| auto map_it = word_count.begin(); |
| |
| cout << map_it -> first; |
| cout << " " << map_it -> second; |
| map_it -> first = "new key"; |
| ++ 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; |
| 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) |
| ++ 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; |
| |
| authors.insert({"Barth, John", "Sot-Weed Factor"}); |
| |
| 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"; |
| |
| |
| 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; |
| 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"]; |
| ++ word_count["Anna"]; |
| cout << word_count["Anna"]; |
有时只是想知道一个元素是否已在 map 中,但在不存在时并不想添加元素。在这种情况下,就不能使用下标运算符
访问元素
在一个关联容器中查找元素的操作
| lower_bound 和 upper_bound 不适用于无序容器 |
| 下标和 at 操作只适用于非 const 的 map 和 unordered_map。 |
| |
| 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() |
对 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 |
整体代码
| |
| #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> |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| 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); |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!