《C++ Primer》第11章 关联容器

参考资料:

  • 《C++ Primer》第5版
  • 《C++ Primer 习题集》第5版

关联容器支持高效关键字查找和访问,两个主要的关联容器是 mapsetmap 中的元素是键-值( key value )对set 中的元素只包含一个关键字。

标准库提供 8 个关联容器:

c1bb87d4899db8488ee0fe08d9b3f9e

容易看出,上面的 8 个关联容器的不同点主要体现在 3 个维度上:

  1. 或者是 set ;或者是 map
  2. 或者要求不重复关键字;或者允许重复关键字,以 multi 开头
  3. 或者按顺序保存元素;或者无序保存,以 unordered_ 开头

mapmultimap 定义在头文件 map 中;setmultiset 定义在头文件 set 中;无序容器则分别定义在头文件 unordered_mapunordered_set 中。

11.1 使用关联容器(P374)

map 通常被称为关联数组( associative array ),可以通过关键字来查找值;set 可以用来判断某个值是否存在。

使用map

一个经典的使用 map的例子是单词计数程序:

map<string, size_t> word_count;
string word;
while (cin >> word) {
	++word_count[word];
}
for (const auto &w : word_count) {
	cout << w.first << ' ' << w.second << endl;
}

map 也是模板,在定义时必须指定关键字和值的类型。在 while 循环中,如果关键字 wordword_count 中还不存在,下标运算符会创建一个新元素,其关键字为 word ,值为 0 。当从 map 中提取一个元素时,会得到一个 pair 对象,其 first 成员保存关键字,second 成员保存值。

使用set

如果我们在上述程序的基础上,想要忽略一些常见单词,可以使用 set

// 将需要忽略的单词保存在set中
set<string> exclude = { "the","and","a","an" };
// 判断当前单词word是否需要忽略
if(exclude.find(word) == exclude.end())
    ...

11.2 关联容器概述(P376)

关联容器(有序和无序)支持大多数普通容器的操作,除了:

  • 和位置相关的操作,如 push_backpush_front
  • 接受一个元素值和一个数量值的构造函数和插入操作。

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

11.2.1 定义关联容器(P376)

每个关联容器都定义了一个默认构造函数,它创建一个空容器;我们也可以将关联容器初始化为另一个同类型容器的拷贝;也可以对关联容器进行值初始化:

map<string, string> authors = {
	{"Joyce", "James"},
	{"Austen", "Jane"}
};

初始化multimapmultiset

vector<int> vi;
// 定义一个含有20个元素的vector
for (int i = 0; i < 10; ++i) {
	vi.push_back(i);
	vi.push_back(i);    // 重复一次
}
set<int> si(vi.cbegin(), vi.cend());    // si的size为10
multiset<int> msi(vi.cbegin(), vi.cend());    // msi的size为20

11.2.2 关键字类型的要求(P378)

对于有序容器,关键字类型必须定义元素比较的方法,默认使用 < 运算符。

有序容器的关键字类型

我们可以提供自己定义的操作来替换关键字类型的 < 运算符,所提供的操作必须在关键字类型上定义一个严格弱序(strict weak ordering)。严格弱序可以看作“小于”,具有如下基本性质:

  • 两个关键字不能同时“小于”对方;
  • 如果 a “小于” b 且 b “小于” c ,那么 a “小于” c ;
  • 如果两个关键字都不“小于”对方,则这两个关键字等价

如果两个关键字是等价的,那么容器将它们视作相等

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

为了指定使用自定义操作,我们必须在定义关联容器类型时提供操作的类型,操作类型在尖括号中紧跟着元素类型给出:

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

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

此处,我们使用 decltype 来指出自定义操作的类型,用 compareIsbn 来初始化 bookstore 对象,表明当我们向 bookstore 中添加元素时,调用 compareIsbn 来为这些元素排序。

11.2.3 pair类型(P379)

pair类型定义在头文件 utility 中。一个 pair 保留两个数据成员:

pair<string, string> anon;
pair<string, size_t> word_count;

pair 的默认构造函数对数据成员进行值初始化pair 的数据成员是 public 的,两个成员分别命名为 firstsecondpair 支持的操作如下:

2d633684fee8d2c54767091fb3cd7d4

创建pair对象的函数

pair<string, size_t> process(vector<string> &v) {
	if (!v.empty()) {
		return { v.back(), v.back().size() };
	}
	else {
		return {};
	}
}

上面的代码中,我们使用了初始值列表来返回 pair 。此外,我们还可以显式构造返回值或者使用 make_pair 函数:

return pair<string, size_t>(v.back(), v.back().size());

return make_pair(v.back(), v.back().size())

11.3 关联容器操作(P381)

关联容器还定义了一些类型别名

7db39818b8341c2e441ab334da43ad2
set<string>::value_type v1;    // string
set<string>::key_type v2;    // string
map<string, int>::value_type v3;    // pair<const stirng, int>
map<string, int>::key_type v4;    // string
map<string, int>::mapped_type v5;    // int

11.3.1 关联容器迭代器(P382)

解引用关联容器迭代器时,会得到一个 value_type 的引用:

auto map_it = word_count.begin();
map_it->first = "new key";    // 错误,关键字是const
map_it->second = 0;    // 正确

set的迭代器是const

set 类型同时定义了 iteratorconst_iterator ,但两种类型都只允许只读访问 set 中的元素:

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

遍历关联容器

auto map_it = word_count.cbegin();
while (map_it != word_count.end()) {
	cout << map_it->first << ' ' << map_it->second;
	++map_it;
}

本程序的输出是按照字典序排列的。

关联容器和算法

我们通常不对关联容器使用泛型算法。在实际编程中,如果我们真要对一个关联容器使用算法,要么将它当成一个源序列,要么当作一个目的位置。例如,使用 copy 算法将关联容器中的内容拷贝到另一个序列。

练习

c95a5aa0961e88ce71564d030a723fd

只有第二个调用是错误的,因为 back_inserter 是通过容器的 push_back 成员实现元素插入的,而关联容器没有 push_back 成员。

11.3.2 添加元素(P383)

向不允许重复关键字的容器(如 mapset )插入一个已经存在的关键字,对容器没有任何影响

vector<int> vi = { 0,1,2,3 };
set<int> s;
s.insert(vi.cbegin(), vi.cend());
s.insert({ 0,1,2 });
s.insert(4);    // 只添加一个元素也是可以的

map添加元素

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));
55c0d8b739520b29b72fae0799bda88

检测insert的返回值

对于不允许重复关键字的关联容器,添加单一元素的 insertemplace 返回一个 pairpairfirst 成员是一个迭代器,指向具有给定关键字的元素second 成员是一个 bool 值,指出元素是否插入成功(插入成功为 true):

map<string, size_t> word_count;
string word;
while (cin >> word) {
	auto it = word_count.insert({ word,1 });
	if (!it.second) {
		++it.first->second;
	}
}

multisetmultimap添加元素

对于允许关键字重复的关联容器,接受单个元素的 insert 返回一个指向新元素的迭代器

11.3.3 删除元素(P386)

0c6e296d8421cbf36821b044c0ccbae

关联容器提供一个特殊版本的 erase :它接受一个 key_type 参数,删除所有关键词匹配的元素,返回实际删除的元素数量。

11.3.4 map的下标操作(P387)

mapunordered_map 都支持下标运算符和 at 函数,但 multimapunordered_multimap 不支持。

map 的下标运算符接受一个关键字。如果该关键字不在 map 中,下标运算符会为它创建一个元素并插入到 map 中,关联值进行值初始化。

5a1c69eed6b23bf8cd366d4f3a3d39c

使用下标操作的返回值

对一个 map 进行下标操作时,会获得一个 mapped_type 对象;解引用一个 map 迭代器时,会得到一个 value_type 对象。

11.3.5 访问元素(P388)

df3cb934f7a853a075fd797234cdea1 beefc3a822da4c064cf3e2b59b24b7d

需要纠正的是,constmapunordered_map 是可以使用 at 操作的。

当我们需要判断一个特定元素是否在容器中时,find 是最佳选择。

map使用find代替下标操作

前面提到,下标运算符会在关键字不存在时插入对应元素,如果我们不希望插入新元素的话,可以使用 find

multimapmultiset中查找元素

如果一个 multimapmultiset 中有多个元素具有相同的关键字,则这些元素在容器中会相邻存储

假设我们有一个作者到著作的 multimap 映射,我们想要打印一个特定作者的所有著作,可以通过三种不同的方法来解决这个问题:

  1. 使用 findcount

    auto it = authors.find(name);
    auto cnt = authors.count(name);
    while(cnt--){
        cout << it->second << endl;
        ++it;
    }
    
  2. 使用 lower_boundupper_bound 。如果关键字在容器中,lower_bound 指向第一个具有给定关键字的元素,upper_bound 指向最后一个匹配元素之后的位置;如果关键字不在容器中,lower_boundupper_bound 会返回相等的迭代器,指向一个不影响排序的关键字插入位置

    for (auto beg = authors.lower_bound(name),
    	end = authors.upper_bound(name); beg != end; ++beg) {
    	cout << beg->second << endl;
    }
    
  3. 使用 equal_range 函数。此函数接受一个关键字,返回一个迭代器 pairfirst 成员和 lower_bound 返回值相同,second 成员和 upper_bound 返回值相同:

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

11.4 无序容器(P394)

**无序关联容器(unordered associative container)**使用哈希函数和关键字类型的 == 来组织元素。

虽然理论上哈希技术具有较好的平均性能,但实际中想要达到很好的效果需要进行一些测试和调优工作。通常来讲,使用有序容器更为简单,也更为高效(这里中文版翻译错了)。

使用无序容器

通常可以用无序容器替换对应的有序容器。

管理桶

无序容器在存储上组织为一组,每个通保存零个或多个元素。为了访问一个元素,容器首先计算元素的哈希值,根据哈希值顺序搜索对应的桶。无序容器的性能依赖于哈希函数的质量和桶的数量。

无序容器提供了一组管理桶的函数:

cdaafd9d016087a8d912923ed1361eb

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

默认情况下,无序容器使用关键字类型的 == 来比较元素,使用 hash<key_type> 类型的对象来生成元素的哈希值。标准库为内置类型、string 和智能指针提供了 hash 模板。

如果我们想要定义关键字类型自定义类型的无序容器,必须提供 hash 模板版本。为了将 Sale_data 用作关键字,我们可以提供函数来替代 == 和 哈希函数:

size_t hasher(const Sales_data &sd){
    // 可以理解为:先利用默认构造函数构造一个匿名对象,然后调用对象重载的圆括号运算符
    return hash<string>()(sd.isbn());
}
bool equal(const Sales_data &lhs, const Sales_data &rhs){
    return lhs.isbn() == rhs.isbn();
}

然后,我们可以利用上面的函数定义无序容器:

using SD_multiset = unordered_multiset<Sales_data,
	decltype(hasher) *, decltype(equal) *>;
// 参数是桶数量的下限、哈希函数指针、相等判断函数指针
SD_multiset bookstore(42, hasher, equal);
  • 6
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值