第十一章 关联算法
使用关键字类型的比较函数
我们不能直接定义一个 Sales_data
的 multiset
,因为 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)
返回的值依赖于容器类型和参数。对于不包含重复关键字的容器,添加单一元素的 insert
和 emplace
版本返回一个 pair
,告诉我们插入操作是否成功。pair
的 first
成员是一个迭代器,指向具有给定关键字的元素; second
成员是一个 bool
值,指出元素是插入成功还是已经存在于容器中。如果关键字已在容器中,则 insert
什么事情也不做,且返回值中的 bool
部分为 false
。如果关键字不存在,元素被插入容器中,且 bool
为 true
;
// 单词计数
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_bound
和upper_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 >= n 且 bucket_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);
此集合的哈希和相等性判断操作与 haser
和 eqOp
函数有者相同的类型。通过使用这种类型,在定义 bookstore
时可以将我们希望它使用的函数的指针传递给他。
如果我们的类定义了 ==
运算符,则可以只重载哈希函数
// 使用 FooHash 生成哈希值;Foo 必须有 == 运算符
unordered_set<Foo, decltype(FooHash)*> fooSet(10, FooHash);