Associative Container
关联容器和顺序容器有根本的不同:
- 关联容器中的元素是按关键字来保存和访问的。
- 顺序容器中的元素是按它们在容器中的位置来顺序保存和访问的。
关联容器支持高效的关键字查找和访问。两个主要的**关联容器(associative container)**的类型是map和set。
标准库提供8个关联容器。
这8个容器不同体现在三个维度上:
每个容器:
- 或是一个set,或是一个map
- 或者要求不重复的关键字,或者允许重复的关键字
- 按顺序保存元素,或无序保存
允许重复关键字的容器名字中都包含单词multi;
不保持关键字按顺序存储的容器的名字都以unordered开头。
set是一个要求不重复关键字,有序存储的集合
无序容器使用hash函数来组织元素
map和multimap定义在头文件map中;set和multiset定义在头文件set中;无序容器定义在头文件unordered_set和unordered_set中。
关联容器类型 | 说明 |
---|---|
按关键字有序保存元素 | |
map | 关联数组:保存key-value对 |
set | 关键字即值,只保存关键字的容器 |
multimap | 关键字可重复出现的map |
multiset | 关键字可重复出现的set |
无序集合 | |
unordered_map | 用hash函数组织的map |
unordered_set | … |
unordered_multimap | … |
unordered_multiset | … |
使用关联容器
使用map
关联容器也是模板。为了定义map,我们必须指定关键字和值的类型:
map<string, size_t> word_count;
对word_count进行下标操作时,我们用一个string作为下标,获得与此string相关联的size_t类型的数据。
使用string对word_count进行下标操作时,如果这个string还不在map中,下标运算符会创建一个新的元素。
从map中提取一个元素时,我们会得到一个pair类型的对象。简单来说,pair是一个模板类型,保存两个名为first和second的共有数据成员。map所使用的pair使用first保存关键字,使用second保存对应的值。
使用set
set也是一个模板。与顺序容器一样,可以对一个关联容器的元素进行列表初始化。
set<string> exclude = {"the", "but"};
检查set中有没有指定关键字:
if(exclude.find(word) == exclude.end());
find调用返回一个迭代器。如果关键字在set中,迭代器指向该关键字,否则,find返回尾后迭代器。
关联容器概述
关联容器都支持顺序容器(见顺序容器)中介绍的普通容器操作。
关联容器不支持顺序容器中位置相关的操作,例如push_back,push_front。因为关联容器中元素是根据关键字存储的,这些操作对关联容器没有意义。
关联容器还支持一些顺序容器不支持的操作和类型别名。
此外,无序容器还提供一些用来调整哈希性的操作。
关联容器的迭代器都是双向的。
定义关联容器
每个关联容器都定义了一个默认构造函数,它创建一个指定类型的空容器。
也可以将关联容器初始化为另一个同类型容器的拷贝。
或是从一个值范围来初始化关联容器。
初始化multimap和multiset
一个map或set的关键字必须唯一。但容器multi-系列没有这个限制。
它们都允许多个元素有相同的关键字。
关键字类型的要求
关联容器对于关键字类型有一定限制。对于有序关联容器,关键字类型必须定义元素比较的方法。默认情况下,标准库使用关键字类型的<
运算符来比较两个关键字。
集合类型中,关键字类型就是元素类型。映射类型中,关键字类型是元素的第一部分的类型。
pair类型
标准库类型 pair 定义在头文件utility
中。
一个pair保存两个数据成员。类似容器,pair是一个用来生成特定类型的模板。当创建一个pair时,我们必须提供两个类型名,pair的数据成员将具有对应的类型。两个类型不要求一样。
与其他标准库类型不同,pair的数据成员是public的。两个成员名分别是first和second。
关联容器操作
关联容器迭代器
当解引用一个关联容器迭代器时,我们会得到一个类型为容器的value_type的值的引用。对map而言,value_type是一个pair类型。
我们可以改变pair的值,但是不能改变关键字成员的值。
set的迭代器是const的
虽然set类型同时定义了iterator和const_iterator类型,但两种类型都只允许只读访问set中的元素。与不能改变一个map的关键字一样,一个set中的关键字也是const的。
遍历关联容器
map和set类型都支持begin和end操作。可用这些函数获取迭代器,然后用迭代器来遍历容器。
关联容器和算法
我们通常不对关联容器使用泛型算法。关键字是const这一特性意味着不能将关联容器传递给修改或重排容器元素的算法。
set类型中的元素是const的,map中的元素是pair,其第一个成员是const。
添加元素
关联容器的insert成员向容器添加一个元素或一个元素范围。
由于set和map包含不重复的关键字,因此插入一个已存在的元素对容器没有任何影响。
向map添加元素
对一个map进行insert操作的时候,必须记住元素类型是pair。通常对于要插入的数据,并没有一个现成的pair对象,可以在insert的参数列表中创建一个pair:
// 向word_count插入word的4中方法
word_count.insert({word, 1});
word_count.insert(make_pair(word,1));
word_count.insert(pair<string, size_t>(word,1));
word_count.insert(map<string, size_t>::value_type(word,1));
检测insert的返回值
insert返回的值依赖于容器类型和参数。对于不包含重复关键字的容器,添加单一元素的insert和emplace版本返回一个pair,告诉我们插入操作是否成功。
pair的first成员是一个迭代器,指向具有给定关键字的元素。second是一个bool值,指出元素是插入成功还是已经存在于容器中,如果关键字已在容器中,则insert什么都不做,且返回bool部分未false。如果关键字不存在,元素被插入容器,且bool值为true。
向multiset或multimap添加元素
有时我们想建立一个关键字到多个值的映射。即有相同关键字的多个元素。
对于允许重复关键字的容器,接受单个元素的insert操作返回一个指向新元素迭代器。这里无需返回一个bool。
删除元素
关联容器定义了三个版本的erase。与顺序容器一样,我们可以通过传递给erase一个迭代器或一个迭代器对来删除一个元素或一个元素范围。
指定元素被删除,返回void。
关联容器提供一个额外的erase操作,它接受一个key_value参数,此版本删除所有匹配给定关键字的元素,返回实际删除的元素的数量。
从关联容器删除元素 | 说明 |
---|---|
c.erase(k) | 从c中删除每个关键字为k的元素,返回一个size_type值,指出删除的元素数量。 |
c.erase§ | 从c中删除迭代器p指定的元素。返回一个指向p之后的元素的迭代器。 |
c.erase(b,e) | 删除迭代器对b和e所表示范围中的元素,返回e |
map的下标操作
map和unordered_map容器提供了下标运算符和一个对应的at函数。
set类型不支持下标,因为set中没有与关键字相关联的“值”。元素本身就是关键字。
不能对一个multimap或一个unordered_multimap进行下标操作,因为这些容器中可能有多个值与一个关键字相关联。
map下标运算符接受一个索引(即一个关键字),获取与此关键字相关联的值。但是,如果关键字并不在map中,会为它创建一个元素并插入到map中,关联至进行值初始化。
map和unordered_map的下标操作 | 说明 |
---|---|
c[k] | 返回关键字为k的元素;如果k不在c中,添加一个关键字为k的元素,并对其进行值初始化 |
c.at[k] | 访问关键字为k的元素,带参数 |
对一个map,解引用一个迭代器会得到一个value_type对象,进行下标运算会得到一个mapped_type对象。
访问元素
关联操作提供多种查找一个指定元素的方。
在一个关联容器中查找元素的操作 | 说明 |
---|---|
c.find(k) | 返回一个迭代器,指向第一个关键字为k的元素,若k不再容器中,则返回尾后迭代器 |
c.count(k) | 返回关键字等于k的元素的数量。 |
c.lower_bound(k) | 返回一个迭代器,指向第一个关键字不小于k的元素 |
c.upper_bound(k) | 返回一个迭代器,指向第一个关键字不大于k的元素 |
c.equal_range(k) | 返回一个迭代器pair,表示关键字等于k的元素的范围。若k不存在,pair的两个成员均等于c.end() |
注意:
- lower_bound 和upper_bound不适用于无序容器
- 下标和at操作只适用于非const的map和unordered_map
对map使用find代替下标操作
对map和unordered_map类型,下标运算符提供了最简单的提取元素的方法。但是,下标操作有一个严重的副作用:如果关键字还未在map中,下标操作会插入一个具有给定关键字的元素。如果不想改变map,这样就不能使用下标运算符来检查一个元素是否存在。这种情况下应该用find。
在multiset和multimap中查找元素
用相同的关键字调用lower_bound和upper_bound会得到一个一个迭代器范围,表示具有该关键字的元素的范围。
如果没有元素与给定关键字匹配,则lower_bound和upper_bound会返回相等的迭代器——都指向给定关键字的插入点,能保持容器中元素顺序的插入位置。
使用equal_range函数。此函数接受一个关键字,返回一个迭代器pair。
无序容器
新标准定义了4个无序关联容器(unordered associative container),这些容器不是使用比较运算符来组织元素,而是使用一个**哈希函数(hash function)**和关键字类型的==
运算符。
在关键字类型的元素没有明显的序关系的情况下,无序容器是非常有用的。某些应用中,维护元素的序代价非常高,此时无序容器很有用。
虽然理论上hash技术能获得更好的平均性能,但在实际中想达到很好的效果还需要进行一些性能测试和调优工作。因此使用无序容器通常更简单(通常也会有更好的性能)。
如果关键字类型固有就是无序,或者性能测试发现问题可以用hash技术解决,那么就可以使用无序容器。
使用无序容器
除了hash管理操作之外,无序容器还提供了与有序容器相同的操作(find,insert等),意味着我们用于map和set的操作也能用于unordered版本的set和map。也循序重复关键字的版本。
通常可以用无序容器替换对应的有序容器,反之亦然。
管理桶
无序其在存储上组织为一组同,每个桶保存零个或多个元素。
无序容器通过hash将元素映射到桶。
为了访问一个元素,容器首先应计算元素的hash值,它指出该搜索哪个桶。
容器将具有特定hash值的所有元素都保存在相同的桶中。
如果允许重复关键字,所有具有相同关键字的元素都会在一个桶中。
因此,无序容器的性能依赖于hash函数的质量和桶的数量和大小。
对于相同的参数,hash函数必须总是产生相同的结果。
理想情况下,hash函数能将每个特定的值映射到唯一的桶。
无序容器提供了一组管理桶的函数,这些成员函数允许我们查询容器的状态以及在必要时强制容器进行重组。