引入

继上文 STL序列式容器使用注意、概念总结 继续总结关联式容器的概念以及一些使用事项。

关联式容器与容器适配器

基础容器

STL 中的关联式底层容器:RB tree, hash table,可以作为其他容器的底层结构。

1.RB tree

  1. RB tree 红黑树这玩意儿可是重量级的,书中从基本的树概念讲起,到二叉树、BSTAVL,然后才到 RB tree。简单来说,AVL 是高度极其平衡的 BST,任意节点的左右子树高度差不超过 1 。那为什么还要 RB tree 呢?因为实践证明 AVL 为了保持其平衡性需要经常进行旋转操作,影响性能。而 RB tree 减轻了对平衡的要求,根据它的定义(我实在没能背下来,也不知道发明它的人是怎么想的,牛B),任意节点的左右子树高度差有可能超过 1,由此带来的好处是不那么频繁的旋转操作。 在实际应用中,RB tree 的表现比 AVL 更好。
  2. 总结起来 RB tree 就是一个虽然不是严格平衡但也非常平衡的二叉搜索树,插入、删除、查找等操作都能保持在 \(O(log \space n)\) 的时间复杂度。
  3. 它的迭代器类型为 Bidirectional Iterator 双向迭代器。

2.hash table

  1. 真正的哈希表,目的是使得各项操作如插入、删除、查找的均摊时间开销是 \(O(1)\)。
  2. 哈希函数:将一个元素映射到一个“大小可接受的索引”。随着 C++ 标准的发展,现在几乎所有内置类型都自带了哈希函数,我遇到过 pair 和自定义类型需要自定义哈希函数(自定义方法见下文 unordered_map )。
  3. 哈希碰撞:哈希函数可能将不同的元素映射到相同的位置。
  4. 哈希碰撞的解决方法:
    1. 线性探测:存在主集团问题,即碰撞概率太大,大部分元素集中在一起,导致插入一个元素经常需要遍历经过这些集中在一起的元素才能找到一个空位,成本太高。
    2. 二次探测:消除了线性探测存在的主集团问题,但仍存在次集团问题,即若两个元素的哈希位置相同,则探测位置也相同,第二个元素需要探测第一个元素曾经走过的所有位置。
    3. 拉链法:表格存放的不再是 value每一个位置都是一个链表指针,将映射到相同位置的元素放在同一条链表中。
  5. 负载系数:元素个数除以表格大小(表格是存放 value 的地方)。除了拉链法,其他方法的负载系数都在 0-1 之间。
  6. STL 采用拉链法实现,自带的 vector 和自维护的 list(没有使用已经实现的双向循环链表 list)。相信大家都用过这种方法,它的一大局限是映射到同一个位置的元素过多,链表会变得很长,此时查找等操作的时间开销就很大了。STL 实现的哈希表有一个表格重整的机制,专门解决这种情况。
  7. 采用拉链法实现哈希表,表格的大小一般设置为质数以减少碰撞,STL 内置了 28 个质数。
  8. 它的迭代器 Forward Iterator 前向迭代器,只支持自增操作(++)。

容器适配器

以某种容器作为底层结构,改变其接口,使之符合某种特性。关联式容器适配器有:

  1. RB tree 为底层结构的 mapset
  2. hash table 为底层结构的 unordered_mapunordered_set

4 种容器适配器都不允许 key 重复,它们都有对应的允许 key 重复的版本:multimap, multiset, unordered_multimapunordered_multiset

这里不讲实现细节,讲讲一些注意事项和使用方法。

  1. 通过下标访问 map 时,一定要确保 key 已存在,否则将插入一个新建的 key-value 对,key 是指定的,value 是类型的默认值,如下代码所示:

    map<int, int> m;
    cout << boolalpha << (m.find(2) == m.end()) << endl; // true
    if (m[2] > 0) { // do something } // if 里潜在地插入了 m[2] = 0
    cout << boolalpha << (m.find(2) == m.end()) << endl; // false

    除非你本身需要这种特点,否则要特别注意喔。

  2. set 集合,key 就是 value,由于 key 不可变,所以我们不能通过 set 的迭代器更改其元素值,其迭代器是一种常量迭代器。

  3. mapset 会对插入的元素进行排序,因为底层的 RB tree 也是 BST,对 BST 进行中序遍历即可得到有序序列。

  4. 正如它们的名字所说的那样,unordered_mapunordered_set 不会对插入的元素排序。

  5. 如何为 unordered_map 自定义哈希函数和 key 比较函数呢?下面给出两个示例,一个针对自定义类类型,另一个针对 pair核心思想是重载函数调用符。

    一、为 pair 自定义哈希函数和 key 比较函数

    其实 pair 本身重载了比较函数,不自己定义也可以。

    #include <unordered_map>
    using namespace std; // 哈希函数
    struct MyKeyHash
    {
    size_t operator()(const pair<int, int>& p) const {
    // 第一种写法里第一个()是实例化一个hash<int>类对象
    // 第二个()是像函数一样使用该对象
    // 因为它重载了函数调用符
    // return hash<int>()(p.first) ^ hash<int>()(p.second); // 这第二种写法C++11开始支持
    // {}表示列表初始化
    // 类似vector<int>{1, 2, 3}
    return hash<int>{}(p.first) ^ hash<int>{}(p.second); // 异或一下
    }
    }; // key比较函数
    struct MyKeyEqual
    {
    bool operator() (const pair<int, int>& key1, const pair<int, int>& key2) const {
    return key1.first == key2.first && key1.second == key2.second;
    }
    }; int main()
    {
    unordered_map<pair<int, int>, int, MyKeyHash, MyKeyEqual> um;
    // unordered_map<pair<int, int>, int, MyKeyHash> um; // 其实pair本身重载了比较函数,不自己定义也可以。
    um.insert(pair<pair<int, int>, int>(pair<int, int>(1, 1), 2)); // 这句长到头皮发麻
    return 0;
    }

    哈希函数通过异或实现,网上说这种方法在数据量较大时发生碰撞的概率很大,导致性能很差。有位大佬的博客说他在《C++ 标准库》第二版这本书里找到了正解,用过都说好的哈希函数,链接在这:C++ pair 作为 unordered_map unordered_set 的键值

    二、为自定义类类型自定义哈希函数和 key 比较函数

    #include <unordered_map>
    #include <string>
    using namespace std; // 自定义一个Person类型
    class Person {
    private:
    int m_id; // 身份证
    int m_age; // 年龄
    string m_name; // 姓名
    // 其他成员
    public:
    int getId() const { return m_id; }
    int getAge() const { return m_age; }
    string getName() const { return m_name; } Person() = default;
    Person(int id, int age, string name): m_id(id), m_age(age), m_name(name) {}
    }; struct MyKeyHash
    {
    size_t operator()(const Person& p) const {
    // 这里只使用一个人的年龄和姓名进行哈希映射
    // return hash<int>{}(p.getAge()) ^ hash<string>{}(p.getName());
    return hash<int>()(p.getAge()) ^ hash<string>()(p.getName());
    }
    }; struct MyKeyEqual
    {
    bool operator() (const Person& p1, const Person& p2) const {
    // 当且仅当两个人的身证份一致时才认为是同一个人
    return p1.getId() == p2.getId();
    }
    }; int main()
    {
    unordered_map<Person, int, MyKeyHash, MyKeyEqual> um;
    um.insert(pair<Person, int>(Person(), 1));
    return 0;
    }

最后

如果你有疑惑,欢迎评论,我会尽可能回复!

如果本文对你有帮助,点个赞吧,这是我坚持的动力!

STL关联式容器使用注意、概念总结的更多相关文章

  1. STL——关联式容器

    一.关联式容器 标准的STL关联式容器分为set(集合)/map(映射表)两大类,以及这两大类的衍生体multiset(多键集合)和 multimap(多键映射表).这些容器的底层机制均以RB-tre ...

  2. 关联式容器(associative containers)

    关联式容器(associative containers) 根据数据在容器中的排列特性,容器可分为序列式(sequence)和关联式(associative)两种. 标准的STL关联式容器分为set( ...

  3. STL源码分析读书笔记--第5章--关联式容器

    1.关联式容器的概念 上一篇文章讲序列式容器,序列式容器的概念与关联式容器相对,不提供按序索引.它分为set和map两大类,这两大类各自有各自的衍生体multiset和multimap,的底层机制都是 ...

  4. STL学习笔记--关联式容器

    关联式容器依据特定的排序准则,自动为其元素排序.缺省情况下以operator<进行比较.set multiset map multimap是一种非线性的树结构,具体的说是采用一种比较高效的特殊平 ...

  5. C++ 容器:顺序性容器、关联式容器和容器适配器

    什么是容器 首先,我们必须理解一下什么是容器,在C++ 中容器被定义为:在数据存储上,有一种对象类型,它可以持有其它对象或指向其它对像的指针,这种对象类型就叫做容器.很简单,容器就是保存其它对象的对象 ...

  6. STL序列式容器学习总结

    STL序列式容器学习总结 参考资料:<STL源码剖析> 参考网址: Vector: http://www.cnblogs.com/zhonghuasong/p/5975979.html L ...

  7. C++关联式容器的排序准则

    stl中set和map为关联式容器,会根据排序准将元素自动排序.原型如下: template<class _Kty, class _Pr = less<_Kty>, class _A ...

  8. STL关联式容器之map和multimap

    一,map和multimap的概念 1.map和multimap的基本知识 map是标准的关联式容器,一个map是一个键值对序列,即(key,value)对.它提供基于key的快速检索能力. map中 ...

  9. STL关联式容器之set\map ----以STL源码为例

    关联式容器的特征:所用元素都会根据元素的键值自动被排序. set STL 中的关联式容器低层数据结构为红黑树,其功能都是调用低层数据结构中提供的相应接口. set元的元素不会像map那样同时拥有键(k ...

  10. Map(关联式容器)

    map是一类关联式容器,自动建立Key - Value的对应 , key 和 Value可以是任意你需要的类型.下面介绍 map 中一些常用的函数: 一.map 中的 begin 和 end 函数 m ...

随机推荐

  1. 为什么 softmax 计算时要先减去最大值

    根据 softmax 最基本的定义,计算公式如下所示: $$S_i=\frac{e^{x_i}}{\sum_j e^{x_j}}$$ 原理也很简单,将原向量变为分布的形式(和为1). 看似很美好,但是 ...

  2. for in 和 for of 的区别和v-for指令的三种使用方法

    for...in 循环:只能获得对象的键名,不能获得键值 for...of 循环:允许遍历获得键值 var arr = ['red', 'green', 'blue'] for(let item in ...

  3. 【网络】安装Nginx笔记

    目录 前言 安装前先更新下 安装依赖库 下载Nginx Nginx编译配置 编译&安装&验证nginx Nginx服务配置 配置SSL 参考 前言 up安装nginx主要是为了在服务器 ...

  4. 我的Python基础(二)

    python包含6种内奸的序列:列表.元组.字符串.Unicode字符串.buffer对象和xrange对象 列表和元组的主要区别在于,列表可以修改,元组则不能. 索引: 使用负数索引时,最后一个元素 ...

  5. Android Studio运行Failed to find Build Tools revision 30.0.3

    问题 第一次安装好Android Studio2022.5的版本之后开启虚拟机运行文件报错提示 Failed to find Build Tools revision 30.0.3 打开SDK已经安装 ...

  6. 【每日一题】【dfs重载原始函数&循环/函数结束条件&左右下标在数组中位置的确定】2022年2月7日-NC12 由先序和中序遍历重建二叉树

    描述给定节点数为 n 的二叉树的前序遍历和中序遍历结果,请重建出该二叉树并返回它的头结点.例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建 ...

  7. 【消息队列面试】15-17:高性能和高吞吐、pull和push、各种MQ的区别

    十五.kafka高性能.高吞吐的原因 1.应用 日志收集(高频率.数据量大) 2.如何保证 (1)磁盘的顺序读写-pagecache关联 rabbitmq基于内存读写,而kafka基于磁盘读写,但却拥 ...

  8. [OpenCV实战]19 使用OpenCV实现基于特征的图像对齐

    目录 1 背景 1.1 什么是图像对齐或图像对准? 1.2 图像对齐的应用 1.3 图像对齐基础理论 1.4 如何找到对应点 2 OpenCV的图像对齐 2.1 基于特征的图像对齐的步骤 2.2 代码 ...

  9. .NET周报【1月第1期 2023-01-06】

    国内文章 [开源]基于.net6+gtksharp实现的Linux下的图形界面串口调试工具 https://www.cnblogs.com/flykai/p/17007554.html 由于公司的上位 ...

  10. 面对集中式缓存实现上的挑战,Redis交出的是何种答卷?聊聊Redis在分布式方面的能力设计

    大家好,又见面了. 本文是笔者作为掘金技术社区签约作者的身份输出的缓存专栏系列内容,将会通过系列专题,讲清楚缓存的方方面面.如果感兴趣,欢迎关注以获取后续更新. 在本专栏前面的文章中,我们介绍了各种本 ...