11.3关联容器操作
- 除了表9.2(第295页)中列出的类型,关联容器还定义了表11.3中列出的类型。这些类型表示容器关键字和值的类型。
- 对于set类型,key_type和value type是一样的;set中保存的值就是关键字。
- 在一个map中,元素是关键字_值对。即每个元素是一个pair对象,包含一个关键字和一个关联的值。由于我们不能改变一个元素的关键字,因此这些pai匕的关键字部分是const的:
- 与顺序容器一样(参见9.2.2节,第297页),我们使用作用域运算符来提取一个类型的成员----例如,map<stringf int>: :key_type
- 只有 map 类型 (unordered_map 、unordered_multimap、 multimap 和 map)才定义了 mapped_type
11.3.1关联容器迭代器
- 当解引用一个关联容器迭代器时,我们会得到一个类型为容器的value_type的值的引用。对 map而言,value_type是一个pair类型,其 first成员保存const的关键字,second成员保存值:
- / / 获得指向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; / / 正确:我们可以通过迭代器改变元素
- 必须记住,一个map的value_type是一个pair,我们可以改变pair的值,但不能改变关键字成员的数值
set的迭代器是const的
- 虽然set类型同时定义了 iterator和 const_iterator类型,但两种类型都只允许只读访问set中的元素。与不能改变一个map元素的关键字一样,一个set中的关键字也是const的。可以用一个set迭代器来读取元素的值,但不能修改:
遍历关联容器
- map和set类型都支持表9.2(第295页)中的begin和end操作。与往常一样,我们可以用这些函数获取迭代器,然后用迭代器来遍历容器。例如,我们可以编写一个循环来打印第375页中单词计数程序的结果,如下所示
- while的循环条件和循环中的迭代器递增操作看起来很像我们之前编写的打印一个vector或一个string的程序。我们首先初始化迭代器map_it,让它指向word_count中的首元素。只要迭代器不等于end,就打印当前元素并递增迭代器。输出语句解引用map_it 来获得pair的成员,否则与我们之前的程序一样。
- 本程序的输出是按字典序排列的。当使用一个迭代器遍历一个map、multimap、set或 multiset时,迭代器按关键字升序遍历元素,
关联容器和算法
- 我们通常不对关联容器使用泛型算法(参见第10章)。关键字是const这一特性意味着不能将关联容器传递给修改或重排容器元素的算法,因为这类算法需要向元素写入值,而 set类型中的元素是const的,map中的元素是pair,其第一个成员是const的。 关联容器可用于只读取元素的算法。但是,很多这类算法都要搜索序列。由于关联容器中的元素不能通过它们的关键字进行(快速)查找,因此对其使用泛型搜索算法几乎总是个坏主意。例如,我们将在11.3.5节 (第 388页)中看到,关联容器定义了一个名为find的成员,它通过一个给定的关键字直接获取元素。我们可以用泛型find算法来查找一个元素,但此算法会进行顺序搜索。使用关联容器定义的专用的find成员会比调用 泛型find快得多。
- 在实际编程中,如果我们真要对一个关联容器使用算法,要么是将它当作一个源序列,要么当作一个目的位置。例如,可以用泛型copy算法将元素从一个关联容器拷贝到另一 个序列。类似的,可以调用inserter将一个插入器绑定(参见10.4.1节,第 358页) 到一个关联容器。通过使用inserter,我们可以将关联容器当作一个目的位置来调用另一个算法。
11.3.2添加元素
- 关联容器的insert成员(见表1L4,第 384页)向容器中添加一个元素或一个元素范围。由于map和 set (以及对应的无序类型)包含不重复的关键字,因此插入一个已存在的元素对容器没有任何影响:
- insert有两个版本,分别接受一对迭代器,或是一个初始化器列表,这两个版本的行为 类似对应的构造函数(参见11.2.1节,第 376页 )—— 对于一个给定的关键字,只有第一个带此关键字的元素才被插入到容器中。
向 map添加元素
- 对一个map进行insert操作时,必须记住元素类型是pair。通常,对于想要插入 的数据,并没有一个现成的pair对象。可以在insert的参数列表中创建一个pair:
- 如我们所见,在新标准下,创建一个pair最简单的方法是在参数列表中使用花括号初始化。也可以调用make_pair或显式构造pair。 最后一个insert调用中的参数:
- map<string, size_t>::value_type(s, 1)
- 构造一个恰当的pair类型,并构造该类型的一个新对象,插入到map中。
检测 insert的返回值
- insert (或emplace)返回的值依赖于容器类型和参数。对于不包含重复关键字的容器,添加单一元素的insert和 emplace版本返回一个pair,告诉我们插入操作是否成功。pair的 first成员是一个迭代器,指向具有给定关键字的元素;second成员 是一个bool值,指出元素是插入成功还是已经存在于容器中。如果关键字已在容器中, 则 insert什么事情也不做,且返回值中的bool部分为falseo如果关键字不存在,元素被插入容器中,且 bool值为true。
展开递增语句
- 在这个版本的单词计数程序中,递增计数器的语句很难理解。通过添加一些括号来反映出运算符的优先级(参见4.1.2节,第 121页),会使表达式更容易理解一些:
- ++ ( (ret. first) ->second) ; // 等价的表达式
- 下面我们一步一步来解释此表达式:
向 multiset或 multimap添加元素
- 我们的单词计数程序依赖于这样一个事实:一个给定的关键字只能出现一次。这样,任意给定的单词只有一个关联的计数器。我们有时希望能添加具有相同关键字的多个元素。例如,可能想建立作者到他所著书籍题目的映射。在此情况下,每个作者可能有多个条目,因此我们应该使用multimap而不是map。由于一个multi容器中的关键字不必唯一,在这些类型上调用insert总会插入一个元素
11 .3 .3 删除元素
- 关联容器定义了三个版本的erase,如表11.5所示。与顺序容器一样,我们可以通过传递给erase 一个迭代器或一个迭代器对来删除一个元素或者一个元素范围。这两个 版本的erase与对应的顺序容器的操作非常相似:指定的元素被删除,函数返回void。 关联容器提供一个额外的erase操作,它接受一个key_type参数。此版本删除所有匹配给定关键字的元素(如果存在的话),返回实际删除的元素的数量。我们可以用此版本在打印结果之前从word_count中删除一个特定的单词:
- 对于保存不重复关键字的容器,erase的返回值总是0或 1。若返回值为0,则表明想要 删除的元素并不在容器中
- 对允许重复关键字的容器,删除元素的数量可能大于1:auto ent = authors . erase (*'Barth, John"); 如果authors是我们在11.3.2节 (第 386页)中创建的multimap,则 ent的值为2。
11.3.4 map的下标操作
- map和unordered_map容器提供了下标运算符和一个对应的at函数(参见9.3.2节,第311页),如表11/所示。set类型不支持下标,因为set中没有与关键字相关联的"值”。元素本身就是关键字,因此“获取与一个关键字相关联的值”的操作就没有意义了。我们不能对一个multimap或一个unordered_multimap进行下标操作,因为这些容器中可能有多个值与一个关键字相关联。
- 类似我们用过的其他下标运算符,map下标运算符接受一个索引(即,一个关键字),获取与此关键字相关联的值。但是,与其他下标运算符不同的是,如果关键字并不在map中,会为它创建一个元素并插入到map中,关联值将进行值初始化(参见3.3.1节,第88页)。例如,如果我们编写如下代码
- map〈string,size_t>word_count;〃emptymap
- word_count["Anna"]=1; //插入一个关键字为Anna的元素,关联值进行值初始化;然后将1赋予它
- 将会执行如下操作:
- 在word_count中搜索关键字为Anna的元素,未找到。
- 将一个新的关键字 值对插入到word_count中。关键字是一个const string,保存Anna.值进行值初始化,在本例中意味着值为0。
- 提取出新插入的元素,并将值1赋予它。
- 由于下标运算符可能插入一个新元素,我们只可以对非const的map使用下标操作。
- 使对用一一个map使用下标操作,其行为与数组或vector上的下标操作很不相同:使用一个不在容器中的关键字作为下标,会添加一个具有此关键字的元素到map中
使用下标操作的返回值
- map的下标运算符与我们用过的其他下标运算符的另一个不同之处是其返回类型。通常情况下,解引用一个迭代器所返回的类型与下标运算符返回的类型是一样的。但对map 则不然:当对一个map进行下标操作时,会获得一个mapped_type对象;但当解引用—个 map迭代器时,会得到一个value_type对 象 (参见11.3节’第 381页)。
- 与其他下标运算符相同的是,map的下标运算符返回一个左值(参见4.L1节,第 121 页)。由于返回的是一个左值,所以我们既可以读也可以写元素:
- 如果关键字还未在map中,下标运算符会添加一个新元素,这一特性允许我们编写出异常简洁的程序,例如单词计数程序中的循环(参见11.1节,第 375页)。另一方面,有时只是想知道一个元素是否已在map中,但在不存在时并不想添加元素。在这种情况下,就不能使用下标运算符。
1 1 .3 .5 访问元素
- 关联容器提供多种查找一个指定元素的方法,如表11.7所示。应该使用哪个操作依赖于我们要解决什么问题。如果我们所关心的只不过是一个特定元素是否已在容器中,可能find是最佳选择。对于不允许重复关键字的容器,可能使用find还是count没什么区别。但对于允许重复关键字的容器,count还会做更多的工作:如果元素在容器中,它还会统计有多少个元素有相同的关键字。如果不需要计数,最好使用find:
在 multimap或 multiset中查找元素
- 在一个不允许重复关键字的关联容器中查找一个元素是一件很简单的事情—— 元素要么在容器中,要么不在。但对于允许重复关键字的容器来说,过程就更为复杂:在容器中可能有很多元素具有给定的关键字。如果一个multimap或 multiset中有多个元素具有给定关键字,则这些元素在容器中会相邻存储。
- 例如,给定一个从作者到著作题目的映射,我们可能想打印一个特定作者的所有著作。可以用三种不同方法来解决这个问题。最直观的方法是使用find和 count:
- 首先调用count确定此作者共有多少本著作,并调用find获得一个迭代器,指向第一 个关键字为此作者的元素。for循环的迭代次数依赖于count的返回值。特别是,如果 count返回0.则循环一次也不执行。
- 当我们遍历一个multimap或 multiset时,保证可以得到序列中所有具有给定关键字的元素
一种不同的,面向迭代器的解决方法
- 我们还可以用lower_bound和 upper_bound来解决此问题。这两个操作都接受一个关键字,返回一个迭代器。如果关键字在容器中,lower_bound返回的迭代器将指向第一个具有给定关键字的元素,而 upper_bound返回的迭代器则指向最后一个匹配给定关键字的元素之后的位置。如果元素不在multimap中,则lower_bound和 upper_bound会返回相等的迭代器—— 指向一个不影响排序的关键字插入位置。
- 因此,用相同的关键字调用lower_bound和 upper_bound会得到一个迭代器范围(参见9.2.1节,第296页),表示所有该关键字的元素的范围。
- 当然,这两个操作返回的迭代器可能是容器的尾后迭代器。如果我们查找的元素具有容器中最大的关键字,则此关键字的upper_bound返回尾后迭代器。如果关键字不存在, 且大于容器中任何关键字,则 lowe_bound返回的也是尾后迭代器。
- lower_bound返回的迭代器可能指向一个具有给定关键字的元素,但也可能不指向。如果关键字不在容器中,则 lower_bound会返回关键字的第一个安全插入点—— 不影响容器中元素顺序的插入位置
- 此程序与使用count和find的版本完成相同的工作,但更直接。对lower_bound的调用将beg定位到第一个与search_item匹配的元素(如果存在的话)。如果容器中没有这样的元素,beg将指向第一个关键字大于search_item的元素,有可能是尾后迭代器。upper_bound调用将end指向最后一个匹配指定关键字的元素之后的元素。这两个操作并不报告关键字是否存在,重要的是它们的返回值可作为一个迭代器范围(参见9.2.1节,第296页)。
- 如果没有元素与给定关键字匹配,则lower_bound和upper_bound会返回可相等的迭代器--都指向给定关键字的插入点,能保持容器中元素顺序的插入位置。
- 假定有多个元素与给定关键字匹配,beg将指向其中第一个元素。我们可以通过递增beg来遍历这些元素。end中的迭代器会指出何时完成遍历当beg等于end时,就表明已经遍历了所有匹配给定关键字的元素了。
- 由于这两个迭代器构成一个范围,我们可以用一个for循环来遍历这个范围。循环可能执行零次,如果存在给定作者的话,就会执行多次,打印出该作者的所有项。如果给定作者不存在,beg和end相等,循环就一次也不会执行。否则,我们知道递增beg最终会使它到达end,在此过程中我们就会打印出与此作者关联的每条记录。
- 如果lower_bound和 upper_bound返回相同的迭代器,则给定关键字不在容器中
equal_range 函数
- 解决此问题的最后一种方法是三种方法中最直接的:不必再调用upper bound和lower_bound,直接调用equal_range即可。此函数接受一个关键字,返回一个迭代 器 pair。
- 若关键字存在,则第一个迭代器指向第一个与关键字匹配的元素,第二个迭代器指向最后一个匹配元素之后的位置。若未找到匹配元素,则两个迭代器都指向关键字可以插入的位置。
- 此程序本质上与前一个使用upper_bound和 lower_bound的程序是一样的。不同之处就是,没有用局部变量beg和 end来保存元素范围,而是使用了 equal_range返回的 pair。此 pair的 first成员保存的迭代器与lower_bound返回的迭代器是一样的, second保存的迭代器与upper_bound的返回值是一样的。因此,在此程序中, pos.first 等价于 beg, pos , second 等价于 end。
11.3.6 一个单词转换的map
- 我们将以一个程序结束本节的内容,它将展示map 的创建、搜索以及遍历。这个程序的功能是这样的:给定一个string,将它转换为另一个string。程序的输入是两个文件。第一个文件保存的是一些规则,用来转换第二个文件中的文本。每条规则由两部分组成:一个可能出现在输入文件中的单词和一个用来替换它的短语。表达的含义是,每当第一个单词出现在输入中时,我们就将它替换为对应的短语。第二个输入文件包含要转换的文本。
1 1 .4 无序容器
- 新标准定义了 4 个无序关联容器(unordered associative container)o 这些容器不是使用比较运算符来组织元素,而是使用一个哈希函数(hash function)和关键字类型的==运算 贸 符。在关键字类型的元素没有明显的序关系的情况下,无序容器是非常有用的。在某些应用中,维护元素的序代价非常高昂,此时无序容器也很有用。
- 虽然理论上哈希技术能获得更好的平均性能,但在实际中想要达到很好的效果还需要进行一些性能测试和调优工作。因此,使用无序容器通常更为简单(通常也会有更好的性能)。
- 如果关键字类型固有就是无序的,或者性能测试发现问题可以用哈希技术解决,就可以使用无序容器」
使用无序容器
- 除了哈希管理操作之外,无序容器还提供了与有序容器相同的操作(find,insert等)。这意味着我们曾用于map和set的操作也能用于unordered_map和unordered_seto类似的,无序容器也有允许重复关键字的版本。因此,通常可以用一个无序容器替换对应的有序容器,反之亦然。但是,由于元素未按顺序存储,一个使用无序容器的程序的输出(通常)会与使用有序容器的版本不同。
- 例如,可以用unordered_map重写最初的单词计数程序(参见11.1节,第375页):
- 此程序与原程序的唯一区别是word_count的类型。输出不按照字典顺序进行排列
管理桶
- 无序容器在存储上组织为一组桶,每个桶保存零个或多个元素。无序容器使用一个哈希函数将元素映射到桶。为了访问一个元素,容器首先计算元素的哈希值,它指出应该搜索哪个桶。容器将具有一个特定哈希值的所有元素都保存在相同的桶中。如果容器允许重复关键字,所有具有相同关键字的元素也都会在同一个桶中。因此,无序容器的性能依赖于哈希函数的质量和桶的数量和大小。
- 对于相同的参数,哈希函数必须总是产生相同的结果。理想情况下,哈希函数还能将每个特定的值映射到唯一的桶。但是,将不同关键字的元素映射到相同的桶也是允许的。当一个桶保存多个元素时,需要顺序搜索这些元素来查找我们想要的那个。计算一个元素的哈希值和在桶中搜索通常都是很快的操作。但是,如果一个桶中保存了很多元素,那么查找一个特定元素就需要大量比较操作。
- 无序容器提供了一组管理桶的函数,如表11.8所示。这些成员函数允许我们查询容器的状态以及在必要时强制容器进行重组。
无序容器对关键字类型的要求
- 默认情况下,无序容器使用关键字类型的==运算符来比较元素,它们还使用一个hash<key_type>类型的对象来生成每个元素的哈希值。标准库为内置类型(包括指针)提供了hash模板。还为一些标准库类型,包括string和我们将要在第12章介绍的智能指针类型定义了hash。因此,我们可以直接定义关键字是内置类型(包括指针类型)、string还是智能指针类型的无序容器。.但是,我们不能直接定义关键字类型为自定义类类型的无序容器。与容器不同,不能直接使用哈希模板,而必须提供我们自己的hash模板版本。我们将在16.5节(第626页)中介绍如何做到这一点。我们不使用默认的hash,而是使用另一种方法,类似于为有序容器重载关键字类型的默认比较操作(参见11.2.2节,第 378页)。为了能将Sale_data用作关键字,我们需要提供函数来替代==运算符和哈希值计算函数。我们从定义这些重载函数开始:
小结
- 关联容器支持通过关键字高效查找和提取元素。对关键字的使用将关联容器和顺序容器区分开来,顺序容器中是通过位置访问元素的。
- 标准库定义了8个关联容器,每个容器
- 是一个map或是yi个set。map保存关键字-值对;set保存关键字。
- 要求关键字唯一或不要求
- 保持关键字有序或不保证有序。
- 有序容器使用比较函数来比较关键字,从而将元素按顺序存储。默认情况下,比较操作是用关键字类型的<运算符。无序容器使用关键字类型的==运算符和一个hash<key_type>类型的对象来组织元素。
- 允许重复关键字的容器的名字中都包含multi;而使用哈希技术的容器的名字都以unordered开头。例如,set是一个有序集合,其中每个关键字只可以出现一次;unordered_multiset则是一"无序的关键字集合,其中关键字可以出现多次。关联容器和顺序容器有很多共同的元素。但是,关联容器定义了一些新操作,并对一些和顺序容器和关联容器都支持的操作重新定义了含义或返回类型。操作的不同反映出关联容器使用关键字的特点。
- 有序容器的迭代器通过关键字有序访问容器中的元素。无论在有序容器中还是在无序容器中,具有相同关键字的元素都是相邻存储的。
术语表
- 关联数组元素通过关键字而不是位置来索引的数组。我们称这样的数组将一个关键字映射到其关联的值。
- 关联容器(associativecontainer)类型,保存对象的集合,支持通过关键字的高效查找。
- hash 特殊的标准库模板,无序容器用它来管理元素的位置。
- 哈希函数 (hashfunction)将给定类型的值映射到整形(size_t)值的函数。相等 的值必须映射到相同而整数:不相等的值应尽可能映射到不同整数。
- key_type关联容器定义的类型,用来保存和提取值的关键字的类型。对于一个map,key_type是用来索引map的类型。对于 set,key_type 和 value_type 是一样的
- map关联容器类型,定义了一个关联数组。类 似 vector, map是一个类模板。但是,一个map要用两个类型来定义:关键字的类型和关联的值的类型.在一个map中,一个给定关键字只能出现一次。 每个关键字关联一个特定的值。解引用一个map迭代器会生成一个pair,它保存 一个const关键字及其关联的值。
- mapped_type映射类型定义的类型,就是映射中关键字关联的值的类型。
- multimap关联容器类型,类似map,不同之处在于,在一个multimap中,一个给定的关键字可以出现多次。multimap不支持下标操作。
- multiset保存关键字的美联容器类型。在一个multiset中,一个给定关键字可以出现多次。
- pair类型,保存名为first和second的public数据成员.pair类型是模板类型,接受两个类型参数,作为其成员的类型。
- set 保存关键字的关联容器。在一个set 中,一个给定的关键字只能出现一次。
- 严格弱序 关联容器所使用的关键字间的关系。在一个严格弱序中,可以比较任意两个值并确定哪个更小。若任何一个都不小于另一个,则认为两个值相等。
- 无序容器 关联容器,用哈希技术而不是比较操作来存储和访问元素。这类容器的性能依赖于哈希函数的质量。
- unordered_map保存关键字-值对的容器,不允许重复关键字。
- unordered_multimap保存关键字-值对的容器,允许重复关键字。
- unordered_multiset保存关键字的容器,允许重复关键字。
- unordered_set保存关键字的容器,不允许重复关键字。
- value_type容器中元素的类型。对于set和multiset,value_type和key_type是一样的。对于map和multimap.此类型是一个pair,其first成员类型为constkey_type,second成员类型为mapped_type。
- *运算符解引用运算符。当应用于map、set、multimap或multiset的迭代器时,会生成一个value_type值。注意,对map和multimap,value_type是一个pair
- []运算符下标运算符。只能用于map和unordered_map类型的非const对象。对于映射类型,[]接受一个索引,必须是一个key_type值(或者是能转换为key_type的类型)。生成一个
mapped_type值。