第十一章 关联容器

66 阅读9分钟

关联容器与顺序容器有着根本的不同:关联容器中的元素按关键字保存和访问,顺序容器中的元素按它们在容器中的位置来顺序保存和访问。

关联容器支持高效地关键字查找和访问。以下是两个主要的关联容器(associative-container)类型:

  • map

    元素是一个键-值(key-value)对,关键字作为索引,值则表示与索引关联的数据。

  • set

    元素只是一个关键字,支持高效地关键字查询。

标准库提供了 8 个关联容器,它们的区别体现在:

  1. set 还是 map
  2. 是否允许关键字重复,容器名中的 multi 表示允许关键字重复。
  3. 顺序存储还是无序存储,以 unordered 开头的表示非顺序存储。

associative-container.png

  • mapmultimap 定义在头文件 map 中。
  • setmultiset 定义在头文件 set 中。
  • unordered_mapunordered_multimap 定义在头文件 unordered_map 中。
  • unordered_setunordered_multiset 定义在头文件 unordered_set 中。

存储键-值对的 map 类型通常也被称为关联数组(associative array),只不过其下标不必是整数,而是关键字。而 set 类型则只是关键字的简单集合。

关联容器概述

表 9.2 中的普通容器操作,关联容器都支持,但不支持顺序容器位置相关的操作,比如 push_frontpush_back,也不支持通过 “元素个数 + 元素值” 的构造或插入操作。

关联容器的迭代器都是双向的。

定义关联容器

  • 定义 map 时,必须同时指明关键字类型和值类型。
  • 定义 set 时,只需指明关键字类型。

关联容器构造函数支持:

  • 默认初始化。
  • 同类型容器的拷贝初始化。
  • 可转为容器所需类型的值范围初始化。

关联容器也是模板。

map<string, size_t> word_count;
set<string> exclude = { "the", "but" };
map<string, string> authors = {
  { "Karl", "Marx" },
  { "Tom", "Jerry" }
};

列表初始化 map 时,需要将键-值对包在一起,来表示 map 中的一个元素:

{ key, value }

容器 mapset 的关键字必须唯一,容器 multimapmultiset 允许关键字重复。

关键字类型的要求

传给排序算法的可调用对象,必须满足和关联容器中关键字一样的类型要求。

关联容器对其关键字类型有一定限制。对于有序容器 mapmultimapsetmultiset,关键字类型必须定义元素比较的方法。标准库默认使用 < 运算符进行比较。也可以自定义操作来代替 <。该操作必须在关键字类型上定义一个严格弱序 \leqslant(strict weak ordering):

  • 对于两个关键字 k1k_{1}k2k_{2}k1k2k2k1k_{1}\leqslant k_{2}\Rightarrow k_{2}\nleqslant k_{1}
  • k1k2k2k3k1k3k_{1}\leqslant k_{2}、k_{2}\leqslant k_{3}\Rightarrow k_{1}\leqslant k_{3}
  • k1k2k_{1}\nleqslant k_{2}k2k1k_{2}\nleqslant k_{1},则称两者等价 k1k2k_{1}\sim k_{2};而且 \sim 必须满足 k1k2k2k3k1k3k_{1}\sim k_{2}、k_{2}\sim k_{3}\Rightarrow k_{1}\sim k_{3}

容器将两个等价的关键字视为相等,用作 map 关键字时,只能有一个元素与这两个关键字关联,可以用两者中任一个访问相应的值。

如果一个类型定义了 “行为正常” 的 <,则可以用作关键字类型。

用于组织容器中元素的操作类型也是该容器类型的一部分。为了自定义操作,必须在定义关联容器类型时提供操作类型。自定义操作类型必须紧跟着元素类型给出。创建容器时,还需以构造函数参数的形式提供真正的比较操作,其类型必须与指定的操作类型相符。

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

multiset<Sales_data, decltype(compareIsbn)*> bookstore(compareIsbn);

pair 类型

头文件 utility 中的标准库类型 pair 也是模板,创建 pair 时需要为它的两个数据成员分别提供类型名。pair 的默认构造函数对数据成员进行值初始化。pair 的两个数据成员 firstsecond 均为 public

pair<string, string> anon;
pair<string, string> author{"Tom", "Jerry"};
pair<string, vector<int>> anon1;

pair.png

关联容器操作

除了普通容器上定义的类型之外,关联容器还定义了关键字和值的类型。

type.png

set<string>::value_type v1; // string
set<string>::key_type v2; // string

map<string, int>::value_type v3; // pair<const string, int>
map<string, int>::key_type v4; // string
map<string, int>::mapped_type v5; // int

map 关键字不能修改,因此 value_type 中关键字为 const key_type 类型。

关联容器迭代器

解引用关联容器迭代器得到的是一个对类型 value_type 的引用。

auto map_it = word_count.begin();
cout << map_it->first;
++map_it->second;

set 类型的关键字也不能修改,因此 iteratorconst_iteratorset 中的元素都是只读的。

set<int> iset = { 0, 1, 2, 3 };
auto set_it = iset.begin();
*set_it = 1; // error

while (set_it != iset.end()) {
  cout << *set_it++ << endl;
}

当使用迭代器遍历一个 mapmultimapsetmultiset 时,迭代器按关键字升序遍历元素。

通常不会对关联容器使用泛型算法。关键字是 const 意味着不能使用修改或重排元素的算法。关联容器虽然可用于只读元素的算法,但这些算法大多需要搜索序列,而不通过关键字快速查找。实践中,对关联容器使用算法时,要么将它作为源序列,要么作为目标位置。

添加元素

关联容器的 insert 成员可以向容器添加一个元素或一个元素范围。关键字不允许重复的容器,只有第一个带此关键字的元素才能被插入容器。

map 的元素类型是 pair

vector<int> ivec = { 2, 4, 6, 2, 4, 6 };
set<int> set2;
set2.insert(ivec.cbegin(), ivec.cend());
set2.insert({ 1, 3, 5, 1, 3, 5 });

map<string, size_t> word_count;
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.png

insert/emplace 返回值依赖于容器类型和参数。对于不允许重复关键字的容器,添加单个元素的 insert/emplace 返回 pair 类型;它的 first 成员是一个指向具有给定关键字的元素的迭代器,second 成员是一个表示是否插入成功的 bool 值;如果关键字已存在,则 second 成员值为 false,否则为 true

map<string, size_t> word_count;

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

multi 容器上调用 insert 总会插入元素,因此插入单个元素的 insert 返回一个指向新元素的迭代器。

删除元素

除了接受一个迭代器或一个迭代器对来删除一个元素或一个元素范围之外,关联容器还提供一个额外的 erase 操作,接受一个 key_type 参数,删除所有匹配给定关键字的元素,返回实际删除的元素数量。

erase.png

map 下标操作

mapunordered_map 容器提供了下标运算符和 at 函数。如果关键字不在 map 中,下标运算会创建一个元素并插入 map 中,关联值将会值初始化;介于此,只能对非 constmap 使用下标。下标运算的返回结果是一个 mapped_type 类型的左值对象。

set 类型没有与关键字相关联的值,因此不支持下标。

multimapunordered_multimap 中可能存在多个值与一个关键字关联,因此不支持下标。

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

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

index.png

访问元素

如果不需要计数,只想知道一个特定元素是否在容器中,最好用 find

set<int> iset = { 0, 1, 2, 3, 4 };
iset.find(1);
iset.count(1);

find.png find-1.png

对于 mapunordered_map 容器,下标运算符有自动插入元素的副作用,如果不想改变 map,就应该使用 find

multimapmultiset 中有多个元素具有相同关键字时,这些元素在容器中会相邻存储。所有具有给定关键字的元素可以通过 countfind 来获取,也可以使用 lower_boundupper_bound 来获取,或者直接通过 equal_range

lower_boundupper_bound 都接受一个关键字,返回一个迭代器。

  • 如果关键字在容器中,lower_boundupper_bound 返回值构成一个迭代器范围,它包含所有具有该关键字的元素。
  • 如果关键字不在容器中,lower_boundupper_bound 返回值相等,指向一个不影响容器中元素顺序的给定关键字的插入位置。

lower_boundupper_bound 都可能返回尾后迭代器。

multimap<string, string> authors;
string search_item = "marx";

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

equal_range 接受一个关键字,返回一个迭代器 pair,它的 firstsecond 成员与 lower_boundupper_bound 返回值相同。

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

无序容器

无序关联容器(unordered associative container)使用哈希函数(hash function)和关键字类型的 == 运算符来组织元素,而不是比较运算符,常用于以下情况:

  • 关键字类型没有明显的序关系;
  • 维护元素的序代价很高。

如果关键字类型本身就是无序的,或者性能测试发现问题可以用哈希技术解决,就可以使用无序容器。

除了哈希管理操作之外,无序容器还提供了与有序容器相同的 findinsert 等操作,而且也有允许关键字重复的版本。因此,通常无序容器和有序容器可以互相替换。但由于没有按顺序存储,无序容器的输出和有序容器不同。

管理桶

无序容器的存储用一组桶来组织,每个桶可以保存多个元素。元素通过一个哈希函数映射到一个桶,相同参数的哈希函数总是产生相同结果,哈希值相同的所有元素保存在一个桶中。要访问一个元素,首先计算它的哈希值,然后对桶中的元素顺序搜索。无序容器的性能依赖于哈希函数的质量、桶的数量和大小。

无序容器提供了一组管理桶的函数,这些成员函数可以查询容器的状态以及在必要时强制容器重组。

unordered-container.png

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

默认情况下,无序容器使用关键字类型的 == 运算符来比较元素,使用 hash<key_type> 类型的对象来生成哈希值。标准库为包括指针在内的内置类型、string 类型、智能指针类型提供了 hash 模板,因此可以直接定义关键字为这些类型的无序容器。

关键字为自定义类类型的无序容器,必须提供自定义的 hash 模板,或者提供函数代替 == 运算符和哈希函数。

size_t hasher(const Sales_data &sd) {
  return 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);

如果类已经定义了 == 运算符,则可以只提供哈希函数。