关联容器
关联容器
关联容器支持高效的关键字查找和访问操作。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模板版本。





