欢乐C++ —— 9. 关联容器概览
简述
在之前的顺序容器概览中,已经简单的介绍了STL 和容器的概念,这节主要看看基础的关联容器操作。
关联容器与顺序容器的主要区别:1. 关联容器中的元素是按关键字保存并访问元素,而顺序容器的元素是按照它们在容器中的位置来顺序保存和访问的。2. 关联容器保存的是 键值对 或 键,而顺序容器存放的是 值。3. 顺序容器不会自动排序,而有序的关联容器内部保持有序。
以上类型分别包含在4个头文件 <map> <set> <unordered_map> <unordered_set>
关联容器支持高效的关键字查找和访问。两个基础的关联容器类型是map 和 set ,map 保存键值对,而set 只保存键。
对于有序容器,关键字类型必须支持比较,默认通过<比较,也可以在创建容器时自定义比较操作。
因为关联容器内部对元素存放策略,所以map 不可以修改pair和pair的key 成员(但可以修改second成员(值)),set 不可以修改关键字。
pair
map 一般都通过pair 存储键值对。 pair 模板类定义在
pair支持<, >, <=, >=, == 等操作:< 的含义是当 first_1< first_2 || second_1<second_2 时为真,其它操作大都调用<运算符。
容器内置类型
- key_type 表示此容器的关键字类型(键类型) 关键字类型都不可修改
- value_type 表示容器存放的元素类型 。map 是pair 类型, 而set 的 value_type 和 key_type 相同
- mapped_type 表示此容器的关联类型(值类型) 仅 map 有。
底层实现
- map set multimap multiset 红黑树
- unordered_map unordered_set unordered_mulitimap unordered_mulitiset 哈希链表
容器操作
添加元素
大部分操作和顺序容器一样,不过没有按位置插入的操作(push_back ….) 但可以使用insert , emplace 。
-
向 map 添加元素,要注意 map 的元素类型是 pair 类类型。如果 map 内已经有这个关键字,则不做操作。
- 向multimap 添加元素,返回容器中指向这个元素的迭代器。
函数的返回值也是一个pair类型,不过这个pair.first 是 map 的迭代器类型;pair.second 是标记添加元素是否成功的 bool 值。
-
向set 添加元素,没有返回值,并且如果set 中有这个关键字,则不做操作。
- 向multiset 添加元素,没有返回值。
#include <iostream>
#include <vector>
#include <string>
#include <map>
#include <set>
#include <unordered_set>
#include <unordered_map>
#include <utility>
#include <algorithm>
using namespace std;
int main( ) {
map<int, string> m_map;
{
m_map.insert(make_pair(1, "str:1"));
m_map.insert({ 4,"str:4" });
m_map.insert(pair<int, string>(6, "str:6"));
m_map.insert(map<int, string>::value_type(8, "str:8"));
}
pair<map<int, string>::iterator, bool> it;
it = m_map.emplace(10, "str:10");
cout << it.first->first << " " << it.first->second << " " << it.second << endl;
it = m_map.emplace(10, "new str:10");
cout << it.first->first << " " << it.first->second << " " << it.second << endl;
it = m_map.try_emplace(10, "str:999");
cout << it.first->first << " " << it.first->second << " " << it.second << endl;
for_each(m_map.begin( ), m_map.end( ), [ ] (auto&i) {cout << i.first << ' ' << i.second << endl; });
cout << "————map over————" << endl;
multimap<int, string> multi_map;
auto it2 = multi_map.insert({ 1,"123" });
cout << it2->first << " " << it2->second << endl;
it2 = multi_map.insert({ 1,"new str123" });
cout << it2->first << " " << it2->second << endl;
cout << "————multimap over————" << endl;
set<int> m_set;
m_set.insert(1);
m_set.insert({ 1,2,3,4,5 });
for_each(m_set.begin( ), m_set.end( ), [ ] (auto &i) {cout << i << ' '; });
cout << endl << "————set over————" << endl;
multiset<int> multi_set;
multi_set.insert({ 1,1,1 });
for_each(multi_set.begin( ), multi_set.end( ), [ ] (auto &i) {cout << i << ' '; });
cout << endl << "————multiset over————" << endl;
return 0;
}
删除元素
关联容器的erase 可以分为两种:
- 按关键字删除,这种会返回非负整数,表示成功删除的元素个数。
- 按迭代器删除,和顺序容器一样,可以指定一个元素,也可以删除一个范围,都返回删除的元素的后一个位置的迭代器。
访问元素
其中,lower_bound() 和 upper_bound() 不适用于无序容器。而equal_range 之所以适合无序容器跟无序容器底层有关。
map 特有的下标操作
两个操作都只适用于非const 的 map 和 unordered_map 。两个操作不同之处在于当关键字key 不在容器中的操作。
- **map[key] **:返回容器中key 映射的值的引用。若容器中没有则添加一个{键为key,值为值初始化}的键值对,再返回其值的引用。
- **map.at(key) **:返回容器中key 映射的值的引用。若容器中没有会抛出 out_of_range 。
无序容器特有操作
在某些应用场景,维护元素有序的代价较大,这时可以考虑无序容器。
相比较有序关联容器利用比较运算符组织元素,无序关联容器使用哈希函数和 == 。其底层存储 表现为一组桶,每个桶可以有多个元素。其性能依赖于哈希函数和桶的大小、数量。
无序容器提供的一组管理桶的函数,我们能在必要的时候手动调整容器底层。
无序容器对关键字的类型是有要求的,1. 其必须支持 == 操作。2. 其必须有hash 函数。
一般而言,内置的类型都支持 == 操作,并且标准库为常见的类型设计了hash 函数。但如果想存储自定义类型时,就需要注意我们须要定制这两个操作。
以下面为例,第一个类型参数为是键类型(MyClass )第二个参数是值类型(int) 第三个类型参数是函数对象类型,表示接受参数为MyClass 返回值为int 的可调用对象,第四个参数是 == 比较运算符,由于我的MyClass中写了==操作,所以这块就可以省略。
unordered_multimap<MyClass,int,function<int(MyClass)>> qwe;
单词计数例子
#include <iostream>
#include <map>
#include <set>
#include <string>
using namespace std;
int main( ) {
map<string, size_t> word_count;
string word;
while ( cin >> word ) {
++word_count[word];
}
for ( auto &pair : word_count ) {
cout << pair.first << ": " << pair.second << endl;
}
return 0;
}
定义一个map,相当于定义了一个关键字-值 映射关系,本例中是 string 与 size_t 的映射关系,它们两个实际上被保存在标准库 pair 结构里面,pair.first 键 保存的是string pair.second 值 保存的是 size_t 。之后的while 循环向map里面添加元素。因为map 重写了 [ ] 运算符,当map 有这个键,则直接返回值的引用。当map中没有这个关键字,会新创建一个键值对并且返回值的引用。
对这个进行小的扩展,例如我们只想统计某些单词的个数,并且忽略大小写和标点。例如 “exp,” “exp.” “Exp” 都应该算作同一个单词。我们可以顶一个set 专门记录要统计的单词,每次输入检测是否在set 中
#include <iostream>
#include <map>
#include <set>
#include <string>
#include <algorithm>
using namespace std;
int main( ) {
map<string, size_t> word_count;
set<string> include = { "yes","no"};
string word;
while ( cin >> word ) {
string trans;
transform(word.begin( ), word.end( ), back_inserter(trans), tolower);
if ( include.find(trans) == include.end( ) ) {
trans.erase(trans.end( ) - 1);
if ( include.find(trans) == include.end( ) ) {
continue;
}
}
++word_count[trans];
}
for ( auto &pair : word_count ) {
cout << pair.first << ": " << pair.second << endl;
}
return 0;
}
/* 结果
apple No, no. No Yes. yes,
^Z
no: 3
yes: 2
*/
初看有点懵,实际上注意关联容器迭代器的限制,multiset没有提供push_back 的函数,所以不能使用back_inserter