关联容器和顺序容器有着根本的不同:关联容器中的元素是按关键字来保存和访问的。与之相对,顺序容器中的元素是按它们在容器中的位置来顺序保存和访问的。
关联容器支持高效的关键字查找和访问。两个主要的关联容器类型是map和set。map中的元素是一些关键字----值对:关键字起到索引的作用,值表示与索引相关联的数据。set中每个元素只包含一个关键字;set支持高效的关键字查询操作----检查一个给定关键字是否在set中。
标准库提供8个关联容器:
类型 | 说明 |
按关键字有序保存元素 | |
map | 关联数组;保存关键字---值对 |
set | 关键字即值,即保存关键字的容器 |
multimap | 关键字可重复出现的map |
multiset | 关键字可重复出现的set |
无序集合 | |
unordered_map | 用哈希函数组织的map |
unordered_set | 用哈希函数组织的set |
unordered_multimap | 哈希组织的map;关键字可以重复出现 |
unordered_multiset | 哈希组织的set;关键字可以重复出现 |
这8个容器间的不同体现在3个维度上:每个容器(1)或者是一个set,或者是一个map;(2)或者要求不重复的关键字,或者允许重复关键字;(3)按顺序保存元素,或无序保存。允许重复关键字的容器的名字中都包含单词multi;不保持关键字按顺序存储的容器的名字都以单词unordered开头。
类型map和multimap定义在头文件map中;set和multiset定义在头文件set中;无序容器则定义在头文件unordered_map和unordered_set中。
二、关联容器概述
关联容器不支持顺序容器的位置相关的操作,例如push_front或push_back。原因是关联容器中元素是根据关键字存储的,这些操作对关联容器没有意义。而且,关联容器也不支持构造函数或插入操作这些接受一个元素值和一个数量值的操作。
除了与顺序容器相同的操作之外,关联容器还支持一些顺序容器不支持的操作和类型别名。此外,无序容器还提供一些用来调整哈希性能的操作。
关联容器的迭代器时双向的。
1、定义关联容器
在新标准下,我们可以对关联容器进行值初始化:

1 #include <iostream> 2 #include <string> 3 #include <algorithm> 4 #include <numeric> 5 #include <iterator> 6 #include <functional> 7 #include <map> 8 #include <set> 9 #include <unordered_map> 10 #include <unordered_set> 11 12 13 int main() 14 { 15 std::map<std::string, std::size_t> word_count; // 空容器 16 std::set<std::string> exclude = { "aa", "bb", "cc" }; // 列表初始化 17 std::map<std::string, std::string> authors = { 18 { "aa", "bb" }, 19 { "cc", "dd" }, 20 { "ee", "ff" } 21 }; 22 return 0; 23 }
2、关键字类型的要求
对于有序容器----map、set、multimap、multiset,关键字类型必须定义元素比较的方法。默认情况下,标准库使用关键字类型的<运算符来比较两个关键字。
1)有序容器的关键字类型
可以提供自己定义的操作来代替关键字上的<运算符。所提供的操作必须在关键字类型上定义一个严格弱序。可以将严格弱序看作“小于等于”,虽然实际定义的操作可能是一个复杂的函数。无论我们怎样定义比较函数,它必须具备如下基本性质:
a、两个关键字不能同时“小于等于”对方;如果k1“小于等于”k2,那么k2绝不能“小于等于”k1。
b、如果k1“小于等于”k2,且k2“小于等于”k3,那么k1必须“小于等于”k3。
c、如果存在两个关键字,任何一个都不“小于等于”另一个,那么我们称这两个关键字是“等价”的。如果k1“等价于”k2,且k2“等价于”k3,那么k1必须“等价于”k3。
如果两个关键字是等价的,那么容器将它们视作相等来处理。
注意:如果一个类型定义了“行为正常”的<运算符,则它可以用作关键字类型。
2)使用关键字类型的比较操作
用来组织一个容器中元素的操作的类型也是该容器类型的一部分。为了指定使用自定义的操作,必须在定义关联容器类型时提供此操作的类型。用尖括号指出要定义哪种类型的容器,自定义的操作类型(应该是一种函数指针类型)必须在尖括号中紧跟着元素类型给出。
在尖括号中出现的每个类型,就仅仅是一个类型而已。当我们创建一个容器(对象)时,才会以构造函数参数的形式提供真正的比较操作(其类型必须与尖括号中指定的类型相吻合),提供的是比较操作的指针。

1 #include <iostream> 2 #include <string> 3 #include <algorithm> 4 #include <numeric> 5 #include <iterator> 6 #include <functional> 7 #include <map> 8 #include <set> 9 #include <unordered_map> 10 #include <unordered_set> 11 12 bool comp(const std::string &s1, const std::string &s2) 13 { 14 return s1.size() < s2.size(); 15 } 16 int main() 17 { 18 std::map<std::string, int, decltype(comp)*> mp(comp); 19 mp["abc"] = 1; 20 mp["a"] = 2; 21 mp["qaq"] = 3; 22 mp["bb"] = 4; 23 for (auto iter = mp.begin(); iter != mp.end(); ++iter) 24 { 25 std::cout << iter->first << " " << iter->second << std::endl; 26 } 27 return 0; 28 }
3、pair类型
pair定义在头文件utility中。一个pair保存两个数据成员,分别命名为first和second。类似容器,pair是一个用来生成特定类型的模板。当创建一个pair时,我们必须提供两个类型名,pair的数据成员将具有的类型。两个类型不要求一样。
pair上的操作:
操作 | 说明 |
pair<T1, T2> p; | p是一个pair,两个类型分别为T1和T2的成员都进行了值初始化 |
pair<T1, T2> p(v1, v2); | p是一个成员类型为T1和T2的pair;first和second分别用v1和v2进行初始化 |
pair<T1, T2> p = {v1, v2}; | 等价于p(v1, v2) |
make_pair(v1, v2) | 返回一个用v1和v2初始化的pair。pair的类型从v1和v2的类型推断出来 |
p.first | 返回p的名为first的公有数据成员 |
p.second | 返回p的名为second的公有数据成员 |
p1 relop p2 |
关系运算符(<、>、<=、>=)按字典序定义:例如,当p1.first<p2.first或!(p2.first<p1.first)&&p1.second<p2.second成立时,p1<p2为true。 关系运算符利用元素的<运算符来实现 |
p1==p2 | 当first和second成员分别相等时,两个pair分别相等。相等性利用元素的==运算符实现 |
p1!=p2 |
1)创建pair对象的函数
在新标准下,我们可以对返回值进行列表初始化:

1 #include <iostream> 2 #include <string> 3 #include <algorithm> 4 #include <numeric> 5 #include <iterator> 6 #include <functional> 7 #include <map> 8 #include <set> 9 #include <unordered_map> 10 #include <unordered_set> 11 #include <utility> 12 13 std::pair<std::string, int> func() 14 { 15 return{ "QAQ", 233 }; 16 } 17 int main() 18 { 19 auto p = func(); 20 std::cout << p.first << ", " << p.second << std::endl; 21 return 0; 22 }
三、关联容器操作
关联容器额外的类型别名:
类型别名 | 说明 |
key_type | 此容器类型的关键字类型 |
mapped_type | 每个关键字关联的类型:只适用于map类型(各种map) |
value_type |
对于set,与key_type相同。 对于map,为pair<const key_type, mapped_type> |
对于set类型,key_type和value_type是一样的;set中保存的值就是关键字。在一个map中,元素是关键字----值对。即,每个元素是一个pair对象,包含一个关键字和一个关联的值。由于我们不能改变一个元素的关键字,因此这些pair的关键字部分是const的。我们使用作用域运算符提取一个类型的成员。

1 #include <iostream> 2 #include <string> 3 #include <algorithm> 4 #include <numeric> 5 #include <iterator> 6 #include <functional> 7 #include <map> 8 #include <set> 9 #include <unordered_map> 10 #include <unordered_set> 11 #include <utility> 12 13 int main() 14 { 15 std::set<std::string>::value_type v1; // v1是一个string 16 std::set<std::string>::key_type v2; // v2是一个string 17 std::map<std::string, int>::value_type v3; // v3是一个pair<const string, int> 18 std::map<std::string, int>::key_type v4; // v4是一个string 19 std::map<std::string, int>::mapped_type v5; // v5是一个int 20 return 0; 21 }
1、关联容器迭代器
当解引用一个关联容器迭代器时,我们会得到一个类型为容器的value_type的值的引用。对于map而言,value_type是一个pair类型,其first成员保存const的关键字,second成员保存值。我们可以改变pair的值,但是不能改变关键字成员的值。

1 #include <iostream> 2 #include <string> 3 #include <algorithm> 4 #include <numeric> 5 #include <iterator> 6 #include <functional> 7 #include <map> 8 #include <set> 9 #include <unordered_map> 10 #include <unordered_set> 11 #include <utility> 12 13 int main() 14 { 15 std::map<std::string, int> mp = { { "QAQ", 233 } }; 16 auto iter = mp.begin(); 17 ++iter->second; // 改变元素的值 18 // iter->first = "hello"; // 错误:关键字是const的 19 std::cout << iter->first << ", " << iter->second << std::endl; 20 return 0; 21 }
1)set的迭代器是const
虽然set类型同时定义了iterator和const_iterator类型,但两种类型都只允许访问set中的元素。与不能改变一个map元素的关键字一样,一个set中的关键字也是const的。可以用一个set迭代器来读取元素的值,但是不能修改。

1 #include <iostream> 2 #include <string> 3 #include <algorithm> 4 #include <numeric> 5 #include <iterator> 6 #include <functional> 7 #include <map> 8 #include <set> 9 #include <unordered_map> 10 #include <unordered_set> 11 #include <utility> 12 13 int main() 14 { 15 std::set<int> s = { 5, 3, 4 }; 16 auto iter = s.begin(); 17 //*iter = 10; // 错误:set中的关键字是只读的 18 for (auto iter = s.begin(); iter != s.end(); ++iter) 19 { 20 std::cout << *iter << std::endl; 21 } 22 return 0; 23 }
2)遍历关联容器
当使用以迭代器遍历一个map、set、multimap、multiset时,迭代器按关键字升序遍历元素。
3)关联容器和算法
我们通常不对关联容器使用泛型算法。关键字const这一特性意味着不能讲关联容器传递给修改或重排元素的算法,因为这类算法向元素写入值,而set类型中的元素是const的,map中的元素是pair,其第一个成员是const的。
关联容器可用于只读取元素的算法。但是,很多算法都要搜索序列。由于关联容器中的元素不能通过它们的关键字进行查找,因此对其使用泛型搜索算法几乎是个坏主意。
在实际编程中,如果我们真要对一个关联容器使用算法,要么是将它当作一个源序列,要么当作一个目的位置。
2、添加元素
关联容器的insert成员向容器中添加一个元素或一个元素范围。由于map和set(以及对应的无序类型)包含不重复的关键字,因此插入一个已存在的元素对容器没有任何影响。
关联容器的insert操作:
操作 | 说明 |
c.insert(v) |
v是value_type类型对象;args用来构造一个元素。 对于map和set,只有当元素的关键字不在c中时才插入(或构造)元素。 函数返回一个pair,包含一个迭代器,指向具有指定关键字的元素,以及一个指示插入是否成功的bool值。 对于multimap和multiset,总会插入(或构造)给定元素,并返回一个指向新元素的迭代器 |
c.emplace(args) | |
c.insert(b, e) |
b和e是迭代器,表示一个c::value_type类型值的范围;items是这种值的花括号列表。函数返回void。 对于map和set,只会插入关键字不在c中的元素。对于multimap和multiset,则会插入范围中的每个元素 |
c.insert(items) | |
c.insert(p, v) | 类似insert(v)(或emplace(args)),但将迭代器p作为一个指示,指出从哪里开始搜索新元素应该存储的位置。返回一个迭代器,指向具有给定关键字的元素 |
c.emplace(p, args) |
1)向map添加元素
对一个map进行insert操作时,必须记住元素类型是pair。

1 #include <iostream> 2 #include <string> 3 #include <algorithm> 4 #include <numeric> 5 #include <iterator> 6 #include <functional> 7 #include <map> 8 #include <set> 9 #include <unordered_map> 10 #include <unordered_set> 11 #include <utility> 12 13 int main() 14 { 15 std::map<std::string, int> words; 16 words.insert({ "a", 1 }); 17 words.insert(std::make_pair("b", 2)); 18 words.insert(std::pair<std::string, int>("c", 3)); 19 words.insert(std::map<std::string, int>::value_type("d", 4)); 20 return 0; 21 }
2)检测insert的返回值
insert(或emplace)返回的值依赖于容器类型和参数。对于不包含重复关键字的容器,添加单一的insert和emplace版本返回一个pair,告诉我们插入操作是否成功。pair的first成员是一个迭代器,指向具有给定关键字的元素;second成员是一个bool值,指出元素是插入成功还是已经存在于容器中。如果关键字已在容器中,则insert什么事情也不做,且返回值中的bool部分为false。如果关键字不存在,元素被插入容器中,且bool值为true。

1 #include <iostream> 2 #include <string> 3 #include <algorithm> 4 #include <numeric> 5 #include <iterator> 6 #include <functional> 7 #include <map> 8 #include <set> 9 #include <unordered_map> 10 #include <unordered_set> 11 #include <utility> 12 13 int main() 14 { 15 std::vector<std::string> words = { "a", "a", "b", "c", "c", "c" }; 16 std::map<std::string, int> word_count; 17 for (auto word: words) 18 { 19 auto ret = word_count.insert({ word, 1 }); 20 if (!ret.second) // word已在map中 21 ++ret.first->second; 22 } 23 for (auto iter = word_count.begin(); iter != word_count.end(); ++iter) 24 { 25 std::cout << iter->first << " " << iter->second << std::endl; 26 } 27 return 0; 28 }
3)向multimap或multiset添加元素
对允许重复关键字的容器,接受单个元素的insert操作返回一个指向新元素的迭代器。这里无须返回一个bool值,因为insert总是向这类容器中加入一个新元素。
3、删除元素
关联容器定义了3个版本的erase:
操作 | 说明 |
c.erase(k) | 从c中删除每个关键字为k的元素。返回一个size_type值,指出删除的元素的数量 |
c.erase(p) | 从c中删除迭代器p指定的元素。p必须指向c中一个真实元素,不能等于c.end()。返回一个指向p之后元素的迭代器,若p指向c中的尾元素,则返回c.end() |
c.erase(b, e) | 删除迭代器b和e所表示的范围中的元素。返回e |
4、map的下标操作
map和unordered_map容器提供了下标运算符和一个对应的at函数。set类型不支持下标,因为set中没有与关键字相关联的“值”。元素本身就是关键字,因此“获取一个与关键字相关联的值”的操作就没有意义了。我们不能对一个multimap或一个unordered_multimap进行下标操作,因为这些容器中可能有多个值与一个关键字相关联。
map和unordered_map的下标操作:
操作 | 说明 |
c[k] | 返回关键字为k的元素;如果k不在c中,添加一个关键字为k的元素,对其进行值初始化 |
c.at(k) | 访问关键字为k的元素,带参数检查;若k不在c中,抛出一个out_of_range异常 |
由于下标运算符可能插入一个新元素,我们只可以对非const的map使用下标操作。
当对一个map进行下标操作时,会获得一个mapped_type对象。map的下标运算符返回一个左值,因此我们既可以读也可以写元素。
有时只是想知道一个元素是否已在map中,但在不存在时并不想添加元素。在这种情况下,就不能使用下标运算符。
5、访问元素
在一个关联容器中查找元素的操作:
lower_bound和upper_bound不适用于无序容器。
下标和at操作只适用于非const的的map和unordered_map。
操作 | 说明 |
c.find(k) | 返回一个迭代器,指向一个关键字为k的元素,若k不在容器中,则返回尾后迭代器 |
c.count(k) | 返回关键字等于k的元素的数量。对于不允许重复关键字的容器,返回值永远是0或1 |
c.lower_bound(k) | 返回一个迭代器,指向第一个关键字不小于k的元素 |
c.upper_bound(k) | 返回一个迭代器,指向第一个关键字大于k的元素 |
c.equal_range(k) | 返回一个迭代器pair,表示关键字等于k的元素的范围。若k不存在,pair的两个成员均等于c.end() |
有序容器的迭代器通过关键字有序访问容器中的元素。无论在有序容器中还是在无序容器中,具有相同关键字的元素都是相邻存储的。
四、无序容器
新标准定义了4个无序关联容器。这些容器不是使用比较运算符来组织元素,而是使用一个哈希函数和关键字类型的==运算符。在关键字类型的元素没有明显的有序关系的情况下,无序容器时非常有用的。在某些应用中,维护元素的序代价非常高,此时无序容器也很有用。
1、使用无序容器
除了哈希管理操作之外,无序容器还提供了与有序容器相同的操作(find、insert等)。这意味着我们曾用于map和set的操作也能用于unordered_map和unordered_set。类似的,无序容器也允许重复关键字的版本。
因此,通常可以用一个无序容器替换对应的有序容器,反之亦然。但是,由于元素未按顺序存储,一个使用无序容器的程序的输出通常会与使用有序容器的版本不同。
2、管理桶
无序容器在存储组织为一组桶,每个桶保存零个或多个元素。无序容器使用一个哈希函数将元素映射到桶。为了访问一个元素,容器首先计算元素的哈希值,它指出应该搜索哪个桶。容器将具有一个特定哈希值的所有元素都保存在相同的桶中。如果容器允许重复关键字,所有具有相同关键字的元素也都会在同一个桶中。因此,无序容器的性能依赖于哈希函数的质量和桶的数量和大小。
对于相同的参数,哈希函数必须总是产生相同的结果。理想情况下,哈希函数还能降每个特定的值映射到唯一的桶。但是,将不同关键字的元素映射到相同的桶也是允许的。当一个个桶保存多个元素时,需要顺序搜索这些元素来查找我们想要的那个。计算一个元素的哈希值和在桶中搜索通常都是很快的操作。但是,如果一个桶中保存了很多元素,那么查找一个特定元素就需要大量比较操作。
无序容器管理操作:
操作 | 说明 |
桶接口 | |
c.bucket_count() | 正在使用的桶数目 |
c.max_bucket_count() | 容器能容纳的最多的桶的数量 |
c.bucket_size(n) | 第n个桶中有多少个元素 |
c.bucket(k) | 关键字为k的元素在哪个桶中 |
桶迭代 | |
local_iterator | 可以用来访问桶中元素的迭代器类型 |
const_local_iterator | 桶迭代器的const版本 |
c.begin(n), c.end(n) | 桶n的首元素迭代器和尾后迭代器 |
c.cbegin(n), c.cend(n) | 与前两个函数类似,但返回const_local_iterator |
哈希策略 | |
c.load_factor() | 每个桶的平均元素数量,返回float值 |
c.max_load_factor() | c试图维护的平均桶大小,返回float值。c会在需要时添加新的桶,以使得load_factor<=max_load_factor |
c.rehash(n) | 重组存储,使得bucket_count>=n且bucket_count>size/max_load_factor |
c.reserve(n) | 重组存储,使得c可以保存n个元素且不必rehash |
3、无序容器对关键字类型的要求
默认情况下,无序容器使用关键字类型的==运算符来比较元素,它还使用一个hash<key_type>类型的对象来生成每个元素的哈希值。标准库为内置类型(包括指针)提供了hash模板。还有一些标准库类型,包括string和智能指针类型定义了hash。因此我们可以直接定义关键字是内置类型、string还是智能指针类型的无序容器。
但是,我们不能直接定义关键字类型为自定义类型的无序容器。与容器不同,不能直接使用哈希模板,而必须提供我们自己的hash模板版本。
我们不使用默认的hash,而是使用另外一种方法,类似为有序容器重载关键字类型的比较操作。为了能降自定义类型用作关键字,我们需要提供函数来替代==运算符和哈希值计算函数。

1 #include <iostream> 2 #include <string> 3 #include <algorithm> 4 #include <numeric> 5 #include <iterator> 6 #include <functional> 7 #include <map> 8 #include <set> 9 #include <unordered_map> 10 #include <unordered_set> 11 #include <utility> 12 13 class Sale_data 14 { 15 public: 16 Sale_data() = default; 17 Sale_data(std::string _id) :id(_id){} 18 std::string id; 19 }; 20 21 std::size_t hasher(const Sale_data &sd) 22 { 23 return std::hash<std::string>()(sd.id); 24 } 25 26 bool eqop(const Sale_data &sd1, const Sale_data &sd2) 27 { 28 return sd1.id == sd2.id; 29 } 30 31 int main() 32 { 33 // 参数分别指桶大小、哈希函数指针、相等性判断函数指针 34 std::unordered_map<Sale_data, int, decltype(hasher)*, decltype(eqop)*> book(42, hasher, eqop); 35 // 如果类定义了==运算符,则可以只重载哈希函数 36 book.insert({ Sale_data("a"), 1 }); 37 book.insert({ Sale_data("b"), 233 }); 38 for (auto iter = book.begin(); iter != book.end(); ++iter) 39 { 40 std::cout << iter->first.id << " " << iter->second << std::endl; 41 } 42 return 0; 43 }