概念

先了解一下关联容器涉及到的一些概念。
点击查看【processon】
顺序容器:通过元素在容器位置来保存、访问。
关联容器:通过关键字key来保存、访问。
关联容器也是模板。

分类

标准库有8个关联容器。
两大类:

  • set
    • 元素是关键字,适合用于判断是否存在之类的需求,关键字的集合。
  • map
    • 元素是键值对,key起到索引作用。
    • 看成数组,下标是key,内置数组下标是整型。

根据key的特性可以再细分:

  • key是否可重复
    • 容器类型名中有multi字眼,则表示容器的key可重复,否则不可重复
    • 插入重复key元素,插入失败,什么也不做。
  • key是否有序
    • 无序:通过哈希函数来组织元素。
    • 有序:通过key比较运算来组织元素,默认是调用key类型的<运算符。
    • 容器类型名中有unordered字眼,则表示容器的key是无序的;否则是有序的。 | 关联容器类型 | key是否可重复 | key是否有序 | | :—-: | :—-: | :—-: | | map | 否 | | | set | 否 | | | multimap | | | | multset | | | | unordered_map | 否 | 否 | | unordered_set | 否 | 否 | | unordered_multimap | | 否 | | unordered_multiset | | 否 |

自定义有序key

自定义的比较必须满足以下:

  • 反对称性
    • key1<=key2,则key2不能<=key1
  • 传递性
    • key<=key2,key2<=key3,则key1<=key3
  • 等价传递性
    • 等价:key1==key2,即key1!<=key2,key2!<=key1
    • key1==key2,key2==key3,则key1==key3 ```cpp /**如何自定义有序关联容器*/ bool compare(const int &a, const int &b){ return a < b; }

multiset bookstore(compare);

  1. <a name="xOLLx"></a>
  2. # pair类型
  3. 定义utility头文件。<br />保存两个对象,类模板,必须提供两个数据的类型,类型几乎没有限制。<br />成员是public的,分别是first、second。<br />默认构造函数是对数据成员进行值初始化。<br />**pair操作**
  4. ```cpp
  5. pair<T1 ,T2> p; //默认构造,first为T1类型,second为T2类型,进行了值初始化。
  6. pair<Tl,T2> p(v1,v2); //first=v1,second=v2
  7. pair<T1,T2> p = {v1,v2}; //等价于p(v1,v2)
  8. make_pair(v1,v2); //返回一个pair,类型自行推断。
  9. //代替pair<T1, T2>(v1, v2),简化操作。
  10. p.first; //返回first数据
  11. p.second; //返回second数据
  12. p1 < p2; //利用first、second的对应运算符来确定
  13. p1 <= p2;
  14. p1 > p2;
  15. p1 >= p2;
  16. p1 == p2; //p1.first==p2.first && p2.second==p2.second
  17. p1 != p2; //上面的逻辑反

pair使用例子

  1. pair<string, string> anon; //默认构造,保存两个string,值为空
  2. pair<string, size_t> word_count; //默认构造,保存一个string 和一个size_t,值为空和0
  3. pair<string, vector<int>> line; //默认构造,保存 string 和 vector<int>,值为空和空。
  4. pair<string, string> author{ "James", " Joyce"};
  5. pair<string , int> process(vector<string> &v){
  6. if (!v.empty())
  7. return {v.back(), v.back().size()}; //列表初始化
  8. else
  9. return pair<string, int>(); //隐式构造返回值
  10. }

迭代器

关联容器迭代器解引用返回元素的引用,即value_type&。
关联容器的key是const,不可修改。
遍历容器和顺序容器的方式一样。
遍历一个有序关联容器时,迭代器按key升序遍历元素 ,比如字典顺序。

  1. it; //假设为关联容器迭代器
  2. *it; //返回元素的引用。
  3. //set:const key_type&
  4. //map: pair<const key, value_type>&
  5. /*****************关联容器的key是const类型*********************/
  6. auto map_it = word_count.begin(); //获得指向word_count中一个元素的迭代器
  7. auto elem = *map_it; //(*map_it)是指向一个pair对象的引用
  8. //类型为pair<const string, size_t>
  9. /*****************关联容器的遍历*********************/
  10. cout << map_it->first; //打印此元素的关键字
  11. cout << map_it->second; //打印此元素的值
  12. map_it->first = "newkey"; //错误:关联容器的key都是const,也就是不能修改的。
  13. ++map_it->second; //正确:我们可以通过迭代器改变元素
  14. set<int> iset = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
  15. set<int>::iterator set_it = iset.begin();
  16. if (set_it != iset.end()) {
  17. *set_it = 42 ; //错误: 关键字const
  18. cout << *set_it << endl; //正确: 可以读关键字
  19. }
  20. /*****************关联容器的遍历*********************/
  21. auto map_it = wordcount.cbegin(); //得一个指向首元素的迭代器
  22. //因为是有序关联容器,按字典顺序输出。
  23. while(map_it! = word_count.cend()){ //比较当前迭代器和尾后迭代器
  24. cout << map_it->first << "occurs" //关键字key
  25. << map_it->second << "times" //元素值value
  26. << endl;
  27. ++map_it; //递增迭代器,移动到下一个元素
  28. }

操作

关联容器支持C++_通用容器的全部操作。还有以下特有操作:

类型别名

  1. key_type //关键字类型
  2. value_type //元素类型
  3. //set:key_type == value_type
  4. //map:pair<const key_type , mapped_type>
  5. mapped_type //只有map才有:每个关键字关联的类型;
  1. set<string>::value_type v1; //v1是一个string
  2. set<string>::key_type v2; //v2是一个string
  3. map<string, int>::value_type v3; //v3是一个pair<const string,int>
  4. map<string, int>::key_type v4; //v4是一个string
  5. map<string, int>::mapped_type v5; //v5是一个int
  6. map<string, size_t> word_count; //空容器

初始化

  1. //列表初始化
  2. set<string> exclude= {
  3. "the", "but", "and", "or", "an", "a",
  4. "The","But", "And", "Or", "An", " A"
  5. };
  6. //三个元素:authors 将姓映射为名
  7. map<string, string> authors= {
  8. {"Joyce", "James"},
  9. {"Austen", "Jane"},
  10. {" Dickens", "Charles"}
  11. };
  12. /*****************set和multiset的差别**********************/
  13. //定义一个有20个元素的vector,保存0到9每个整数的两个拷贝
  14. vector<int> ivec;
  15. for(vector<int>::size_type i = O; i != 10 ; ++i) {
  16. ivec.push_back(i);
  17. ivec.push_back(i); //每个数重复保存一次
  18. }
  19. //iset包含来自ivec的不重复的元素;miset包含所有20个元素
  20. set<int> iset(ivec.cbegin(), ivec.cend());
  21. multiset<int> miset(ivec.cbegin(), ivec.cend());
  22. cout << ivec.size() << endl; //打印出 20
  23. cout << iset.size() << endl; //打印出 10
  24. cout << miset.size() << endl; //打印出 20

插入

  1. //插入单个元素
  2. //返回:pair<it, bool>,it指向插入元素,bool表示是否插入成功。
  3. //非multi时,key重复则不会插入。
  4. pair<it, bool> p = c.insert(v); //v是value_type类型对象
  5. pair<it, bool> p = c.emplace(args); //args用来构造元素,map的元素是pair,set的元素是key
  6. //插入范围元素,返回void
  7. //非multi时,只插入不重复的元素
  8. void c.insert(b, e); //是value_type类型的范围。
  9. void c.insert(il); //初始值列表,值类型是value_type
  10. //类似c.insert(v)、c.emplace(args)
  11. //返回迭代器ti,指向有给定关键字的元素
  12. //迭代器p指出从哪里开始搜索新元素应该存储的位置
  13. it = c.insert(p, v);
  14. it = c.emplace(p, args);

插入元素例子

  1. vector<int> ivec = {2, 4, 6, 8, 2, 4, 6, 8}; //ivec 有 8 个元素
  2. set<int> set2; //空
  3. set2.insert(ivec.cbegin(), ivec.cend()); //set2有4个元素:key是不可重复的。
  4. set2.insert({1, 3, 5, 7, 1, 3, 5, 7}); //set2现在有8个元素
  5. //map的插入insert的四种方法
  6. word_count.insert({word, 1}) ;
  7. word_count.insert(make_pair(word, 1)) ;
  8. word_count.insert(pair<string, size_t>(word, 1));
  9. word_count.insert(map<string, size_t>::value_type(word, 1));
  10. /********************统计单词重复次数例子************************/
  11. map<string, size_t> word_count; string word;
  12. while( cin >> word){
  13. //插入单词,成功则表示单词没有重复
  14. auto ret = word_count.insert({word,l}); //key=word,value=1
  15. if(!ret.second) ++ret.first->second; //单词重复一次
  16. }

删除

  1. //n:删除的元素数量。
  2. size_type n = c.erase(key); //删除指定key的元素
  3. //it:p之后的元素迭代器,可用于循环多次删除。
  4. iterator it = c.erase(p); //删除指定迭代器的元素
  5. //返回e,即范围之后的元素。
  6. iterator e = c.erase(b, e); //删除迭代器范围的元素。

访问

通用访问

  1. c.find(k); //查找第一个key==k的元素,没找到返回end()
  2. //返回;迭代器
  3. c.count(k); //返回key==k的元素数量。非multi时,返回1或0。
  4. //不适用于无序容器。
  5. c.lower_bound(k); //找到第一个key不小于k的元素,左闭右开原则的左闭
  6. //返回迭代器
  7. //不适用于无序容器。
  8. c.upper_bound(k); //找到第一个大于k的元素,左闭右开原则的右开
  9. //返回迭代器
  10. c.equal_range(k); //找到key==k的范围
  11. //返回:pair<it1, it2>

例子

  1. set<int> iset = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9} ;
  2. iset.find(1); //返回一个迭代器,指向key==1的元素
  3. iset.find(11); //返回一个迭代器,其值等于iset.end()
  4. iset.count(1); //返回1
  5. iset.count(ll); //返回0
  6. string search_item("Alain de Botton"); //要查找的作者
  7. auto entries = authors.count(search_item) ; //元素的数量
  8. auto iter = authors.find(search_item); //此作者的笫一本书
  9. while(entries) { //用一个循环查找此作者的所有著作
  10. cout << iter->second << endl; //打印每个题目
  11. ++iter; //前进到下一本书
  12. --entries; //记录已经打印了多少本书
  13. }
  14. string search_item("Alain de Botton"); //要查找的作者
  15. auto begin = authors.lower_bound(search_item);
  16. auto end = authors.upper_bound(searchitem)
  17. for (auto beg = begin, beg != end; ++beg)
  18. cout << beg->second << endl ;
  19. string search_item("Alain de Botton"); //要查找的作者
  20. auto range = authors.equal_range(search_item)
  21. for(auto begin = range; begin.first != begin.second; ++begin.first)
  22. cout << begin.first->second << endl;

map特有:下标操作

两种下标操作方式:

  • c[key]
    • key存在,返回mapped_type对象。注意不是元素(pair对象)
    • key不存在,会创建一个元素,返回mapped_type对象
  • c.at[key]函数
    • key存在,返回mapped_type对象
    • key不存在,抛出一个out_of_range异常 ```cpp //返回k关联的mapped_type对象,注意不是元素,map的元素是pair对象, //返回左值,可以修改mapped_type对象。 c[k]; //k不存在,则创建元素,并对其值初始化 c.at(k); //带参数检查;若k不在c中,抛出一个out_of_range异常

//下标和at操作只适用于非const的map和unordered_map,

  1. **为什么是下标操作,不是下标访问****?**
  2. - 第一,不是返回元素,而是返回mapped_type对象。
  3. - 第二,操作可能会创建元素。
  4. 总之,map的下标操作有访问的作用,但是不能看成是访问。
  5. <a name="6NMNl"></a>
  6. # 无序关联容器
  7. 标准定义了4个无序关联容器( unordered associative container),通过一个哈希函数 ( hash function ) key的==运算符来组织元素。<br />采用[桶式散列](https://www.yuque.com/tvvhealth/cs/ucqi75#KaZbJ)来存储元素,每个桶对应一个哈希值,相同key的哈希值一样,不同key的哈希值可能一样。性能取决于哈希质量和桶的大小。
  8. <a name="TqueJ"></a>
  9. ## 操作
  10. <a name="QEmz8"></a>
  11. ### 管理桶
  12. ```cpp
  13. c.bucket_count(); //当前桶数量
  14. c.max_bucket_count(); //容器能容纳的最大桶数量
  15. c.bucket_size(n); //第n个桶有多少元素
  16. c.bucket_(k); //k在哪个桶。
  17. local_iterator; //访问桶中元素的迭代器
  18. const_local_iterator; //同上,const版
  19. c.begin(n), c.end(n); //桶n的首尾迭代器
  20. c.cbegin(n), c.cend(n); //同上,const版

管理哈希

  1. c.load_factor(); //每个桶的平均元素数量,float
  2. c.max_load_factor(); //试图维护桶平均大小,返回float,可能添加新桶来维持
  3. //load_factor <= max_load_factor
  4. c.rehash(n); //重组,使用了一段时间后,可以考虑重组一下,减少哈希冲突提高性能。
  5. //当前桶数量:bucket_count
  6. //使bucket_count >= n
  7. //bucket_count > size/max_load_factor
  8. c.reserve(n); //重组,使得c保存n个元素不会发生rehash
  9. //使得在创建map的时候hash好,在使用的时候可以快速访问。

关键字类型要求

一句话,无序容器的key默认支持内置类型,key是自定义类型的话,需要自定义哈希函数和==。
因为无序容器时根据key来生成哈希值的,所以哈希函数必须支持key的类型对象。
标准库已经为内置数据类型定义了hash模板,也就是说有支持内置类型的哈希函数,所以我们可以直接使用key为内置类型的无序容器。
标准库还定义了部分标准库类型的hash模板,如string、智能指针。

我们不能直接定义key是自定义类型的无序容器,因为标准库肯定不能提前帮你设计好你的自定义类型的哈希函数。所以我们必须自己设计好哈希函数和==函数。下面是定义一个key是自定义类型无序容器的例子。

  1. size_t hasher(const Fucker &sd){
  2. return hash<int>()(sd.isbn());
  3. }
  4. bool eqOp(const Fucker &lhs, const Fucker &rhs){
  5. return lhs.isbn() == rhs.isbn();
  6. }
  7. using Fucker_multiset = unordered_multiset<Fucker, decltype(hasher)*, decltype(eqOp)*>;
  8. //42:桶大小
  9. //hasher:哈希函数指针
  10. //eqOp==:相等性判断函数指针
  11. Fucker_multiset obj(42 , hasher, eqOp);

泛型算法不适用

关联容器一般不用泛型算法,而是用自己的。
写、重排算法是肯定不能使用,因为key是const的。
读算法一般是查找,但是泛型find是顺序查找,关联容器是通过key来获取,不适用,所以有关联容器专门的find算法。