• Cpp浅析系列-STL之map


    前言

    CPP

    map有序的键值对容器,元素的键是唯一的,值允许重复。用比较函数 Compare 排序键。搜索、移除和插入操作拥有对数复杂度,即O(logn)。 底层实现为红黑树。

    Map定义

    需要包含模板类头文件,需要关键字和存储对象两个模板参数。

    这样就定义了一个用int作为索引,并拥有相关联的指向string的指针.

    1. #include <map> 
    2. using namespace std;
    3. void init() {
    4.     map<int, string> m1;//空对象
    5.     //自带初值
    6.     map<int, string> m2(
    7.             {
    8.                     {1"A"},
    9.                     {3"C"},
    10.                     {2"B"}
    11.             }
    12.     );
    13.     //默认按照索引less递增输出为
    14.     // 1 A
    15.     // 2 B
    16.     // 3 C
    17.     map<int, string,greater<int>> m3(
    18.             {
    19.                     {1"A"},
    20.                     {3"C"},
    21.                     {2"B"}
    22.             }
    23.     );
    24.     // 3 C
    25.     // 2 B
    26.     // 1 A
    27. }

    有时候为了使用方便,可以对模板类以及指针定义成为更简单的名字。

    1. typedef map<int,string> istrmap;
    2. typedef map<int,string>::iterator IT;
    3. istrmap map1;
    4. IT iter

    Map常规操作

    成员函数

    C++中文在线手册:https://zh.cppreference.com/

    元素访问
    at用索引访问指定的元素,同时进行越界检查
    [operator]用索引访问或插入指定的元素
    迭代器
    begin和cbegin(C++11)返回指向起始的迭代器
    end和cend(C++11)返回指向末尾的迭代器
    rbegin和crbegin(C++11)返回指向起始的逆向迭代器
    rend和crend(C++11)返回指向末尾的逆向迭代器
    容量
    empty检查容器是否为空
    size返回容纳的元素数
    max_size返回可容纳的最大元素数
    修改器
    clear清除内容
    insert插入元素或结点 (C++17 起)
    insert_or_assign(C++17)插入元素,或若键已存在则赋值给当前元素
    emplace(C++11)原位构造元素
    emplace_hint(C++11)使用提示原位构造元素
    try_emplace(C++17)若键不存在则原位插入,若键存在则不做任何事
    erase擦除元素
    swap交换内容
    extract(C++17)从另一容器释出结点
    merge(C++17)从另一容器接合结点
    查找
    count返回匹配特定键的元素数量
    find寻找带有特定键的元素
    contains(C++20)检查容器是否含有带特定键的元素
    equal_range返回匹配特定键的元素范围
    lower_bound返回指向首个不小于给定键的元素的迭代器
    upper_bound返回指向首个大于给定键的元素的迭代器
    观察器
    key_comp返回用于比较键的函数
    value_comp返回用于在value_type类型的对象中比较键的函数。

    增加元素

    总共有三种插入方式。

    1. void add1() {
    2.     map<int, string> m(
    3.             {
    4.                     {1"A"},
    5.                     {3"C"},
    6.                     {2"B"}
    7.             }
    8.     );
    9.     // 当索引是不存在的值,成功插入;当索引已经存在,则不进行操作
    10.     //调用make_pair函数模板,好处是构造对象不需要参数,用起来更方便
    11.     m.insert(pair<int, string>(24"Z"));
    12.     m.insert(map<int, string>::value_type(23"Y"));
    13.     m.insert(make_pair(1"Z"));
    14.     // 索引是原先没有的,直接插入;索引已经存在直接修改
    15.     m[22= "X";
    16.     m[3= "X";
    17.     // 当索引是不存在的值,成功插入;当索引已经存在,则不进行操作
    18.     m.emplace(pair<int, string>(21"W"));
    19.     m.emplace(pair<int, string>(1"W"));
    20.     map<int, string>::iterator iter;
    21.     for (iter = m.begin(); iter != m.end(); iter++) {
    22.         cout << iter->first << ' ' << iter->second << endl;
    23.     }
    24. }
    25. //1 A
    26. // 2 B
    27. // 3 X
    28. // 21 W
    29. // 22 X
    30. // 23 Y
    31. // 24 Z

    以上三种用法,虽然都可以实现数据的插入,但是它们是有区别的:

    insert函数和emplace函数插入数据,在数据的插入上涉及到集合的唯一性这个概念,即当map中有这个关键字时,insert操作是插入数据不了的。

    用索引[]方式就不同了,它可以覆盖对应的值。

    遍历元素

    强烈建议使用迭代器遍历集合!

    1. void search1() {
    2.     map<int, string> m(
    3.             {
    4.                     {1"A"},
    5.                     {3"C"},
    6.                     {2"B"}
    7.             }
    8.     );
    9.     map<int, string>::iterator iter;
    10.     for (iter = m.begin(); iter != m.end(); iter++) {
    11.         cout << iter->first << ' ' << iter->second << endl;
    12.     }
    13. }
    14. //1 A
    15. // 2 B
    16. // 3 C

    下面介绍一个反面例子,看看直接使用索引去遍历而产生的结果。

    1. void search2() {
    2.     map<int, string> m(
    3.             {
    4.                     {1"A"},
    5.                     {3"C"},
    6.                     {5"B"}
    7.             }
    8.     );
    9.     cout << "遍历前元素的个数:" << m.size() << endl;
    10.     for (int i = 0; i < m.size(); i++) {
    11.         cout << i << ' ' << m[i] << endl;
    12.     }
    13.     cout << "遍历后元素的个数:" << m.size();
    14. }
    15. //遍历前元素的个数:3
    16. // 0
    17. // 1 A
    18. // 2
    19. // 3 C
    20. // 4
    21. // 5 B
    22. // 遍历后元素的个数:6

    很明显,因为没有判定是否存在而是直接无脑使用,原意是遍历一遍集合,结果却是修改了集合!

    删除元素

    直接删除元素

    可以清空,也可以用迭代器删除指定范围元素或者单个元素。

    但是在遍历的时候要注意,使用迭代器删除元素后,迭代器可能会变成类似野指针的存在!

    1. /*
    2.  * 删除有两种方式,
    3.  * clear是直接清空
    4.  * erase是删除指定迭代器范围内的数字
    5.  * 也可以用来删除指定的单个元素
    6.  * */
    7. void del1() {
    8.     map<int, string> m(
    9.             {
    10.                     {1"A"},
    11.                     {2"B"},
    12.                     {3"C"}
    13.             }
    14.     );
    15.     //清空
    16.     m.clear();//{}
    17.     if (m.empty()) {//判断Vector为空则返回true
    18.         m.insert(pair<int, string>(4"D"));
    19.         m.insert(pair<int, string>(5"E"));
    20.         m.insert(pair<int, string>(6"F"));
    21.         //用迭代器删除单个元素,注意指针被删除后就失效了
    22.         map<int, string>::iterator iter = m.begin();
    23.         m.erase(iter);//所剩元素{5,E},{6,F},此时的iter仍然是{4,D}
    24.         cout << "错误的迭代器内容:" << iter->first << ' ' << iter->second << endl;
    25.         //删除一个范围, 只保留最后一个
    26.         m.erase(m.begin(), ++m.end()); //{6,F}
    27.         //通过关键字索引的数据存在就删除,并返回1;如果关键字索引的数据不存在就不操作,并返回0
    28.         m.erase(2);
    29.     }
    30.     map<int, string>::iterator iter;
    31.     for (iter = m.begin(); iter != m.end(); iter++) {
    32.         cout << iter->first << ' ' << iter->second << endl;
    33.     }
    34. }

    遍历集合并删除元素

    如果想要遍历整个map,并删除所有满足指定数值的应该如下:

    1. /*
    2.  * 遍历集合以删除指定条件的元素
    3.  * */
    4. void del2() {
    5.     map<int, string> m(
    6.             {
    7.                     {1"A"},
    8.                     {2"B"},
    9.                     {3"C"}
    10.             }
    11.     );
    12.     map<int, string>::iterator iter;
    13.     // 删除元素后,期望iter指针是继续指向{3,C}的,
    14.     // 但是经过iter++后,竟然又到了上一个元素!
    15.     // 很明显,删除元素后的迭代器变成了类似野指针的存在!
    16.     // for (iter = m.begin(); iter != m.end(); iter++) {
    17.     //     if (iter->first == 2 || iter->second == "B") {
    18.     //         m.erase(iter);
    19.     //     }
    20.     //     cout << iter->first << ' ' << iter->second << endl;
    21.     // }
    22.     //结果是:
    23.     // 1 A
    24.     // 2 B
    25.     // 1 A
    26.     // 3 C
    27.     // 正确做法应该是先复制出来一个临时迭代器
    28.     // 接着将原来的迭代器后移一位指向正常的元素
    29.     // 最后用临时迭代器删除指定元素!
    30.     // 第二步和第三步不能反了,否则也会影响到原来正常的迭代器!
    31.     for (iter = m.begin(); iter != m.end();) {
    32.         if (iter->first == 2) {
    33.             map<int, string>::iterator iterTemp = iter;
    34.             ++iter;
    35.             m.erase(iterTemp);
    36.         } else {
    37.             cout << iter->first << ' ' << iter->second << endl;
    38.             ++iter;
    39.         }
    40.     }
    41.     // 结果是
    42.     // 1 A
    43.     // 3 C
    44. }

    用迭代器删除元素,先是断言确定迭代器不是尾迭代器,接着将当前迭代器复制到一个新对象,最后返回的就是这个新的迭代器对象。调用_M_erase_aux方法删除迭代器指向的元素,并且节点数目减一。

    1. void _M_erase_aux(const_iterator __position)
    2.     {
    3.       _Link_type __y =
    4.  static_cast<_Link_type>(_Rb_tree_rebalance_for_erase
    5.     (const_cast<_Base_ptr>(__position._M_node),
    6.      this->_M_impl._M_header));
    7.       _M_drop_node(__y);
    8.       --_M_impl._M_node_count;
    9.     }

    查找函数

    count统计元素个数

    count函数是用来统计一个元素在当前容器内的个数。由于Map的特性,所以只能返回1或者0。

    1. /*
    2.  * 用count函数寻找元素,
    3.  * */
    4. void find1(set<int> s ){
    5.     if (s.count(4== 1) {
    6.         cout << "元素4存在"<<endl;
    7.     }
    8.     if (s.count(8== 0) {
    9.         cout << "元素8不存在";
    10.     }
    11. }

    追查源码,我发现他是用的find方法,将结果跟尾迭代器比较,如果不等于尾迭代器就是找到了,返回1;反之就是没找到,返回0。

    1.     find(const _Key& __k) const
    2.     {
    3.       const_iterator __j = _M_lower_bound(_M_begin(), _M_end(), __k);
    4.       return (__j == end()
    5.        || _M_impl._M_key_compare(__k,
    6.      _S_key(__j._M_node))) ? end() : __j;
    7.     }

    find获取元素迭代器

    1. /*
    2.  * 用find函数寻找元素,
    3.  * */
    4. void find2(set<int> s ){
    5.     if (s.find(4)!= s.end() ) {
    6.         cout << "元素4存在"<<endl;
    7.     }else{
    8.         cout << "元素4不存在";
    9.     }
    10.     if (s.find(8)!= s.end() ) {
    11.         cout << "元素8存在"<<endl;
    12.     }else{
    13.         cout << "元素8不存在";
    14.     }
    15. }

    而底层是调用的不带const标的find函数,函数体是一样的!而其中的核心逻辑就是用_M_lower_bound函数查找来确定位置。

    1.     _M_lower_bound(_Link_type __x, _Base_ptr __y, const _Key &__k){
    2.         while (__x != 0) {
    3.             if (!_M_impl._M_key_compare(_S_key(__x), __k))
    4.                 __y = __x, __x = _S_left(__x);
    5.             else
    6.                 __x = _S_right(__x);
    7.         }
    8.         return iterator(__y);
    9.     }

    比较函数

    key排序

    map中默认就是使用key排序的,自动按照key的大小,增序存储,这也是作为key的类型必须能够进行 < 运算比

    较的原因。

    首先看一眼map模板的定义,重点看下第三个参数: class Compare = less

    1. template < class Key, class T, class Compare = less<Key>
    2.            class Allocator = allocator<pair<const Key,T> > > class map;

    less相对的还有greater,都是STL里面的一个函数对象,那么什么是函数对象呢?

    函数对象:即调用操作符的类,其对象常称为函数对象(function object),它们是行为类似函数的对象。表现出一个函数的特征,就是通过“对象名+(参数列表)”的方式使用一个 类,其实质是对operator()操作符的重载。

    具体的例子可以去看另一篇文章:Cpp浅析系列-STL之set,这里就不赘述了。

    value排序

    逻辑上是先转为vector数组,接着将数组用指定的规则重新排序得到排序好的结果。至于是否用排序好的数组去转换为map对象则是看要求了。

    1. bool Special(pair<string, int> a, pair<string, int> b) {
    2.     return a.second < b.second;//从小到大排序
    3. }
    4. void specialCompare() {
    5.     // 初始map集合
    6.     map<string, int> m;
    7.     m["a"= 2;
    8.     m["b"= 3;
    9.     m["c"= 1;
    10.     // 转为vector集合
    11.     vector<pair<string, int> > demo(m.begin(), m.end());
    12.     for (auto it = demo.begin(); it != demo.end(); ++it) {
    13.         cout << (*it).first << " " << (*it).second << endl;
    14.     }
    15.     cout << endl;
    16.     // 排序后查看效果
    17.     sort(demo.begin(), demo.end(), Special);
    18.     for (auto it = demo.begin(); it != demo.end(); ++it) {
    19.         cout << (*it).first << " " << (*it).second << endl;
    20.     }
    21.     cout << endl;
    22.     // 转换为新的map集合,区别就是前后类型反了。
    23.     map<int, string> m2;
    24.     for (vector<pair<string, int> >::iterator it = demo.begin(); it != demo.end(); ++it){
    25.         m2[(*it).second]=(*it).first;
    26.     }
    27.     map<int, string>::iterator iter;
    28.     for (iter = m2.begin(); iter != m2.end(); iter++) {
    29.         cout << iter->first << ' ' << iter->second << endl;
    30.     }
    31. }
    32. //2
    33. // b 3
    34. // c 1
    35. //
    36. // c 1
    37. // a 2
    38. // b 3
    39. //
    40. // 1 c
    41. // 2 a
    42. // 3 b

    感谢

    C++中的STL中map用法详解

    C++ STL中Map的按Key排序和按Value排序

    感谢现在努力的自己。

     

  • 相关阅读:
    深入解读Prometheus Adapter:云原生监控的核心组件
    Redis—听说你速度跟甲斗一样快?——哨兵
    现货黄金中如何应用背离?
    洛谷P1518 [USACO2.4]两只塔姆沃斯牛 The Tamworth Two
    Mybatis的一级缓存
    Go语学习笔记 - 调用ffmpeg-api实现音频重采样
    第23个520情人节,女程序猿送男朋友什么?
    一次学习引发我对于 synchronized 的再理解
    实验二用机器指令和汇编指令编程
    Redis在SpringBoot项目中使用
  • 原文地址:https://blog.csdn.net/qq_41461536/article/details/126571669