- 关联容器和顺序容器有着根本的不同:关联容器中的元素是按关键字来保存和访问的。与之相对,顺序容器中的元素是按它们在容器中的位置来顺序保存和访问的。
- 虽然关联容器的很多行为与顺序容器相同,但其不同之处反映了关键字的作用
- 关联容器支持高效的关键字查找和访问。两个主要的关联容器(associative-container)类型是map和set。map中的元素是一些关键字-值(key-value)对:关键字起到索引的作用,值则表示与索引相关联的数据。set中每个元素只包含一个关键字;set支持高效的关键字查询操作--检查一个给定关键字是否在set中。例如,在某些文本处理过程中,可以用一个set来保存想要忽略的单词。字典则是一个很好的使用map的例子:可以将单词作为关键字,将单词释义作为值。
- 标准库提供8个关联容器,如表11.1所示。这8个容器间的不同体现在三个维度上:每个容器
- (1)或者是一个set,或者是一个map;
- (2)或者要求不重复的关键字,或者允许重复关键字;
- (3)按顺序保存元素,或无序保存。允许重复关键字的容器的名字中都包含单词multi;不保持关键字按顺序存储的容器的名字都以单词unordered开头。因此一个unordered_multi_set是一个允许重复关键字,元素无序保存的集合,而一个set则是一个要求无重复关键字,有序存储的集合。无序容器使用哈希函数来组织元素,我们将在11.4节(第394页)中详细介绍有关哈希函数的更多内容。
- 类型map和multimap定义在头文件map中;set和multiset定义在头文件set中;无序容器则定义在头文件unordered_map和unordered_set中。
11.1使用关联容器
- 虽然大多数程序员都熟悉诸如vector和list这样的数据结构,但他们中很多人从未使用过关联数据结构。在学习标准库关联容器类型的详细内容之前,我们首先来看一个如何使用这类容器的例子,这对后续学习很有帮助。
- map是关键字-值对的集合。例如,可以将一个人的名字作为关键字,将其电话号码作为值。我们称这样的数据结构为"将名字映射到电话号码”。map类型通常被称为关联数组。关联数组与''正常”数组类似,不同之处在于其下标不必是整数。我们通过一个关键字而不是位置来查找值。给定一个名字到电话号码的map,我们可以使用一个人的名字作为下标来获取此人的电话号码。
- 与之相对,set就是关键字的简单集合。当只是想知道一个值是否存在时,set是最有用的。例如,一个企业可以定义一个名为bad_checks的set来保存那些曾经开过空头支票的人的名字。在接受一张支票之前,可以查询bad_checks来检查顾客的名字是否在其中。
- 此程序读取输入,报告每个单词出现多少次。
- 类似顺序容器,关联容器也是模板(参见3.3节,第86页)。为了定义一个map,我们必须指定关键字和值的类型。在此程序中,map保存的每个元素中,关键字是string类型,值是size_t类型(参见3.5.2节,第103页)。当对word_count进行下标操作时,我们使用一个string作为下标,获得与此string相关联的size_t类型的计数器。while循环每次从标准输入读取一个单词。它使用每个单词对word_count进行下标操作。如果word还未在map中,下标运算符会创建一个新元素,其关键字为word,值为0。不管元素是否是新创建的,我们将其值加1
- 一旦读取完所有输入,范围for语句(参见3.2.3节,第81页)就会遍历map,打印每个单词和对应的计数器。当从map中提取一个元素时,会得到一个pair类型的对象,我们将在11.2.3节(第379页)介绍它。简单来说,pair是一个模板类型,保存两个名为first和second的(公有)数据成员。map所使用的pair用first成员保存关键字,用second成员保存对应的值。因此,输出语句的效果是打印每个单词及其关联的计数器。
使用 set
- 上一个示例程序的一个合理扩展是:忽略常见单词,如"the", "and", "or"等。我们可 以使用set保存想忽略的单词,只对不在集合中的单词统计出现次数:
- 与其他容器类似,set也是模板。为了定义一个set,必须指定其元素类型,本例中是string。与顺序容器类似,可以对一个关联容器的元素进行列表初始化(参见9.2.4节,第300页)。集合exclude中保存了12个我们想忽略的单词。此程序与前一个程序的重要不同是,在统计每个单词出现次数之前,我们检查单词是否在忽略集合中,这是在if语句中完成的:
- find调用返回一个迭代器。如果给定关键字在set中,迭代器指向该关键字。否则,find 返回尾后迭代器。在此程序中,仅当word不在exclude中时我们才更新word的计数器。
1 1 .2 关联容器概述
- 关联容器(有序的和无序的)都支持9.2节 (第 294页)中介绍的普通容器操作(列于表9.2,第 295页)。关联容器不支持顺序容器的位置相关的操作,例如push_front 或 push_back。原因是关联容器中元素是根据关键字存储的,这些操作对关联容器没有意义。而且,关联容器也不支持构造函数或插入操作这些接受一个元素值和一个数量值的操作。
- 除了与顺序容器相同的操作之外,关联容器还支持一些顺序容器不支持的操作(参见表 11.7,第 388页)和类型别名(参见表11.3,第 381页)。此外,无序容器还提供一些
- 用来调整哈希性能的操作,我们将在11.4节 (第 394页)中介绍。关联容器的迭代器都是双向的(参见10.5.1节,第 365页)。
11.2.1定义关联容器
- 如前所示,当定义一个map时,必须既指明关键字类型又指明值类型;而定义一个set时,只需指明关键字类型,因为set中没有值。每个关联容器都定义了一个默认构造函数,它创建一个指定类型的空容器。我们也可以将关联容器初始化为另一个同类型容器的拷贝,或是从一个值范围来初始化关联容器,只要这些值可以转化为容器所需类型就可以。在新标准下,我们也可以对关联容器进行值初始化
- 与以往一样,初始化器必须能转换为容器中元素的类型。对于set,元素类型就是关键字类型。
- 当初始化一个map时,必须提供关键字类型和值类型。我们将每个关键字-值对包围在花括号中:{key,value}来指出它们一起构成了map中的一个元素。在每个花括号中,关键字是第一个元素,值是第二个。因此,authors将姓映射到名,初始化后它包含三个元素。
初始化multimap或multiset
- 一个map或set中的关键字必须是唯一的,即,对于一个给定的关键字,只能有一个元素的关键字等于它。容器multimap和multiset没有此限制,它们都允许多个元素具有相同的关键字。例如,在我们用来统计单词数量的map中,每个单词只能有一个元素。另一方面,在一个词典中,一个特定单词则可具有多个与之关联的词义。
- 下面的例子展示了具有唯一关键字的容器与允许重复关键字的容器之间的区别。首先,我们将创建一个名为ivec的保存int的vector,它包含20个元素:0到9每个整数有两个拷贝。我们将使用此vector初始化一个set和一个multiset:
- 即使我们用整个ivec容器来初始化iset,它也只含有10个元素:对应ivec中每个不同的元素。另一方面,miset有 20个元素,与 ivec中的元素数量一样多。
11.2.2关键字类型的要求
- 关联容器对其关键字类型有一些限制。对于无序容器中关键字的要求,我们将在11.4节 (第 396页)中介绍。对于有序容器map、multimap, set以及multiset,关键字类型必须定义元素比较的方法。默认情况下,标准库使用关键字类型的 < 运算符来比较两个关键字。在集合类型中,关键字类型就是元素类型;在映射类型中,关键字类型是元素的第一部分的类型。因此,11.2节 (第 377页)中 word_count的关键字类型是 string.类似的,exclude的关键字类型也是string。
- 传递给排序算法的可调用对象(参见10.3.1节,第344页)必须满足与关联容器中关键字一样的类型要求。
有序容器的关键字类型
- 可以向一个算法提供我们自己定义的比较操作(参见10.3节,第344页),与之类似,也可以提供自己定义的操作来代替关键字上的<运算符。所提供的操作必须在关键字类型
上定义一个严格弱序(strictweakordering)。可以将严格弱序看作"小于等于”,虽然实际定义的操作可能是一个复杂的函数。无论我们怎样定义比较函数,它必须具备如下基本性质: - 两个关键字不能同时“小于等于”对方;如果kl"小于等于”k2,那么k2绝不能“小于等于”kl。
- 如果kl“小于等于”k2,且k2“小于等于”k3,那么kl必须“小于等于”k3。
- 如果存在两个关键字,任何一个都不"小于等于"另一个,那么我们称这两个关键字是"等价”的。如果kl“等价于”k2,且k2“等价于”k3,那么kl必须“等价于”k3
- 如果两个关键字是等价的(即,任何一个都不“小于等于”另一个),那么容器将它们视作相等来处理。当用作map的关键字时,只能有一个元素与这两个关键字关联,我们可以用两者中任意一个来访问对应的值。
使用关键字类型的比较函数
- 用来组织一个容器中元素的操作的类型也是该容器类型的一部分。为了指定使用自定义的操作,必须在定义关联容器类型时提供此操作的类型。如前所述,用尖括号指出要定义哪种类型的容器,自定义的操作类型必须在尖括号中紧跟着元素类型给出。
- 在尖括号中出现的每个类型,就仅仅是一个类型而已。当我们创建一个容器(对象)时,才会以构造函数参数的形式提供真正的比较操作(其类型必须与在尖括号中指定的类型相吻合)
- 此处,我们使用decltype来指出自定义操作的类型。记住,当用decltype来获得一个函数指针类型时,必须加上一个*来指出我们要使用一个给定函数类型的指针(参见6.7节,第223页)。用comparelsbn来初始化bookstore对象,这表示当我们向bookstore添加元素时,通过调用comparelsbn来为这些元素排序。即bookstore中的元素将按它们的ISBN成员的值排序。可以用comparelsbn代替&comparelsbn作为构造函数的参数,因为当我们使用一个函数的名字时,在需要的情况下它会自动转化为一个指针(参见6.7节,第221页)。当然,使用&comparelsbn的效果也是一样的。
11.2.3pair类型
- 在介绍关联容器操作之前,我们需要了解名为pair的标准库类型,它定义在头文件utility中。
- 一个pair保存两个数据成员。类似容器,pair是一个用来生成特定类型的模板。当创建一个pair时,我们必须提供两个类型名,pair的数据成员将具有对应的类型。两个类型不要求一样:
- pair的默认构造函数对数据成员进行值初始化(参见3.3.1节,第 88页)。因此,anon是一个包含两个空string的 pair, line保存一个空string和一个空vector。 word_count中的size_t成员值为0,而 string成员被初始化为空。我们也可以为每个成员提供初始化器:
- pair<string, string> author( "James", ”Joyce”};
- 这条语句创建一个名为author的pair,两个成员被初始化为"James"和"Joyce”。与其他标准库类型不同,pair的数据成员是public的(参见7.2节,第240页)。两个成员分别命名为first和second。我们用普通的成员访问符号(参见1.5.2节,第20页)来访问它们,例如,在第375页的单词计数程序的输出语句中我们就是这么做的:
- cout<<w.first << " occurs" << w.second<<((w.second>1)?"times":"time")<<endl;
- 此处,w是指向map中某个元素的引用。map的元素是pair.在这条语句中,我们首先打印关键字元素的first成员,接着打印计数器second成员。标准库只定义了有限的几个pair操作,表11.2列出了这些操作。
创建pair对象的函数
- 用想象有一个函数需要返回一个pair。在新标准下,我们可以对返回值进行列表初始化(参见6.3.2节,第203页)
pair<string, int>
process(vector<string> &v)
(
/ / 处理v
if (!v.empty())
return {v.back () , v.back () .size () }; // 列表初始化
else
return pair<stringz int> () ; // 隐式构造返回值
}
- 若v 不为空,我们返回一个由v 中最后一个string及其大小组成的pair。否则,隐式构造一个空pair,并返回它。 在较早的C++版本中,不允许用花括号包围的初始化器来返回pair这种类型的对象, 必须显式构造返回值: