文章目录
- 概述及类型
- map
- set
- pair类型
- 概念
- 初始化
- 默认初始化
- 提供初始化器
- 允许的操作
- 可以创建一个pair类的函数
- 可以作为容器的类型
- 关联容器迭代器
- 概念
- map的迭代器
- set的迭代器是const的
- 初始化
- map and set
- multimap and multiset
- 关联容器的操作
- 额外的类型别名
- 关联容器和算法
- 删除元素
- 添加元素
- insert操作
- 通过insert操作向map添加元素
- insert的返回值
- 向两种multi容器添加元素
- map的下标操作
- 下标操作的返回值
- 通过下标([])对map的key与value进行操作:
- 访问元素
- 概念
- 利用find和count进行查找
- lower_bound和upper_bound面向迭代器
- equal_range函数
- 下标和insert的区别、下标与find的区别
- 有序容器
- 关键字类型
- 重载关键字类型代替<运算符
- 无序容器
- 关键字类型
- 管理桶
- 关键字相同的元素相邻存储
概述及类型
关联容器和顺序容器有着根本的不同:
- 关联容器中的元素是按关键字来保存和访问的。
- 顺序容器中的元素是按它们在容器中的位置来顺序保存和访问的。
- 顺序容器的底层数据结构是数组、链表。
- 关联容器的底层数据结构是红黑树(set、multimap)、哈希表等。
关联容器支持高效的关键字查找和访问。两个主要的关联容器(associative-container)类型是map和set:
- map中的元素是一些关键字-值(key-value)对。
- set中每个元素只包含一个关键字;
标准库提供8个关联容器,这8个容器间的不同体现在三个维度上:每个容器
- 或者是一个set,或者是一个map;
- 或者要求不重复的关键字,或者允许重复关键字;
- 按顺序保存元素,或无序保存。
特性由名字表示:
- 允许重复关键字的容器的名字中都包含单词multi;
- 不保持关键字按顺序存储的容器的名字都以单词unordered开头。
因此一个unordered_multi_set是一个允许重复关键字,元素无序保存的集合,而一个set则是一个要求不重复关键字,有序存储的集合。无序容器使用哈希函数来组织元素。
所在的头文件:
- 类型map和multimap定义在头文件map中;
- set和multiset定义在头文件set中;
- 无序容器则定义在头文件unordered_map和unordered_set中。
下表中的操作关联容器和顺序容器都支持:
map
为了高效实现 “按值访问元素” 这个操作而设计的。
当从map中提取一个元素时,会得到一个pair类型的对象。
简单来说,pair是一个模板类型,保存两个名为 first 和 second 的(公有)数据成员。
set
保存特定的值(如:满足/不满足,忽略/不忽略)集合。
pair类型
概念
- pair是标准库类型,定义在头文件utility中。
- pair的数据成员是public的。两个成员分别命名为 first 和 second。可以用成员访问符号(.)来访问它们。
- map的元素是pair,pair用first成员保存关键字,用second成员保存对应的值。
初始化
默认初始化
一个pair保存两个数据成员。创建一个pair时,我们必须提供两个类型名,但是不要求一样:
pair的默认构造函数对数据成员进行值初始化。因此:
- anon是两个空的string
- line保存一个空的string一个空的vector
- word_count保存一个空串和一个为0的size_t
提供初始化器
pair <string, string> anon = {"zhangsan", "nan"};
允许的操作
可以创建一个pair类的函数
可以利用c++11标准下,允许对返回值进行列表初始化的特性,完成这一目的:
pair<string, int>
fun(vector<string> &vs){if(!vs.empty())return {vs.back(), vs.back().size()}; // 列表初始化//返回尾元素和尾元素的大小组成的pairelsereturn pair<string,int>(); // 隐式构造返回值,返回一个空串和0构成的pair
}
也可以用make_pair来生成pair对象,pair的两个类型来自于make_pair的参数:
return make_pair(vs.back(), v.back().size());
可以作为容器的类型
push_back的参数也可以写成列表初始化的形式或者使用make_pair:
arr.push_back({s, i});
arr.push_back(make_pair(s, i));
关联容器迭代器
概念
当解引用一个关联容器迭代器时,会得到一个类型为容器的value_type的值的引用。
迭代器的类型:
- map:pair<关键字类型,值类型>::iterator
- set:set<元素值类型>::iterator
对于自定义的比较类型的set或者multiset:
set<A, decltype(compareA)*> Aset(compareA);
其迭代器类型应为:pair<关键字类型,自定义比较类型>::iterator
map的迭代器
一个map的value_type是一个pair。可以改变pair的值(即改变映射关系),但不能改变关键字成员的值,因为它是const的。
set的迭代器是const的
我们在概念中说过:当解引用一个关联容器迭代器时,会得到一个类型为容器的value_type的值的引用。
但set的value_type即使它的key_type。因此,虽然set类型同时定义了iterator和const_iterator类型,但两种类型都只允许只读访问set中的元素。
与不能改变一个map元素的关键字一样,一个set中的关键字也是const的。可以用一个set迭代器来读取元素的值,但不能修改:
初始化
map and set
- 每个关联容器都定义了一个默认构造函数,它创建一个指定类型的空容器。
- 可以将关联容器初始化为另一个同类型容器的拷贝。
- 或是从一个值范围来初始化关联容器,只要这些值可以转化为容器所需类型就可以。
在新标准下,我们也可以对关联容器进行值初始化:
正如上图所示,我们初始化map时,必须将每个关键字 - 值对包围在花括号中:
(key,value)
multimap and multiset
一个map或set中的关键字必须是唯一的,即,对于一个给定的关键字,只能有一个元素的关键字等于它。容器multimap和multiset没有此限制,它们都允许多个元素具有相同的关键字。
实例:
关联容器的操作
额外的类型别名
对于set类型,key_type和value_type是一样的——set中保存的值就是关键字。
在一个map中,元素是关键字-值对。即,每个元素是一个pair对象,包含一个关键字和一个关联的值。由于我们不能改变一个元素的关键字,因此这些pair的关键字部分是const的:
关联容器和算法
由上面的知识可知,set类型中的元素是const的,map中的元素是pair,其第一个成员是const的。不能通过迭代器更改set中的元素的值、以及map的关键字的值。也就意味着关联容器只可用于只读算法。
但是由于只读算法都要搜索序列。关联容器又不能通过关键字遍历查找, 因此关联容器定义了很多特有的搜索成员,通过给定的关键字直接获取元素。
因此如果我们真要对一个关联容器使用算法,要么将它当成一个源序列,要么将它当作一个目的位置。
删除元素
删除关键字的版本:对于保存不重复关键字的容器,erase的返回值总是0 or 1,0表示想删除的元素并不在容器中。
添加元素
insert操作
insert有两个版本,分别接受一对迭代器,或者是一个初始化器列表,这两个版本对于一个给定的关键字,只有第一个带此关键字的元素才被插入到容器中:
通过insert操作向map添加元素
对一个map进行insert操作时,必须记住元素类型是pair。
通常,对于想要插入的数据,并没有一个现成的pair对象。可以在insert的参数列表中创建一个pair:
insert的返回值
insert(或emplace)返回的值依赖于容器类型和参数。对于不包含重复关键字的容器,添加单一元素的insert和emplace版本返回一个pair,告诉我们插入操作是否成功。pair的first成员是一个迭代器,指向具有给定关键字的元素;second成员是一个bool值,指出元素是插入成功还是已经存在于容器中。如果关键字已在容器中,则insert什么事情也不做,且返回值中的bool部分为false。如果关键字不存在,元素被插入容器中,且bool值为true。
向两种multi容器添加元素
与map和set不同,一个multi容器中的关键字不必唯一,在这些类型上调用insert总会插入一个元素。返回一个指向新元素的迭代器。
map的下标操作
- map和unordered_map容器提供了下标运算符和一个对应的at函数。
- set类型不支持下标,因为set中没有与关键字相关联的“值”。 元素本身就是关键字,因此“获取与一个关键字相关联的值”的操作就没有意义了。
- 不能对一个multimap或一个unordered_multimap进行下标操作,因为这些容器中可能有多个值与一个关键字相关联。
类似我们用过的其他下标运算符,map下标运算符接受一个索引(即,一个关键字),获取与此关键字相关联的值。
但是,与其他下标运算符不同的是(对于其他下标运算符而言,访问一个不存在地址/索引显然是一个未定义的行为),如果关键字并不在map中,会为它创建一个元素并插入到map中,并对关联值进行值初始化。
用一个实例来看:
将会执行如下操作:
- 在word_count中搜索关键字为Anna的元素,未找到。
- 将一个新的关键字-值对插入到word_count中。关键字是一个const string,保存Anna。值进行值初始化,在本例中值为0。
- 提取出新插入的元素(键值对),并将值1赋予它(它指的是键值对中的值)。
由于下标运算符可能插入一个新元素,因此只可以对非const的map使用下标操作。
下标操作的返回值
通常情况下,解引用一个迭代器所返回的类型与下标运算符返回的类型是一样的(如vector和string)。但对map则不然:
- 当对一个map进行下标操作时,会获得一个mapped_type对象;
- 当解引用一个map迭代器时,会得到一个value_type对象。
通过下标([])对map的key与value进行操作:
void add_val(map<string, vector<string>>& map, const string& key,const string& s){map[key].push_back(s);// 当map[key]对应的vector<string>存在时直接将s push_back进去// 不存在时首先创建一个空的vector<string>,再将s push_back进去
}void add_key(map<string, vector<string>>& map, const string& key) {map[key];// 当map[key]对应的vector<string>存在时只是获取其vector,但不做任何操作// 不存在时,在容器中为key创建一个对象,进行默认初始化,构造一个空的vector//等价于:/*if(map.find(key) == map.end()){map.[key] = vector<string>();}find函数找到参数返回参数对应的迭代器,未找到返回尾迭代器。*/
}
int main(int argc, char const *argv[]) {map<string, vector<string>> map;add_key(map, "li");add_key(map, "chen");add_key(map, "jiang");add_val(map, "li", "si");add_val(map, "chen", "chouchou");add_val(map, "li", "wu");add_val(map, "chen", "bianbian");for(auto& i : map){cout << i.first << ": ";for(auto& j : i.second){cout << j << " ";}cout << endl;}return 0;
}
其实map<string, vector<string>>
和multimap<string, string>
起到的作用是类似的。
如果用multimap存储的话只是不需要add_key函数了、add_value函数可以直接用insert操作。
访问元素
概念
利用find和count进行查找
以multi容器为例,容器中如果有多个元素具有一样的关键字,则这些元素在容器中会相邻存储。
那么想要遍历一个关键字的所有元素应该这么做:
auto countover = multi.count(key); // 关键词出现的次数auto findover = multi.find(key); // 具有给定关键字的第一个元素while (countover--) { // 循环次数为关键字出现的次数cout << findover->second << endl; // 关键字对应的值findover++; // 后移一位元素,相同的关键字相邻排列}
lower_bound和upper_bound面向迭代器
lower_bound和upper_bound返回一个范围:
- 如果关键字在容器中,lower_bound返回的迭代器指向第一个具有关键字的元素;upper_bound返回的迭代器指向最后一个具有关键字的元素的后面位置。 也就是类似于begin和end迭代器组成的左闭右开区间,表示所有具有该关键字的元素的范围。
- 如果关键字不在容器内, 由于lower_bound和upper_bound面向有序的关联容器,因此两者返回的迭代器相同,都指向第一个关键字大于k(给定关键字)的元素。
因此可以重写上面的程序:
for(auto beg = multi.lower_bound(key); beg != multi.upper_bound(key); beg++){cout << beg -> second << endl;
}
equal_range函数
用equal_range函数重写上面的程序:
for(auto pos = multi.equal_range(key); pos.first != pos.second; pos.first++){cout << (pos.first)->second << endl;
}
对照使用lower_bound和upper_bound的代码,可以清晰看出来pos.first等价于beg,pos.second等价于end。返回的都是指向一个multi元素的迭代器,对其解引用会得到一个multi的value_type对象——pair,因此需要通过->来访问second——也就是“键-值对”中的值。
下标和insert的区别、下标与find的区别
[]与insert不同,
- insert: 当map的key存在时不做任何操作;不存在时插入键值对。
- []: 当key存在时,用push_back将value添加进去;如果不存在,创建key然后用push_back将value添加进去。
两者在key不存在时操作是类似的。存在时,insert维护原有键值对(不对容器进行更改),下标使用最新的键值对覆盖原有的键值对(对容器进行更改)。
[]与find不同:
- []: key不存在时,构造一个元素,将其插入到容器;存在时,获取其元素
- find: key不存在时,返回尾后迭代器;存在时,返回的迭代器指向第一次出现的关键字的位置
两者在key存在时操作是类似的。不存在时,find不做更改容器的操作,下标会对容器的内容进行更改。
有序容器
关键字类型
有序容器使用比较函数来比较关键字,从而将元素按顺序存储。
对于有序容器——map、multimap、set以及multiset,关键字类型必须定义元素比较的方法。 默认情况下,标准库使用关键字类型的<运算符来比较两个关键字。在集合类型set中,关键字类型就是元素类型;在映射类型map中,关键字类型是元素的第一部分的类型。
c++11允许提供自己定义的操作来代替关键字上的<运算符。所提供的操作必须在关键字类型上定义一个严格弱序(strict weak ordering)。可以将严格弱序看作“小于等于”:
- 两个关键字不能同时“小于等于”对方;如果k1“小于等于”k2,那么k2绝不能“小于等于”k1。
- 如果k1“小于等于”k2,且k2“小于等于”k3,那么k1必须“小于等于”k3。
- 如果存在两个关键字,任何一个都不“小于等于”另一个,那么我们称这两个关键字是“等价”的。如果k1“等价于”k2,且k2“等价于”k3,那么k1必须“等价于”k3。
如果两个关键字是等价的(即,任何一个都不“小于等于”另一个),那么容器将它们视作相等来处理。当用作map的关键字时,只能有一个元素与这两个关键字关联,我们可以用两者中任意一个来访问对应的值。
重载关键字类型代替<运算符
在定义关联容器时,必须用尖括号指出要定义哪种类型的容器,自定义的比较类型必须在尖括号中紧跟着元素类型给出。
上面说到,可以用一个具有严格弱序性质的函数来起到<运算符的作用,但是函数名并不是一个类型,它仅仅只是一个名字。因此需要提供一个指向函数的指针——此时操作类型为函数指针类型。
struct A{int age;
};bool compareA(const A& a1, const A& a2){return a1.age < a2.age;
}int main(int argc, char const *argv[]) {set<A, decltype(compareA)*> Aset(compareA);return 0;
}
上例中,我们使用decltype来指出自定义操作的类型。
当用decltype来获得一个函数指针类型时,必须加上一个*来指出我们要使用一个给定函数类型的指针。 用compareA来初始化Aset对象,这表示当我们向Aset添加元素时,通过调用compareA来为这些元素排序。即,Aset中的元素将按它们的age成员的值排序。可以用compareA代替&compareA作为构造函数的参数,因为当我们使用一个函数的名字时,在需要的情况下它会自动转化为一个指针。 当然,使用&compareIsbn的效果也是一样的。
用typedef定义与compareA相容的函数指针类型,然后用此类型声明set,起到的效果和上面是一样的:
typedef bool (*pf)(const A& , const A&);
set<A, pf> Aset2(compareA);
无序容器
关键字类型
与有序容器使用比较运算符来组织元素不同。无序容器使用关键字类型的==运算符来比较元素,还使用一个hash<key_type>
类型的对象来生成每个元素的哈希值。
不能直接定义关键字类型为自定义类类型的无序容器。因为不能向容器那样直接使用哈希模板(如 hash<string>()
),必须定义我们自己的hash模板版本。
类似于有序容器重载关键字类型的默认比较操作。无序容器需要提供函数来替代==运算符和哈希值计算函数,格式如下(以unordered_multiset为例):
unordered_multiset<类型名, 重载的哈希函数, 重载的==运算符>
管理桶
无序容器在存储上组织为一组桶,每个桶保存零个或多个元素。无序容器使用一个哈希函数将元素映射到桶。为了访问一个元素,容器首先计算元素的哈希值,它指出应该搜索哪个桶。容器将具有一个特定哈希值的所有元素都保存在相同的桶中。如果容器允许重复关键字,所有具有相同关键字的元素也都会在同一个桶中。因此,无序容器的性能依赖于哈希函数的质量和桶的数量和大小。
不同关键字的元素映射到相同的桶也是允许的。当一个桶保存多个元素时,需要顺序搜索这些元素来查找我们想要的那个。计算一个元素的哈希值和在桶中搜索通常都是很快的操作。但是,如果一个桶中保存了很多元素,那么查找一个特定元素就需要大量比较操作。
关键字相同的元素相邻存储
不论在有序容器还是无序容器中,具有相同关键字的元素都是相邻存储的。