关联容器
关联容器
关联容器支持高效的关键字查找和访问操作。2个主要的关联容器(associative-container)类型是map
和set
。
map
中的元素是一些键值对(key-value):关键字起索引作用,值表示与索引相关联的数据。set
中每个元素只包含一个关键字,支持高效的关键字查询操作:检查一个给定关键字是否在set
中。
标准库提供了8个关联容器,它们之间的不同体现在三个方面:
- 是
map
还是set
类型。 - 是否允许保存重复的关键字。
- 是否按顺序保存元素。
允许重复保存关键字的容器名字都包含单词multi
;无序保存元素的容器名字都以单词unordered
开头。
有序容器:
类型 | 特性 |
---|---|
map |
保存键值对的关联数组 |
set |
只保存关键字的容器 |
multimap |
关键字可重复出现的map |
multiset |
关键字可重复出现的set |
无序容器:
类型 | 特性 |
---|---|
unordered_map |
用哈希函数管理的map |
unordered_set |
用哈希函数管理的set |
unordered_multimap |
关键字可重复出现的unordered_map |
unordered_multiset |
关键字可重复出现的unordered_set |
map
和multimap
类型定义在头文件map
中;set
和multiset
类型定义在头文件set
中;无序容器定义在头文件unordered_map
和unordered_set
中。
使用关联容器(Using an Associative Container)
map
类型通常被称为关联数组(associative array)。
从map
中提取一个元素时,会得到一个pair
类型的对象。pair
是一个模板类型,保存两个名为first
和second
的公有数据成员。map
所使用的pair
用first
成员保存关键字,用second
成员保存对应的值。
1 | // count the number of times each word occurs in the input |
set
类型的find
成员返回一个迭代器。如果给定关键字在set
中,则迭代器指向该关键字,否则返回的是尾后迭代器。
关联容器概述(Overview of the Associative Containers)
定义关联容器(Defining an Associative Container)
定义map
时,必须指定关键字类型和值类型;定义set
时,只需指定关键字类型。
初始化map
时,提供的每个键值对用花括号{}
包围。
1 | map<string, size_t> word_count; // empty |
map
和set
中的关键字必须唯一,multimap
和multiset
没有此限制。
对于有序容器——map
、multimap
、set
和multiset
,关键字类型必须定义元素比较的方法。默认情况下,标准库使用关键字类型的<
运算符来进行比较操作。
用来组织容器元素的操作的类型也是该容器类型的一部分。如果需要使用自定义的比较操作,则必须在定义关联容器类型时提供此操作的类型。操作类型在尖括号中紧跟着元素类型给出。
1 | bool compareIsbn(const Sales_data &lhs, const Sales_data &rhs) |
这段代码定义了一个比较函数
compareIsbn
,然后创建了一个multiset
容器,用于存储Sales_data
类型的对象。让我逐步解释:
bool compareIsbn(const Sales_data &lhs, const Sales_data &rhs)
: 这是一个自定义的比较函数,用于比较两个Sales_data
对象的 ISBN。lhs
和rhs
是要比较的两个对象的引用。函数返回true
表示lhs
的 ISBN 小于rhs
的 ISBN,否则返回false
。
multiset<Sales_data, decltype(compareIsbn)*> bookstore(compareIsbn);
: 这行代码创建了一个multiset
容器,名为bookstore
,用于存储Sales_data
类型的对象。multiset
是一个有序容器,它可以包含重复的元素,并且元素会按照指定的比较函数进行排序。
<Sales_data, decltype(compareIsbn)*>
指定了容器中存储的元素类型为Sales_data
,并且指定了比较函数的类型为decltype(compareIsbn)*
,即指向compareIsbn
函数的指针。
bookstore(compareIsbn)
创建了bookstore
容器,并将compareIsbn
函数作为参数传递给容器的构造函数,用于指定元素的排序规则。由于multiset
容器要求指定一个比较函数来决定元素的顺序,因此需要将compareIsbn
函数的地址传递给容器,以便容器内部能够调用该函数来进行元素的比较和排序。因此,
bookstore
是一个按照compareIsbn
函数指定的排序规则进行排序的multiset
容器,可以存储多个具有相同 ISBN 的Sales_data
对象,并且这些对象将按照 ISBN 的顺序进行排列。
pair
类型(The
pair
Type)
pair
定义在头文件utility
中。一个pair
可以保存两个数据成员,分别命名为first
和second
。
1 | pair<string, string> anon; // holds two strings |
pair
的默认构造函数对数据成员进行值初始化。
在C++11中,如果函数需要返回pair
,可以对返回值进行列表初始化。早期C++版本中必须显式构造返回值。
1 | pair<string, int> process(vector<string> &v) |
关联容器操作(Operations on Associative Containers)
关联容器定义了类型别名来表示容器关键字和值的类型。
类型 | 含义 |
---|---|
key_type |
容器的关键字类型 |
mapped_type |
map 的值类型 |
value_type |
对于set ,与key_type 相同
对于map ,为pair<const key_type, mapped_type> |
对于set
类型,key_type
和value_type
是一样的。set
中保存的值就是关键字。对于map
类型,元素是键值对。即每个元素是一个pair
对象,包含一个关键字和一个关联的值。由于元素关键字不能改变,因此pair
的关键字部分是const
的。另外,只有map
类型(unordered_map
、unordered_multimap
、multimap
、map
)才定义了mapped_type
。
1 | set<string>::value_type v1; // v1 is a string |
关联容器迭代器(Associative Container Iterators)
解引用关联容器迭代器时,会得到一个类型为容器的value_type
的引用。对map
而言,value_type
是pair
类型,其first
成员保存const
的关键字,second
成员保存值。
1 | // get an iterator to an element in word_count |
虽然set
同时定义了iterator
和const_iterator
类型,但两种迭代器都只允许只读访问set
中的元素。类似map
,set
中的关键字也是const
的。
1 | set<int> iset = {0,1,2,3,4,5,6,7,8,9}; |
map
和set
都支持begin
和end
操作。使用迭代器遍历map
、multimap
、set
或multiset
时,迭代器按关键字升序遍历元素。
通常不对关联容器使用泛型算法。
添加元素(Adding Elements)
使用insert
成员可以向关联容器中添加元素。向map
和set
中添加已存在的元素对容器没有影响。
通常情况下,对于想要添加到map
中的数据,并没有现成的pair
对象。可以直接在insert
的参数列表中创建pair
。
1 | // four ways to add word to word_count |
insert
或emplace
的返回值依赖于容器类型和参数:
- 对于不包含重复关键字的容器,添加单一元素的
insert
和emplace
版本返回一个pair
,表示操作是否成功。pair
的first
成员是一个迭代器,指向具有给定关键字的元素;second
成员是一个bool
值。如果关键字已在容器中,则insert
直接返回,bool
值为false
。如果关键字不存在,元素会被添加至容器中,bool
值为true
。 - 对于允许包含重复关键字的容器,添加单一元素的
insert
和emplace
版本返回指向新元素的迭代器。
删除元素(Erasing Elements)
与顺序容器不同,关联容器提供了一个额外的erase
操作。它接受一个key_type
参数,删除所有匹配给定关键字的元素(如果存在),返回实际删除的元素数量。对于不包含重复关键字的容器,erase
的返回值总是1或0。若返回值为0,则表示想要删除的元素并不在容器中。
map
的下标操作(Subscripting
a map
)
操作 | 含义 |
---|---|
c[k] |
返回关键字为k 的元素;若k 不存在,则向c 中添加并进行值初始化 |
c.at(k) |
返回关键字为k 的元素;若k 不存在,则抛出out_of_range 异常 |
map
下标运算符接受一个关键字,获取与此关键字相关联的值。如果关键字不在容器中,下标运算符会向容器中添加该关键字,并值初始化关联值。
由于下标运算符可能向容器中添加元素,所以只能对非const
的map
使用下标操作。
对map
进行下标操作时,返回的是mapped_type
类型的对象;解引用map
迭代器时,返回的是value_type
类型的对象。
访问元素(Accessing Elements)
操作 | 含义 |
---|---|
c.find(k) |
返回指向第一个关键字为k 的元素的迭代器或尾后迭代器 |
c.count(k) |
返回关键字为k 的元素的数量 |
c.lower_bound(k) |
返回指向第一个关键字不小于k 的元素的迭代器 |
c.upper_bound(k) |
返回指向第一个关键字大于k 的元素的迭代器 |
c.equal_range(k) |
返回一个迭代器pair ,表示关键字为k 的元素范围 |
如果multimap
或multiset
中有多个元素具有相同关键字,则这些元素在容器中会相邻存储。
1 | multimap<string, string> authors; |
这段代码的目的是从
multimap
容器authors
中查找特定作者的所有作品,并打印它们的标题。让我逐步解释:
定义了一个
multimap
容器authors
:
1 multimap<string, string> authors;这行代码创建了一个
multimap
容器,用于存储作者名和作品名的键值对。每个键(作者名)可以对应多个值(作品名)。向容器中插入元素:
1
2 authors.insert({"Barth, John", "Sot-Weed Factor"});
authors.insert({"Barth, John", "Lost in the Funhouse"});这两行代码分别向
authors
容器中插入了两个具有相同键("Barth, John")的元素。因为multimap
允许多个元素具有相同的键,所以这两个作品都会被插入。定义要搜索的作者:
1 string search_item("Alain de Botton");这行代码定义了要搜索的作者名称。在这个示例中,搜索的作者并不存在于
authors
容器中。查找元素:
1
2 auto entries = authors.count(search_item); // number of elements
auto iter = authors.find(search_item); // first entry for this author
authors.count(search_item)
返回与搜索项匹配的元素数量。在这里,因为搜索项并不存在于容器中,所以entries
被初始化为0。authors.find(search_item)
返回与搜索项匹配的第一个元素的迭代器。在这里,由于搜索项并不存在于容器中,iter
被初始化为指向容器末尾的迭代器(即authors.end()
)。输出作者作品:
1
2
3
4
5
6 while(entries)
{
cout << iter->second << endl; // print each title
++iter; // advance to the next title
--entries; // keep track of how many we've printed
}这是一个循环,它的条件是
entries
不为0。但是,由于搜索项并不存在于容器中,所以循环体不会执行。因此,没有任何输出。综上所述,这段代码的作用是试图从
multimap
容器authors
中查找指定作者的作品,并打印它们的标题。但由于搜索项并不存在于容器中,所以不会有任何输出。
lower_bound
和upper_bound
操作都接受一个关键字,返回一个迭代器。如果关键字在容器中,lower_bound
返回的迭代器会指向第一个匹配给定关键字的元素,而upper_bound
返回的迭代器则指向最后一个匹配元素之后的位置。如果关键字不在multimap
中,则lower_bound
和upper_bound
会返回相等的迭代器,指向一个不影响排序的关键字插入位置。因此用相同的关键字调用lower_bound
和upper_bound
会得到一个迭代器范围,表示所有具有该关键字的元素范围。
1 | // definitions of authors and search_item as above |
lower_bound
和upper_bound
有可能返回尾后迭代器。如果查找的元素具有容器中最大的关键字,则upper_bound
返回尾后迭代器。如果关键字不存在,且大于容器中任何关键字,则lower_bound
也返回尾后迭代器。
equal_range
操作接受一个关键字,返回一个迭代器pair
。若关键字存在,则第一个迭代器指向第一个匹配关键字的元素,第二个迭代器指向最后一个匹配元素之后的位置。若关键字不存在,则两个迭代器都指向一个不影响排序的关键字插入位置。
1 | // definitions of authors and search_item as above |
这段代码使用了
equal_range
函数来查找multimap
容器中指定键的所有元素,并将它们的位置存储在迭代器对pos
中。然后,通过循环遍历该范围,并输出每个匹配键的对应值。让我逐步解释:
auto pos = authors.equal_range(search_item);
: 这行代码调用了equal_range
函数,该函数会返回一个表示指定键在容器中的范围的迭代器对。authors
是一个multimap
容器,search_item
是要查找的键值。pos
是一个自动类型的变量,用于存储返回的迭代器对。
pos.first != pos.second
: 这是一个循环条件,它检查迭代器对pos
所表示的范围是否为空。如果范围不为空,则说明在容器中找到了与search_item
匹配的元素。
++pos.first
: 在循环的每次迭代中,迭代器pos.first
会向前移动,以便遍历匹配键的所有元素。
cout << pos.first->second << endl;
: 这行代码输出了匹配键的对应值。由于multimap
容器可以包含多个具有相同键的元素,因此pos.first
是一个迭代器,它指向范围内的当前元素。pos.first->second
访问了该元素的值部分,即容器中存储的第二个数据成员(假设作者与书名关联),并将其输出到标准输出流中。通过这种方式,该循环会逐个输出与指定键匹配的所有元素的对应值,直到范围中的所有元素都被遍历完毕。
无序容器(The Unordered Containers)
新标准库定义了4个无序关联容器(unordered associative
container),这些容器使用哈希函数(hash
function)和关键字类型的==
运算符组织元素。
无序容器和对应的有序容器通常可以相互替换。但是由于元素未按顺序存储,使用无序容器的程序输出一般会与有序容器的版本不同。
无序容器在存储上组织为一组桶,每个桶保存零或多个元素。无序容器使用一个哈希函数将元素映射到桶。为了访问一个元素,容器首先计算元素的哈希值,它指出应该搜索哪个桶。容器将具有一个特定哈希值的所有元素都保存在相同的桶中。因此无序容器的性能依赖于哈希函数的质量和桶的数量及大小。
默认情况下,无序容器使用关键字类型的==
运算符比较元素,还使用一个hash<key_type>
类型的对象来生成每个元素的哈希值。标准库为内置类型和一些标准库类型提供了hash模板。因此可以直接定义关键字是这些类型的无序容器,而不能直接定义关键字类型为自定义类类型的无序容器,必须先提供对应的hash模板版本。