【C++】【C++ Primer】11-关联容器

1 使用关联容器

1.1 关联容器简介

关联容器支持高效的关键字查找和访问。

如表1所示,标准库提供8个关联容器。它们的不同体现在三个维度上:

  • 每个容器或者是map,或者是set。
  • 每个容器或者要求不重复的关键字,或者允许重复关键字。
  • 每个容器按顺序保存元素,或无序保存。

容器的命名规律如下:

  • 允许重复关键字的容器名中包含multi
  • 无序保存元素的容器名以unordered开头

无序容器使用哈希函数来组织元素。

表1 关联容器类型
有序关联容器
map关联数组。保存key-value对
set关键字即值,即只保存关键字的容器
multimap关键字可重复出现的map
multiset关键字可重复出现的set
无序关联容器
unordered_map用哈希函数组织的map
unordered_set用哈希函数组织的set
unordered_multimap用哈希函数组织的map,关键字可以重复出现
unordered_multiset用哈希函数组织的set,关键字可以重复出现

类型map和multimap定义在头文件map中。set和multiset定义在头文件set中。无序容器则定义在头文件unordered_map和unordered_set中。

map是key-value对的集合,亦称关联数组。关联数组和正常数组类似,区别在于其下标不必是整数,通过关键字而非位置来查找值。譬如将一个人的名字作为key,电话号码作为value。这样的数据结构被称作“将名字映射到电话号码”。给定名字到电话的map后,使用人名作为下标来获取对应的电话。

set是关键字的集合。如果只是想知道某个值是否存在,set是最有用的。

关联容器可以列表初始化,譬如:

map<string, size_t> word_count = {{"albert", 6},
								  {"bill",   3},
								  {"Carol",  7};
set<string> exclude = {"the", "and"};

1.2 使用map

类似顺序容器,关联容器也是模板。定义map时,必须指定key和value的类型。

从map提取元素时,会得到一个pair类型的对象(详见)。pair是一个模板类型,保存名为first和second的公有数据成员。map使用pair.first保存key,pair.second保存value。

以下代码读取输入,统计单词出现次数。

int main(int argc, char **argv)
{
    string word;
    map<string, size_t> word_count;

    while (cin >> word) {
        ++word_count[word];
    }

    for (const auto &w : word_count) {
        cout << w.first << " occurs " << w.second
            << ((w.second) > 1 ? " times" : " time") << endl;
    }

    return 0;
}

1.3 使用set

set也是模板类型,定义时必须指定其元素类型。

以下代码在上一小节代码的基础上,增加了集合exclude,其中保存想忽略的单词。输入单词后,首先检查是否在exclude集合中,仅统计不在此集合中的元素。

int main(int argc, char **argv)
{
    string word;
    map<string, size_t> word_count;
    set<string> exclude = {"the", "but", "and"};

    while (cin >> word) {
        if (exclude.find(word) == exclude.end())
            ++word_count[word];
    }

    for (const auto &w : word_count) {
        cout << w.first << " occurs " << w.second
            << ((w.second) > 1 ? " times" : " time") << endl;
    }

    return 0;
}

2 关联容器概述

不论有序无序,关联容器都支持下表中的普通容器操作:

表2 容器操作
类型别名
iterator此容器类型的迭代器类型
const_iterator只读迭代器类型
size_type无符号整数类型,足够保存此容器类型最大大小
difference_type带符号整数类型,足够保存两个迭代器之间的距离
value_type元素类型
reference元素的左值类型,与value_type&含义相同
const_reference元素的const左值类型,与const value_type&含义相同
构造函数
C c;默认构造函数,构造空容器
C c1(c2);构造c2的拷贝c1
C c(b, e);构造c,将迭代器b和e指定范围内的元素拷贝到c(array不支持)
C c{a, b, c, ...};列表初始化c
赋值与swap
c1 = c2;将c1中的元素替换为c2中的元素
c1 = {a, b, c, ...};将c1中的元素替换为列表中的元素(不适用于array)
a.swap(b);交换a和b的元素
swap(a, b);与a.swap(b)等价
大小
c.size()c中元素的数目(不支持forward_list)
c.max_size()c可保存的最大元素数目
c.empty()c为空则返回true
添加/删除元素(不适用于array,且在不同容器中这些操作的接口不同)
c.insert(args)将args中的元素拷贝进c
c.emplace(inits)使用inits构造c中的一个元素
c.erase(args)删除args指定的元素
c.clear()删除c中的所有元素,返回void
关系运算符
==, !=所有容器都支持相等/不相等运算符
<, <=, >, >=关系运算符(无序容器不支持)
获取迭代器
c.begin()、c.end()返回指向c的首元素和尾后元素位置的迭代器
c.cbegin()、c.cend()返回const_iterator
反向容器的额外成员(不支持forward_list)
reverse_iterator按逆序寻址元素的迭代器
const_reverse_iterator不能修改元素的逆序迭代器
c.rbegin()、c.rend()返回指向c的尾元素和首前元素位置的迭代器
c.crbegin()、c.crend()返回const_reverse_iterator

但关联容器不支持以下操作(存疑):

  • 顺序容器的位置相关的操作,譬如push_front、push_back。这是因为关联容器中的元素是根据关键字存储的,这些操作对关联容器没有意义。
  • 不支持构造函数。
  • 不支持插入操作这些接受一个元素值和一个数量值的操作。

关联容器还支持一些顺序容器不支持的操作和类型别名。此外,无序容器还提供了一些用于调整哈希性能的操作。

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

2.1 定义关联容器

2.1.1 定义和初始化关联容器的方式

  • 使用默认构造函数,创建指定类型的空容器;
  • 将关联容器初始化为另一个同类型容器的拷贝;
  • 从一个值范围来初始化关联容器,只要这些值可以转化为容器所需类型即可
  • 在新标准下,可以对关联容器进行值初始化。

初始化map时,必须提供关键字类型和值类型。将每个key-value对包围在花括号中:

{key, value}

初始化set时,元素类型就是关键字类型。

// 空容器
map<string, size_t> word_count;

// 列表初始化
set<string> exclude = {"the", "but", "and"};

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

2.1.2 初始化multimap或multiset

一个map或set的关键字必须是唯一的,对于一个给定的关键字,只能有一个元素的关键字等于它。容器multimap和multiset则没有此限制,它们都允许多个元素具有相同的关键字。

以下代码展示具有唯一关键字的容器和允许重复关键字的容器之间的区别:

// 定义有20个元素的vector,保存0到9每个整数的两个拷贝
vector<int> ivec;
for (vector<int>::size_type i = 0; i != 10; ++i) {
	ivec.push_back(i);
	ivec.push_back(i);
}

// iset包含来自ivec的不重复的元素,miset包含所有20个元素
set<int> iset(ivec.cbegin(). ivec.cend());
multiset<int> miset(ivec.cbegin(), ivec.cend());

cout << ivec.size() << endl;     // 打印出20
cout << iset.size() << endl;     // 打印出10
cout << miset.size() << endl;    // 打印出20

2.2 关键字类型的要求

关联容器对关键字类型有一些限制。无序容器中的关键字要求后续介绍。

有序容器(map、multimap、set、multiset):关键字必须定义元素比较的方法。默认情况下,标准库使用关键字类型的<运算符来比较两个关键字。在集合类型中,关键字类型就是元素类型。在映射类型中,关键字类型是元素第一部分的类型。

2.2.1 有序容器的关键字类型

我们也可以提供自定义的操作来代替关键字上的<运算符,这个自定义操作必须在关键字类型上定义一个严格弱序。可以将严格弱序看作“小于等于”,它必须具备以下基本性质:

  • 两个关键字不能同时“小于等于”对方。如果k1“小于等于”k2,那么k2决不能“小于等于”k1。
  • 如果k1“小于等于”k2,且k2“小于等于”k3,则k1必须“小于等于”k3。
  • 如果存在两个关键字,任何一个都不“小于等于”另一个,则称这两个关键字是“等价”的。如果k1“等价于”k2,k2“等价于”k3,则k1“等价于”k3。

如果两个关键字是等价的,那么容器将它们视作相等来处理。当用作map的关键字时,只能有一个元素与这两个关键字关联,可以使用两者中的任意一个来访问对应的值。

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

我们提供的自定义操作也是容器类型的一部分,必须在定义关联容器对象时指定。在定义关联容器对象时,尖括号中指出要定义哪种类型的容器之后,紧跟着给出自定义的操作类型。

在尖括号中出现的类型仅是标记,真的创建容器对象时,才会以构造函数参数的形式提供比较操作(类型必须和尖括号中指定的类型吻合)。

譬如,我们不能直接定义Sales_data的multiset,因为Sales_data没有<运算符。因此,我们要提供一个自定义的方法,定义严格弱序:

// 在Sales_data对象的ISBN成员上定义一个严格弱序
bool compareIsbn(const Sales_data &lhs, const Sales_data &rhs)
{
	return lhs.isbn() < rhs.isbn();
}

为了使用compareIsbn这个自定义操作,在定义multiset时要提供两个类型

  • 关键字类型Sales_data
  • 比较操作类型(指向compareIsbn的函数指针)

当定义此容器类型的对象时,要提供想要使用的操作的指针。

// bookStore中的元素以ISBN的顺序进行排列
mutiset<Sales_data, decltype(compareIsbn) *> bookstore(compareIsbn);

2.3 pair类型

2.3.1 pair类型概述

标准库类型pair是一个用来生成特定类型的模板,定义在头文件utility中。

一个pair保存两个数据成员,创建pair时必须提供两个类型名。

pair<string, string> anon;
pair<string, size_t> word_count;
pair<string, vector<int>> line;

pair的默认构造函数对数据成员进行值初始化,因此,上述代码中anon是一个包含两个空string的pair。word_count中string成员为空,size_t为0。line保存一个空string和一个空vector。

也可以为每个成员提供初始化,譬如:

pair<string, string> author("James", "Joyce");

pair的数据成员是public的,分别命名为first和second。使用成员访问符号来访问成员。

表3 pair上的操作
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中推断出来
p.first返回p的名为first的数据成员
p.second返回p的名为second的数据成员
p1 relop p2关系运算符(<、<=、>、>=)按字典序定义。例如当p1.first< p2.first或!(p2.first < p1.first) && p1.second < p2.second成立时,p1< p2为true。关系运算利用元素的< 运算符来计算
p1 == p2当first和second成员分别相等时,两个pair相等。相等性判断利用元素的==实现
p1 != p2

2.3.2 创建pair对象的函数

如果函数需要返回一个pair,在新标准下,可以对返回值进行列表初始化。

pair<string, int> process(vector<string> &v)
{
	if (!v.empty())
		return {v.back(), v.back().size()};    // 列表初始化
	else
		return pair<string, int>();            // 隐式构造返回值
}

在较早的C++版本中,不能使用花括号来返回pair这种类型的对象,必须显式构造返回值:

if (!v.empty())
	return pair<string, int>(v.back(), v.back().size());

或者使用make_pair来生成pair对象并返回:

if (!v.empty())
	return make_pair(v.back(), v.back().size());

3 关联容器操作

除了表2列出的类型,关联容器还定义了以下类型,用于表示容器关键字和值的类型。

表4 关联容器额外的类型别名
key_type此容器类型的关键字类型
mapped_type每个关键字关联的类型,只适用于map
value_type对于set,与key_type相同
对于map,为pair< const key_type, mapped_type>

对于set类型,key_type和value_type是一样的。

对于map类型,元素是key-value对。每个元素是一个pair对象,包含一个关键字和一个关联的值。由于不能改变一个元素的关键字,所以这些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 v5;    // v5是int类型

只有map类型(unordered_map、unordered_multimap、multimap、map)定义了mapped_type。

3.1 关联容器迭代器

解引用一个关联容器迭代器时,得到类型为容器value_type的引用。对map而言,value_type是一个pair类型。必须牢记,map的pair中,只能修改second。first是const的,不能修改。

// 获得指向word_count中一个元素的迭代器
auto map_it = word_count.begin();
// *map_it是一个指向pair<const string, size_t>对象的引用
cout << map_it->first;
cout << " " << map_it->second;
map_it->first = "new key";    // 错误,关键字是const的
++map_it->second;             // 正确

3.1.1 set的迭代器是const的

set中的关键字是const的,不可修改。因此虽然set类型同时定义了iterator和const_iterator类型,但两种类型都只允许只读访问set中的元素。

set<int> iset = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
set<int>::iterator set_it = iset.begin();
if (set_it != iset.end()) {
	*set_it = 42;               // 错误。set中的关键字是只读的
	cout << *set_it << endl;    // 正确。可以读取关键字
}

3.1.2 遍历关联容器

map和set类型都支持begin和end操作。我们可以用这些函数获取迭代器,然后用迭代器来遍历容器。

auto map_it  = word_count.cbegin();
while (map_it != word_count.cend()) {
	cout << map_it->first << " occurs "
		<< map_it->second << " times" << endl;
	++map_it;
}

3.1.3 关联容器和算法

通常不对关联容器使用泛型算法。

关键字是const这个特性意味着,不能将关联容器传递给修改或重排容器元素的算法,因为它们要向元素写入数据。

关联容器可用于只读取元素的算法,但这类算法大都要搜索序列。由于关联容器中的元素不能通过关键字快速查找,所以对其使用泛型搜索算法是个坏主意。关联容器定义了名为find的成员,通过给定的关键字直接获取元素。如果用泛型find算法来查找一个元素,会顺序搜索。因此,使用关联容器定义的专用find成员比泛型find快很多。

在实际编程中,如果真的要对关联容器使用算法,要么是将其作为源序列,要么当做一个目的位置。譬如,使用泛型copy算法将元素从一个关联容器拷贝到其他序列。或者,调用inserter将一个插入迭代器绑定到关联容器,使用inserter可以将关联容器作为目的位置来调用算法。

3.2 添加元素

关联容器的insert成员可以向容器中添加一个元素或一个元素范围。由于map和set(及其对应的无需类型)包含不重复的关键字,因此插入一个已存在的元素不会对容器产生任何影响。

insert有两个版本,分别接受一对迭代器,或一个初始化列表。注意,对于一个给定的关键字,只有第一个带此关键字的元素才被插入到容器中。

vector<int> ivec = {2, 4, 6, 8, 2, 4, 6, 8};
set<int> set2;
set2.insert(ivec.cbegin(), ivec.cend());    // set2有4个元素
set2.insert({1, 3, 5, 7, 1, 3, 5, 7});

3.2.1 向map添加元素

对map进行insert操作时,务必注意元素类型是pair。通常没有现成的pair对象,可以在insert的参数列表中创建一个pair。具体方法有以下四种:

word_count.insert({word, 1});
word_count.insert(make_pair(work, 1));
word_count.insert(pair<string, size_t>(word, 1));
word_count.insert(map<string, size_t>::value_type(word, 1));    // 构造一个恰当的pair类型,并构造该类型的新对象,插入到map中
表5 关联容器insert操作
c.insert(v)v是value_type类型的对象。args用来构造一个元素。对于map和set,只有当元素的关键字不在c中时,才插入(构造)元素。函数返回一个pair,包含一个迭代器,指向具有插入指定关键字的元素,以及一个指示插入是否成功的bool值。对于multimap和multiset,总会插入(或构造)给定元素,并返回一个指向新元素的迭代器
c.emplace(args)
c.insert(b, e)b和e是迭代器,表示一个c::value_type类型值的范围。il是这种值的花括号列表。函数返回void。对于map和set,只插入关键字不在c中的元素。对于multimap和multiset,则会插入范围中的每个元素
c.insert(il)
c.insert(p, v)类似insert(v)或emplace(args),但将迭代器p作为一个提示,指出从哪里开始搜索新元素应该存储的位置。返回一个迭代器,指向具有给定关键字的元素
c.emplace(p, args)

3.2.2 检测insert的返回值

insert和emplace的返回值依赖于容器类型和参数。

对于不包含重复关键字的容器,添加单一元素的insert和emplace版本返回一个pair,告诉我们插入操作是否成功。pair的first成员是一个迭代器,指向具有给定关键字的元素。second成员是一个bool值,指出元素是插入成功还是已经存在于容器中。如果关键字已在容器中,则insert什么事都不做,且返回值中的bool值为false。如果关键字不存在,元素被插入到容器中,且bool值为true。

3.2.3 向multiset或multimap添加元素

有时我们希望能添加具有多个相同关键字的元素,譬如建立作者到其著作的映射。此时,应使用multimap而非map。

由于multi容器中,元素的关键字不必唯一,所以在这些类型上调用insert总会插入一个元素。multi容器接受单个元素的insert操作会返回一个指向新元素的迭代器,无需返回bool值,因为总是会插入。

3.3 删除元素

关联容器定义了三个版本的erase。可以通过传递给erase一个迭代器来删除一个元素,也可以传递给erase一对迭代器来删除一个元素范围。这两个版本的erase删除指定元素并返回void。

关联容器还提供了一个erase操作,它接受一个key_type参数,删除所有匹配该关键字的元素,并返回实际删除的元素数量。对于保存不重复关键字的容器,erase的返回值总是0或1。若返回值为0,说明想删除的元素不在容器中。对于允许重复关键字的容器,返回值可能大于1。

表6 从关联容器删除元素
c.erase(k)从c中删除每个关键字为k的元素,返回size_type值,指出删除的元素数量
c.erase(p)从c中删除迭代器p指定的元素。p必须指向c中的一个真实元素,不能等于c.end()。返回一个指向p之后元素的迭代器,若p指向c中的尾元素,则返回c.end()
c.erase(b, e)删除迭代器对b和e所表示的范围中的元素。返回e

3.4 map的下标操作

set类型不支持下标,因为set中元素本身就是关键字。

map和unordered_map容器提供了下标运算符和一个对应的at函数。不能对multimap或unordered_multimap进行下标操作,因为这些容器中可能有多个值与一个关键字相关联。

由于下标运算符可能插入一个新元素,我们只能对非const的map使用下标操作。

map下标运算符接收一个关键字,获取与此关键字相关联的值。与其他下标运算符不同的是,如果关键字并不在map中,会为它创建一个元素并插入到map中,关联值将进行值初始化。

譬如以下代码,将会执行如下操作:

  • 在word_count中搜索关键字为Anna的元素,未找到
  • 将一个新的key-value对插入到word_count中。关键字是一个const string,保存Anna。值进行值初始化,在本例中为0
  • 提取出新插入的元素,并将值1赋予它
map<string, size_t> word_count;
word_count["Anna"] = 1;
表7 map和unordered_map的下标操作
c[k]返回关键字为k的元素。如果k不在c中,添加一个关键字为k的元素,对其进行值初始化
c.at(k)访问关键字为k的元素,带参数检查。若k不在c中,则抛出out_of_range异常

3.4.1 使用下标操作的返回值

map的下标运算符与我们用过的其他下标运算符的另一个不同之处是其返回类型。通常解引用迭代器返回的类型和下标运算符返回的类型相同,但对map则不然。

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

与其他下标运算符相同的是,map的下标运算符返回一个左值,既可以读也可以写。

前面提到,如果关键字不在map中,下标运算符会添加一个新元素。如果我们只是想知道某个元素是否在map中,并不想添加元素,就不能使用下标运算符。

3.5 访问元素

关联容器提供多种查找指定元素的方法,具体使用哪个操作取决于我们想解决什么问题。

  • find:适用于只关心某个元素是否已在容器中的情况;
  • count
    • 对于不允许重复关键字的容器,count和find效果一样;
    • 对于允许重复关键词的容器,count会统计有多少个元素有相同关键字。如果不需要计数,最好使用find。
表8 在关联容器中查找元素的操作
lower_bound和upper_bound不适用于无序容器。
下标和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()

3.5.1 对map使用find代替下标操作

对map和unordered_map类型,下标运算符是最简单的提取元素的方法。但如果下标不存在于map中,则会自动插入一个具有给定关键字的元素,并对其值初始化。

如果不希望自动插入,则应使用find:

if (word_count.find("foobar") == word_count.end())
	cout << "foobar is not in the map" << endl;

3.5.2 在multimap或multiset中查找元素

在不允许重复关键字的关联容器中查找元素相对简单——元素要么在容器中,要么不在。对于允许重复关键字的容器则更为复杂——容器中可能有很多元素具有给定的关键字。

如果一个multimap或multiset中有多个元素具有给定关键字,这些元素在容器中会相邻存储。我们有三种方法可解决此问题。

接下来,以打印某位作者所有著作的场景为例,讲解这三种方法。

3.5.2.1 find+count

find找到该关键字的首元素,count返回该容器中使用该关键字的元素数量。然后循环即可。

string search_item("Alain de Botton");        // 指定作者
auto entries = authors.count(search_item);    // 该作者著作数量
auto iter = authors.find(search_item);        // 该作者的第一本著作

while (entries) {
	cout << iter->second << endl;             // 打印著作名
	++iter;                                   // 指向下一本著作
	--entries;                                // 控制循环
}
3.5.2.2 lower_bound+upper_bound

lower_bound和upper_bound都接受一个关键字,返回一个迭代器。

如果关键字在容器中,lower_bound返回指向第一个具有指定关键字的元素的迭代器。upper_bound则返回指向最后一个具有指定关键字的元素之后位置的迭代器。

如果关键字不在容器中,lower_bound和upper_bound会返回相等的迭代器,指向一个不影响排序的关键字插入位置。

综上,用相同关键字调用lower_bound和upper_bound可以得到表示具有该关键字的元素的迭代器范围。

值得注意的是,lower_bound和upper_bound均有可能指向尾后迭代器。

  • 最大关键字的upper_bound返回尾后迭代器
  • 关键字不存在,且大于容器中任何关键字时,lower_bound也返回尾后迭代器
for (auto begin = authors.lower_bound(search_item),
          end = authors.upper_bound(search_item);
     begin != end; ++begin) {
	cout << begin->second << endl;
}
3.5.2.3 equal_range函数

equal_range接受一个关键字,返回一个迭代器pair。如关键字存在,first指向第一个与关键字匹配的元素,second指向最后一个匹配元素之后的位置。若未找到匹配函数,两个迭代器均指向关键字可插入的位置。

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

3.6 代码示例——一个单词转换的map

编写程序,实现以下功能。给定一个string,将它转换为另一个string。程序的输入是两个文件。第一个文件保存规则,用于转换第二个文件中的文本。每条规则由两部分组成:一个可能出现在输入文件中的单词和一个用来替换它的短语。每当第一个单词出现在输入中时,将其替换为对应的短语。第二个输入文件包含要转换的文本。

如果单词转换文件的内容如下所示:
brb be right back
k okay?
y why
r are
u you
pic picture
thk thanks!
18r later

希望转换的文本为:
where r u
y don’t u send me a pic
k thk 18r

则程序应输出:
where are you
why don’t you send me a picture
okay? thanks! later

map<string, string> buildMap(ifstream &map_file)
{
	string key;      // 要转换的单词
	string value;    // 替换后的内容
	map<string, string> trans_map;    // 保存转换规则
	
	// 读取第一个单词存入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;
}

const string &transform(const string &s, const map<string, string> &m)
{
	// 实际转换工作,这部分是程序核心
	auto map_it = m.find(s);
	// 如果单词在转换规则map中
	if (map_it != m.end()) {
		return map_it->second;    // 返回替换短语
	} else {
		return s;                 // 返回原string
	}
}

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 << " ";                // 在单词间打印空格
			}

			// transform返回它的第一个参数或转换后的形式
			cout << transform(word, trans_map);    // 打印输出
		}
		cout endl;    // 完成一行的转换
	}
}

4 无序容器

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

无序容器适用于两种场景:

  • 关键字类型的元素没有明显的序关系。
  • 在某些应用中,维护元素的序的代价非常高昂。

虽然理论上哈希技术可以获取更好的平均性能,但在实际中想达到很好的效果还要进行一些性能测试和调优工作。因此,使用无序容器通常更为简单,通常也会有更好的性能。

4.1 使用无序容器

除了哈希管理操作,无序容器还提供了与有序容器相同的操作,如find、insert等。所以我们曾用于map和set的操作也可以用于unordered_map和unordered_set。此外,无序容器也有允许重复关键字的版本。

通常来说,有序容器和对应的无序容器可以互换。但是,由于无序容器中,元素未按顺序存储,所以使用有序容器和无序容器的输出通常不同。

4.2 管理桶

无序容器在存储上组织为一组桶,每个桶保存零个或多个元素。

无序容器使用哈希函数将元素映射到桶。为了访问一个元素,容器首先计算元素的哈希值,它指出应该搜索哪个桶。容器将具有特定哈希值的所有元素都保存在相同的桶中。如果容器允许重复关键字,所有具有相同关键字的元素也会在同一个桶中。因此,无序容器的性能依赖于哈希函数的质量和桶的数量及大小。

对于相同的参数,哈希函数必须产生相同的结果。理想情况下,哈希函数还能将每个特定的值映射到唯一的桶。但是,将不同关键字的元素映射到相同的桶也是允许的。

当一个桶保存多个元素时,就要顺序搜索这些元素,以找到我们需要的那个。计算一个元素的哈希值和在桶中搜索通常是很快的操作。但是,如果一个桶中保存了很多元素,查找特定元素就需要大量比较操作。

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

表9 无序容器管理操作
桶接口
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

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

默认情况下,无序容器使用关键字的==运算符来比价元素,使用hash<key_type>类型的对象来生成每个元素的哈希值。

标准库为内置类型(包括指针)提供了hash模板。还为一些标准库类型,包括string和智能指针类型定义了hash。因此,可以直接定义关键字是内置类型(包括指针)、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);

为了简化bookstore的定义,首先为unordered_multiset定义了一个类型别名。此集合的哈希和相等性判断与hasher和eqOp类型相同。通过使用这种类型,在定义bookstore时可以传入我们自定义的函数指针。

如果我们自定义的类类型定义了==运算符,则可以只重载hash函数:

unordered_set<Foo, decltype(FooHash)*> fooSet(10, FooHash);
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值