《C++11Primer》阅读随记 -- 十一、关联容器

第十一章 关联算法

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

我们不能直接定义一个 Sales_datamultiset,因为 Sales_data 没有 < 运算符。但是,可以用一个 compareIsbn 函数来定义一个 multiset

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

为了使用自己定义的操作,在定义 multiset 时,我们必须提供两个类型:关键字类型 Sales_data,以及比较操作类型–应该是一种函数指针类型,可以指向 compareIsbn。当定义此容器类型的对象时,需要提供想要使用的操作的指针。

// bookstore 中多条记录可以有相同的 ISBN
// bookstore 中的元素以 ISBN 的顺序进行排列
multiset<Sales_data, decltype(compareIsbn)*> bookstore(compareIsbn);

在这里插入图片描述
在这里插入图片描述

pair 类型

pair 标准库类型,定义在头文件 utility 中。

pair<string, string> author{"James", "Joyce"};

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 的类型推断出来
p1 relop p2						// 关系运算符( <、>、<=、>= )按字典序定义:例如,当
								// p1.first < p2.first 或 
								// !(p2.first < p1.first) && p1.second < p2.second
								// 成立时,p1 < p2 为 true。关系运算符利用元素的 <
								// 运算符来实现

在一个 map 中,元素是关键字-值对。即,每一个元素是一个 pair 对象,包含一个关键字和一个关联的值

类型别名解释
key_type此容器类型的关键字类型
mapped_type每个关键字关联的类型; 只适用于 map
value_type对于 set, 与 key_type 相同, 对于 map,为 pair<const key_type, 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

insert

insert 或 (emplace) 返回的值依赖于容器类型和参数。对于不包含重复关键字的容器,添加单一元素的 insertemplace 版本返回一个 pair,告诉我们插入操作是否成功。pairfirst 成员是一个迭代器,指向具有给定关键字的元素; second 成员是一个 bool 值,指出元素是插入成功还是已经存在于容器中。如果关键字已在容器中,则 insert 什么事情也不做,且返回值中的 bool 部分为 false。如果关键字不存在,元素被插入容器中,且 booltrue

// 单词计数
map<string, size_t> word_count;	// 从 string 到 size_t 的空 map
string word;
while(cin >> word){
	auto ret = word_count.insert({word, 1});
	if(!ret.second)
		++ret.first->second; // 递增计数器
}

map 的下标操作

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

上述代码会执行如下操作:

  • word_count 中搜索关键字为 Anna 的元素,未找到
  • 将一个新的关键字-值对插入到 word_count 中。关键字是一个 const string,保存 Anna。值进行值初始化,本例中意味着值为 0
  • 提取出新插入的元素,并将值 1 赋予它

对于一个 map 使用下标操作,其行为与数组或 vector 上的下标操作很不相同:使用一个不再容器中的关键字作为下标,会添加一个具有此关键字的元素到 map

访问元素

// lower_bound 和 upper_bound 不适用于无序容器
// 也就是说 vector 这些不能用,但是 vector 这类可以用头文件<algorithm>下的
// lower_bound(iterator a, interator b, value);
// 下标和 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()

返回的都是迭代器
在这里插入图片描述
在这里插入图片描述

如果 lower_boundupper_bound 返回相同的迭代器,则给定关键字不在容器中

equal_range 函数

// string search_item("Alain de Botton");	// 要查找的作用
// authors 是作者簿

for(auto pos = authors.equal_range(search_item); 
		 pos.first != pos.second; 
		 ++pos.first)
	cout << pos.first->second << endl;	// 打印每个题目

一个单词转换的 map

如果单词转换文件的内容如下所示:

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

我们希望转换的文本为

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

程序应该生成这样的输出

where are you
why dont you send me a picture
okay? thanks! later

// transform:
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 << " ";					// 在单词间打印一个空格
			// transfrom 返回它的第一个参数或其转换之后的形式
			cout << transform(word, trans_map);	// 打印输出
		}
		cout << endl;
	}
}

函数首先调用 buildMap 来生成单词转换 map, 我们将它保存在 trans_map 中。函数的剩余部分处理输入文件。while 循环用 getline 一行一行地读取输入文件。这样做地目的是使得输出中地换行位置能和输入文件中一样。为了从每行中读取单词,我们使用勒一个嵌套的 while 循环,它用一个 istringstream 来处理当前行中的每个单词

在输出过程中,内层 while 循环使用一个 bool 变量 firstword 来确定是否打印一个空格,它通过调用 transform 来获得要打印的单词。transform 的返回值或者是 word 中原来的 string, 或者是 trans_map 中指出的对应的转换内容

建立转换映射

函数 buildMap 读入给定文件,建立转换映射

map<string, string> buildMap(ifstream& map_file){
	map<string, string> trans_map;	// 保存转换规则
	string key;						// 要转换的单词
	string value;					// 替换后的内容
	// 读取第一个单词存入 key 中,行中剩余内容存入 value
	while(map_file >> key && getline(map_file, value)){
		if(value.size() > 1)	// 检查是否有转换规则
			trans_map[key] = value.substr(1);		// 跳过前导空格
		else
			throw runtime_error("no rule for " + key);
	}
	return trans_map;
}

生成转换文本

函数 transform 进行实际的转换工作。其参数是需要转换的 string 的引用和转换规则 map

const string& transform(const string& s, const map<string, string>& m){
	// 实际的转换工作; 此部分是程序核心
	auto map_it = m.find(s);
	if(map_it != m.cend())
		return map_it->second;	// 使用替换短语
	else return s;				// 否则返回原 string
}

无序容器

新标准定义了 4 个 无序关联容器( unordered associative container )。这些容器不是使用比较运算符来组织元素,而是使用一个 哈希函数( hash function ) 和关键字类型的 == 运算符。

管理桶

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

计算一个元素的哈希值和在桶中搜索(顺序搜索)通常都是很快的操作。但是,如果一个桶中保存了很多元素,那么查找一个特定元素就需要大量的比较操作

无序容器提供了一组管理桶的函数。这些成员函数允许我们插叙容器的状态以及在必要时强制容器进行重组

桶接口功能
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 >= nbucket_count > size / max_load_factor
c.reserve(n)重组存储,使得 c 可以保存 n 个元素且不必 rehash

在这里插入图片描述

注意: 在我使用 rehash、reserve 指定 n 为 20 的时候,最终结果是 32, 说明在底层,实际的增长或者说扩充是 8 的倍数

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

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

但是我们不能直接定义关键字类型为自定义类类型的无序容器。与容器不同,不能直接使用哈希模板,而必须提供自己的 hash 模板版本。

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

size_t hasher(const Sales_data& sd){
	return hash<string>()(sd.isbn());
	// hash<string>()是一个临时对象,不是一个函数调用操作
}
bool eqOp(const Sales_data* lhs, const Sales_data& rhs){
	return lsh.isbn() == rhs.isbn();
}

hasher 函数使用一个标准库 hash 类型对象来计算 ISBN 成员的哈希值,该 hash 类型建立在 string 类型上。类似的 eqOp 函数通过比较 ISBN 号来比较两个 Sales_data

我们使用这些函数来定义一个 unordered_multiset

using SD_multiset = unordered_multiset<Sales_data,
						decltype(hasher)*, decltype(eqOp)*>;
// 参数是桶大小、哈希函数指针和相等性判断允素福指针
SD_multiset bookstore(42, hasher, eqOp);

此集合的哈希和相等性判断操作与 hasereqOp 函数有者相同的类型。通过使用这种类型,在定义 bookstore 时可以将我们希望它使用的函数的指针传递给他。

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

// 使用 FooHash 生成哈希值;Foo 必须有 == 运算符
unordered_set<Foo, decltype(FooHash)*> fooSet(10, FooHash);

在这里插入图片描述
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Artintel

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值