一、关联式容器概述
- STL内部预先定义好的关联式容器有:
- set:元素依据其value自动排序,每个元素只能出现一次,不允许重复
- multiset:和set的唯一差别是,其元素可以重复
- map:每个元素都是key/value pair,其中key是排序准则的基准。每个key只能出现一次,不允许重复。Map也被视为一种关联式数组,也就是“索引可为任意类型”的数组
- multimap:和map的唯一差别是,其元素都可以重复。multimap可被当做字典使用
- 关联式容器依据特定的排序准则,自动为其元素排序。元素可以是任何类型的value,也可以是key/value pair,其中key可以是任何类型,映射至一个相关value,而value也可以是任意类型
- 排序准则:
- 对于只有value的关联式容器,例如set、multiset,其对value进行排序。对于key/value pair的关联式容器,例如map、multimap,其根据key进行排序
- 默认情况所有容器都以操作符<进行排序,不过你可以提供自己的比较函数,定义出不同的排序准则
- 关联式的底层实现数据结构:
- 关联式容器底层以平衡二叉树(红黑树)实现,源码剖析参阅:https://blog.csdn.net/qq_41453285/article/details/103645839
- 因为使用平衡二叉树实现,所以对于特定的key或value,其能很快的找到特定的元素,因为其操作具备的是对数复杂度,而普通的循序式容器的复杂度是线型的。因此,使用关联式容器,面对1000个元素,平均而言你只要10次比较操作而不是500比较就可以查找特定的元素
- 关联式容器拥有自动排序能力,并不意味着它们在排序方面的执行效能更高。实际上,由于股演练时容器每安插一个新元素都要进行一次排序,速度反而不及序列式容器经常采用的收发:先安插所有元素,然后调用STL提供的排序算法进行一次完全排序
二、map
格式
- map<key,value>:由“键值(key)与值(value)”两部分组成,这两者形成映射关系
- 头文件:#include <map>
特点
- key是唯一的,不可重复;但value可以重复出现
- key与value必须具有可赋值、可拷贝的性质
- key的数据类型必须是可比较的,根据key来进行排序(默认为升序)
- key与value可以是任何类型:int、doule、字符串、结构体、类......
- map的每一组key与value相当于一个pair容器对象(pair容器,见文章:https://blog.csdn.net/qq_41453285/article/details/100628402)
key的比较操作
- 我们知道key必须是可以比较的,如果key的类型不能进行比较(例如“<”运算符进行比较),则程序出错
- 如果使我们自定义的类,如果没有提供比较运算符的重载,那么就不能作为关联容器的key使用,因为自定义的类不能进行比较
- 如果key可以进行比较,且遵循下面图片中的规则:
升序、降序操作
- map的key默认为升序排序,但是我们也可以通过一些操作来实现降序操作,或者显式的进行升序操作
- 下面两个标准库函数(less、greater)都在头文件#include <xfunctional>中
- 演示默认的升序排序:
map<string, int,std::less<string>> word_count;//显式地升序 //map<string, int,> word_count;//默认的升序 string word; while (cin >> word) { ++word_count[word]; } for (const auto &w : word_count) { cout << w.first << " occurs " << w.second << ((w.second>1) ? " times" : " time") << endl; }
- 演示降序排序:
map<string, int,std::greater<string>> word_count;//降序 string word; while (cin >> word) { ++word_count[word]; } for (const auto &w : word_count) { cout << w.first << " occurs " << w.second << ((w.second>1) ? " times" : " time") << endl; }
初始化
- 如果没有给出初始化值,就默认为空容器(使用系统的默认值初始化)
- 手动给出值初始化
下标运算符和at函数
- map和unordered_map提供下标操作,因为其key唯一;multimap与unordered_multimap不提供下标操作,因为其key不唯一
- 下标运算符可以用来创建一个键值对或访问键所对应的值
- 由于下标运算符可能插入一个新元素,我们只可以对非const的map使用下表操作
- 下标运算符则:
- 演示案例:
map<string, size_t> word_count; word_count["a"] = 1; //添加key与value对 word_count["b"] = 2; //添加key与value对 word_count["c"] = 3; //添加key与value对 word_count["a"] = 4; //更改key对应的value cout << word_count["a"] << endl; //打印4 cout << word_count.at("a") << endl; //打印4
迭代器与遍历
- 当解引用一个关联容器迭代器时,我们得到的是容器的value_type的值的引用
- 对于map来说,value_type为一个pair类型,因此得到pair类型之后,我们就可以调用first和second来访问key与value
- 重点:得到的迭代器,我们可以改变其second(value)的值,但不能改变其first(key)的值(因为为const类型)
map<string, int> word_count; word_count["Hello"] = 3; word_count["World"] = 3; auto map_it = word_count.begin(); //得到第一个元素的迭代器 //*map_it是指向一个pair<const string,size_t>的引用 cout << map_it->first << endl;; //打印Hello cout << map_it->second << endl; //打印3 map_it->first = "new key";//错误,key关键字不可以改变(为const类型) ++map_it->second; //正确,key关键字对应的value可以改变
- 通过迭代器遍历容器:可以使用iterator或者const_iterator类型的迭代器遍历
map<string, int> word_count; word_count["Hello"] = 3; word_count["World"] = 3; //map_it为map<string, int>::const_iterator类型 auto map_it = word_count.cbegin(); while (map_it != word_count.cend()) { cout << map_it->first << "" << map_it->second << endl; ++map_it; }
添加元素(insert、emplace)
- 这里介绍的添加元素方式,适合所有关联容器。规则为:
- 对于key唯一的,插入时会先判断key是否存在,不存在则插入;存在则什么都不做
- 对于key不唯一的,每次插入都插入新值
- insert有3个版本:参数为value_type或参数为一对迭代器或者参数为一个初始化列表
- 演示set的插入:
vector<int> ivec = { 2,4,6,8,10 }; set<int> set1; set1.insert(ivec.cbegin(), ivec.cend()); //迭代器版本 set1.insert({ 1,3,5,7,9 }); //初始化器版本插入
- map的插入:
- 由于map的key是pair容器类型,因此map的插入有一些特殊情况
map<string, size_t> word_count; word_count.insert({ "a",1 }); word_count.insert(make_pair("a",1)); word_count.insert(pair<string,size_t>("a", 1)); word_count.insert(map<string,size_t>::value_type("a", 1));
- insert/emplace的返回值:
//案例:单词计数 map<string, size_t> word_count; //空的 string word; while (cin >> word) { //插入一个元素,关键字等于word,值为1 //若word已经存在,insert什么都不做 auto ret = word_count.insert({ word,1 }); if (!ret.second) //检查返回值的bool部分,若为false,则word已在word_count中 ++ret.first->second; //递增计数器 }
- 展开递增语句:
删除元素(erase)
- 关联容器定义了3个版本的erase:
- ①传递一个key_type参数。如果元素存在(删除所有匹配给定关键字的元素,返回实际删除的元素数量);如果元素不存在(函数返回0)
- ②传递给erase一个迭代器来删除一个元素,函数返回void(与顺序容器类似)
- ③传递给erase一个迭代器对来删除一个元素范围,函数返回void(与顺序容器类似)
演示案例
map<string, size_t> word_count; word_count["a"] = 1; word_count["b"] = 2; word_count["c"] = 3; string removal_word = "a"; if (word_count.erase(removal_word)) cout << "ok:"<< removal_word <<" removed" <<endl; else cout << "error:" << removal_word << " not found" << endl;
其他操作
- 其他关联容器都可以参数这里的知识点
对map使用find代替下标操作
- 如果使用下标运算符来查找map、unordered_map中的一个关键字,会有一个缺点,就是如果这个关键字不存在,则会插入这个关键字。因此想要寻找一个关键字是否存在map中,最好使用find函数
map<string, size_t> word_count; //空的 //如果不存在,返回map的尾后迭代器 if (word_count.find("abcd") == word_count.end()) cout <<"abcd is not in the map" << endl;
在multimap、multiset中查找元素
- multimap、multiset中的key可以重复,并且相同的key会相邻存储,因此如果想要查看相同的key键值,可以使用count和find函数配合查看
multimap<string, string> author; //空的 author.insert({ "C Primer","Bob" }); author.insert({ "C++ Primer","Tom" }); author.insert({ "C++ Primer","Alice" }); author.insert({ "Java","Alan" }); auto entried = author.count("C++ Primer"); //计算出键为“C++ Primer”的数量 auto iter= author.find("C++ Primer");//先找到第一个迭代器位置 while (entried) { //循环遍历 cout << iter->second<< endl; ++iter; --entried; }
lower_bound、upper_bound函数
- 在key可以重复的容器中,lower_bound返回这个key所对应的第一个位置,upper_bound返回最后一个匹配给定关键字的元素的下一个位置
- 如果key不存在,lower_bound和upper_bound返回相等的迭代器——指向一个不影响排序的关键字插入位置
- 根据上面的图片可知,如果key不存在容器中,且大于所有的关键字,则lower_bound返回的就是尾后迭代器
- 这两个函数可以实现上面在multimap、multiset中查找重复的key。见下面案例
multimap<string, string> author; //空的 author.insert({ "C Primer","Bob" }); author.insert({ "C++ Primer","Tom" }); author.insert({ "C++ Primer","Alice" }); author.insert({ "Java","Alan" }); //beg指向于key为"C++ Primer"的第一个位置,end指向于最后一个位置的下一个位置 for (auto beg = author.lower_bound("C++ Primer"), end = author.upper_bound("C++ Primer"); beg != end; ++beg) { cout << beg->second << endl; }
equal_range函数
- 该函数接受一个关键字,然后一个参数为迭代器的pair容器类型
- 若关键字存在,则pair容器中的第一个迭代器指向于第一个与关键字匹配的元素,第二个迭代器指向最后一个匹配元素之后的位置;若不存在关键字,两个迭代器都指向关键字可以插入的位置
- 使用来函数也可以实现上面find、lower_bound等函数来遍历相同的key的操作,见下面演示案例
multimap<string, string> author; //空的 author.insert({ "C Primer","Bob" }); author.insert({ "C++ Primer","Tom" }); author.insert({ "C++ Primer","Alice" }); author.insert({ "Java","Alan" }); for (auto pos = author.equal_range("C++ Primer"); pos.first != pos.second; ++pos.first) { cout <<pos.first->second << endl; } /*pos为pair<iterator1,iterator2>类型, iterator1为指向于第一个"C++ Primer"的迭代器, iterator2指向于最后一个"C++ Primer"的后一个位置*/
演示案例
- 利用map来输入保存的字符串,然后输出字符串出现的次数
#include <iostream> #include <map> #include <string> using namespace std; int main() { //key为string类型,value为size_类型 map<string, size_t> word_count; string word; while (cin >> word) { ++word_count[word]; } for (const auto &w : word_count) { cout <<w.first <<":"<<w.second<<((w.second>1)?"times":"time")<< endl; } return 0; }
三、multimap
格式
- 与map相同:由“键值(key)与值(value)”两部分组成,这两者形成映射关系
- 头文件:#include <map>
特点
- key可以重复(如果有相同的key,则相邻存储),默认也为升序排序
- 其它特点与map相同
- multimap不提供下标操作(下标运算符和at函数),因为key可以重复
升序、降序操作
- 与map相同,key默认是升序操作,我们也可以将key设置为显式地升序或者设置为降序排序
- 下面两个标准库函数(less、greater)都在头文件#include <xfunctional>中
- 演示升序操作
multimap<string, int, std::less<string>> word_count;//升序 //multimap<string, int> word_count;//升序 word_count.insert({ "a",1 }); word_count.insert({ "c",1 }); word_count.insert({ "b",1 }); for (const auto &w : word_count) { cout << w.first << " occurs " << w.second << ((w.second>1) ? " times" : " time") << endl; }
- 演示降序操作
multimap<string, int,std::greater<string>> word_count;//降序 word_count.insert({ "a",1 }); word_count.insert({ "c",1 }); word_count.insert({ "b",1 }); for (const auto &w : word_count) { cout << w.first << " occurs " << w.second << ((w.second>1) ? " times" : " time") << endl; }
初始化
- 如果没有给出初始化值,就默认为空容器
迭代器与遍历
- 与map相似,见上面map的介绍
添加元素(insert、emplace)
- 与map相似,见上面map的介绍
删除元素(erase)
- 与map相似,见上面map的介绍
其他操作
- 与map相似,见上面map的介绍