《C++Primer》第十一章——关联容器

第十一章:关联容器

顺序容器中的元素是按它们在容器中的位置来顺序保存和访问的
与之相对,关联容器中的元素是按关键字来保存和访问的
关联容器支持高校的关键字查找和访问,两个主要的关联容器分别为 map 和 set
map:map 中的元素是一些关键字-值对,关键字起到索引的作用,值则表示与索引相关联的数据
set:set 中只每个元素只包含一个关键字,set 支持高效的关键字查询操作

标准库提供了8个关联容器,这8个容器间的不同体现在三个维度上:
1)或者是一个 set,或者是一个 map
2)或者要求不重复的关键字,或者允许重复关键字,允许重复关键字的容器的名字中都包含单词 multi
3)按顺序保存元素,或无序保存,不保持关键字按顺序存储的容器的名字都以 unordered 开头

类型 map 和 multimap 定义在头文件 map 中
类型 set 和 multiset 定义在头文件 set 中
无序容器定义在头文件 unordered_map 和 unordered_set 中,无序容器使用哈希函数来组织元素

11.1 使用关联容器

1.map 是关键字-值对的集合
map 类型也通常被称为关联数组,关联数组与数组类似,不同之处在于其下标不必是整数,我们可以通过一个关键字而不是位置来查找值。
eg:可以将一个人的名字作为关键字,将其电话号码作为值,因此我们可以使用一个人的名字作为下标来获取此人的电话号码
2.set 就是关键字的简单集合
当只是想知道一个值是否存在时,set 是最有用的
eg:一个企业可以定义一个名为 bad_checks 的 set 来保存那些曾经开过空头支票的人的名字,在接受一张支票之前,可以查询 bad_checks 来检查顾客的名字是否在其中

11.2 关联容器概述

  • 关联容器都支持 9.2 节中介绍的普通容器操作(p 295)
  • 关联容器不支持顺序容器的位置相关的操作,eg: push_front 或 push_back,因为关联容器中的元素是根据关键字存储的,这些操作对关联容器没有意义
  • 除了与顺序容器相同的操作之外,关联容器还支持一些顺序容器不支持的操作和类型别名
  • 无序容器还提供一些用来调整哈希性能的操作
  • 关联容器的迭代器都是双向的
    1.定义关联容器
    1)每个关联容器都定义了一个默认构造函数,它创建一个指定类型的空容器;我们也可以将关联容器初始化为另一个同类型容器的拷贝,或是从一个值范围来初始化关联容器,只要这些值可以转化为容器所需类型就可以
map<string, size_t> word_count;	//空容器
//列表初始化
set<string> exclude = {"the", "but", "and", "or"}

2)初始化 multimap 或 multiset
一个 map 或 set 中的关键字必须是唯一的,对于一个给定的关键字只能有一个元素的关键字等于它;
而容器 multimap 和 multiset 没有这个限制,它们允许多个元素具有相同的关键字
2.关键字类型的要求
1)对于有序容器,即 map、multimap、set、multiset,关键字类型必须定义元素比较的方法,默认情况下,标准库使用关键字的 < 运算符来比较两个关键字;对于集合类型,关键字类型就是元素类型;而在映射类型中,关键字类型是元素的第一部分的类型
2)有序容器的关键字类型:我们可以向一个算法提供我们自己定义的比较操作,同样,我们也可以提供自己定义的操作来代替关键字上的 < 运算符,但所提供的操作必须在关键字类型上定义一个严格弱序

  • 两个关键字不能同时小于等于对方;
  • 如果 k1 小于等于 k2,且 k2 小于等于 k3,则 k1 必须小于等于 k3
  • 若存在两个关键字,任何一个都不小于等于另一个,那么称这两个关键字等价。若 k1 等价于 k2,k2 等价于 k3,那么 k1 必须等价于 k3
    补充:实际编程中,只要一个类型定义了“行为正常”的 < 运算符,则它可以用作关键字类型
    3)使用关键字类型的比较函数
  • 为了指定自定义的操作,必须在定义关联容器类型时提供此操作的类型,自定义的操作类型必须在尖括号中紧跟着元素类型给出
  • 但在尖括号中出现的每个类型,都仅仅是一个类型而已,当我们创建一个容器(对象)时,才会以构造函数参数的形式提供真正的比较操作,其类型必须与在尖括号中指定的类型吻合
//满足严格弱序
bool compareIsbn(const Sales_data &lhs, const Sales_data &rhs)
{
	return lhs.isbn < rhs.isbn();
}
multiset<Sales_data, decltype(CompareIsbn)*>
	bookstore(compareIsbn);

可以用 compareIsbn 代替 &compareIsbn 作为构造函数的参数,当我们使用一个函数的名字时,在需要的情况下会自动转化为一个指针
3. pair 类型

  • 定义在头文件 utility 中,一个 pair 保存两个数据成员,类似容器,pair 是一个用来生成特定类型的模板,当创建一个 pair 时,我们必须提供两个类型名,pair 的数据成员将具有对应的类型
  • pair 的默认构造函数对数据成员进行值初始化
  • 与其他标准库类型不同,pair 的数据成员是 public 的,两个成员分别命名为 first 和 second
  • map 的元素是 pair 类型
    1)创建 pair 对象的函数
pair<string, int>
process(vector<string> &v)
{
	if(!v.empty())
		return {v.back(), v.back().size()};		//列表初始化
	else
		return pair<string, int>();	//隐式构造返回值,一个空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());

11.3 关联容器操作

1.关联容器额外的类型别名
1)key_type:此容器类型的关键字类型
2)mapped_type:每个关键字关联的类型,只适用于 map
3)value_type:对于 set,与 key_type 相同;对于 map,为 pair<const key_type, mapped_type>,因为我们不能改变一个元素的关键字,因此这些 pair 的关键字部分是 const 的

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 v4;	//v5是一个int

2.关联容器迭代器
1)当解引用一个关联容器迭代器时,我们会得到一个类型为容器的 value_type 的值的引用
2)set 的迭代器是 const 的
虽然 set 定义了 iterator 和 const_iterator 类型,但是两种类型都只允许只读访问 set 中的元素
与不能改变一个 map 元素的关键字一样,一个 set 中的关键字也是 const 的
3)遍历关联容器:当使用一个迭代器遍历一个 map、multimap、set 和 multiset 时,迭代器按关键字升序遍历元素
4)关联容器和算法:通常不对关联容器使用泛型算法

  • 关键字是const的,很多修改或重排容器元素的算法不适用
  • 对于只读算法,很多都要搜索序列,使用关联容器专用的 find 成员会比调用泛型 find 快是多
    3.添加元素
    对于 map 和 set 以及对应的无需类型包含不重复的关键字,因此插入一个存在的元素对容器没有任何影响
    1)向 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));

2)检测 insert 的返回值:insert 或 emplace 返回的值依赖于容器类型和参数,对于不包含重复关键字的容器添加单一元素的 insert 和 emplace 版本返回一个 pair;pair 的 first 成员是一个迭代器,指向具有给定关键字的元素;pair 的 second 成员是一个 bool 值,指出元素是插入成功还是已经存在于容器中,若关键字已在容器中,insert 什么也不做并且返回值 bool 部分为 false,若关键字不存在,则元素被插入到容器中,且 bool 值为 true
3)向 multiset 或 multimap 添加元素:由于一个 multi 容器中的关键字不必唯一,这些类型上调用 insert 总会插入一个元素,因此对允许重复关键字的容器,接受单个元素的 insert 操作返回一个指向新元素的迭代器,这里无须返回一个 bool 值,因为插入总是成功的
4.删除元素
关联容器定义了三个版本的 erase

  • 通过传递给 erase 一个迭代器来删除一个元素,指定的元素被删除,函数返回 void
  • 通过传递给 erase 一个迭代器对来删除一个元素范围,指定的元素范围被删除,函数返回 void
  • 关联容器额外版本,接受一个 key_type 参数,此版本删除所有匹配给定关键字的元素,并返回实际删除的元素的数量;对于保存不重复关键字的容器,erase 的返回值总是 0 或 1,若返回值为 0,则表明想要删除的元素并不在容器中
    5. map的下标操作
  • map 和 unordered_map 容器提供了下标运算符和一个对应的 at 函数
  • set 类型不支持下标运算符,因为 set 中没有与关键字相关联的值,元素本身就是关键字
  • multimap 和 unordered_multimap 不支持下标运算符,因为在这些容器中,一个关键字可能有多个值与一个关键字相关联
    1)对一个 map 使用下标操作,其行为与数组或 vector 上的下标操作很不相同,若使用一个不在容器中的关键字作为下标,会添加一个具有此关键字的元素到 map 中
    因此若有时只是想知道一个元素是否存在于 map 中,但在不存在时并不想添加元素,就不能使用下表运算符
    由于下标运算符可能插入一个新元素,我们只可以对非 const 的 map 使用下标操作
    2)使用下标操作的返回值
  • 与 vector 和 string 不同,map 的下标运算符返回的类型与解引用 map 迭代器得到的类型不同;map 类型解引用一个迭代器返回的是一个 value_type 对象,而对一个 map 进行下标操作时,会获得一个 mapped_type 对象
  • 与其他下标运算符相同的是,map 的下标运算符返回一个左值,因此我们既可以读也可以写元素
    6.访问元素
    1)对 map 使用 find 代替下标操作:由于使用下标运算符会有副作用,即若关键字不存在的话下标运算符会自动插入一个新元素,有时我们不希望这种情况发生,这时我们应该使用 find 函数,该函数返回第一个关键字符合的元素,若不在容器中,则返回尾后迭代器
    2)在 multimap 或 multiset 中查找元素
  • 如果一个 multimap 或 multiset 中有多个元素具有给定关键字,则这些元素在容器中会相邻存储
    3)一种不同的,面向迭代器的解决方法
  • lower_bound 返回的迭代器将指向第一个具有给定关键字的元素
  • upper_bound 返回的迭代器将指向最后一个匹配给定关键字的元素之后的位置
  • 如果元素不在 multimap 中,则 lower_bound 和 upper_bound 会返回相等的迭代器,指向一个不影响排序的关键字插入位置,也就是说,如果 lower_bound 和 upper_bound 返回相同的迭代器,则给定关键字不在容器中,这可以作为一个判断依据
    4)equal_range 函数
    该函数接受一个关键字,返回一个迭代器 pair,若关键字存在,则第一个迭代器指向第一个与关键字匹配的元素,第二个迭代器指向最后一个匹配元素之后的位置;若未找到匹配元素,则两个迭代器指向关机阻尼可以插入的位置

11.4 无序容器

  • 新标准定义了4个无序关联容器,这些容器不使用比较运算符来组织元素,而是使用一个哈希函数和关键字类型的 == 运算符
  • 如果关键字类型固有就是无序的,或者性能测试发现问题可以用哈希技术解决,则可以使用无序容器
    1.使用无序容器:通常可以用一个无序容器替换对应的有序容器,反之亦然;但是由于元素未按顺序存储,一个使用无序容器的程序的输出通常会与使用有序容器的版本不同
    2.管理桶
  • 无序容器在存储上组织为一组桶,每个桶保存零个或多个元素
  • 无序容器使用一个哈希函数将元素映射到桶,为了访问一个元素,容器首先通过哈希函数计算元素的哈希值,从而指出应该搜素哪个桶
  • 容器将具有一个特定哈希值的元素都保存在相同的桶中,若容器允许重复关键字,相同关键字的元素一定在同一个桶中
  • 在理想情况下,哈希函数将每个特定的值映射到唯一的桶,但是将不同关键字映射到相同的桶也是允许的;因此,当一个桶保存多个元素时,需顺序搜索这些元素来查找我们想要哪个,因此,无序容器的性能依赖于哈希函数的质量和桶的数量和大小
  • 无序容器管理操作(p 395),这些成员函数允许我们查询容器的状态以及必要时强制容器进行重组
    3.无序容器对关键字类型的需求
  • 默认情况下,无序容器使用关键字类型的 == 运算符来比较元素,并使用一个 hash<key_type> 类型的对象来生成每个元素的哈希值
  • 标准库为内置类型(包括指针)提供了 hash 模板,包括 string 和 智能指针类型,对于这些已经定义了 hash 的类型可以直接定义相关的无序容器
  • 但是,不能直接定义关键字类型为自定义类类型的无序容器,因为我们需要提供自己的 hash 模板版本(将在16.5详细介绍)
  • 在这里,我们不使用默认 hash,而是用另一种方法,类似于有序容器重载关键字类型的默认比较操作,为了能将 Sale_data 作为关键字,我们需要提供函数来代替 == 运算符和哈希值计算函数
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);

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值