【面试】标准库相关题型(三)

news2024/11/16 9:44:44

文章目录

    • 1. unordered_map底层实现原理
      • 1.1 散列表
      • 1.2 STL 中的 unordered_map 的实现
      • 1.3 unordered_map
    • 2. 迭代器底层实现原理及种类
      • 2.1 主要作用
      • 2.2 底层原理
      • 2.3 迭代器类型属性
    • 3. 迭代器失效
      • 3.1 容器类别
      • 3.2 失效情况
      • 3.3 C++11容器类别
    • 4. STL容器的线程安全
      • 4.1 背景知识
      • 4.2 解决方案

1. unordered_map底层实现原理

一句话概括:unordered_map 是基于散列表实现的 map。

1.1 散列表

  1. 定义和操作

    散列表是一种数据结构,它通过哈希函数把键(key)映射到哈希表的一个位置,然后在这个位置上存储键值对(key-value pair)。在 C++ 中,哈希表可以通过一个数组和一个哈希函数实现。

    template<typename Key, typename Value>
    class HashTable {
        vector<list<pair<Key, Value>>> data;  // 哈希表数据
        // 其他数据成员和函数成员...
    };
    
  2. 哈希碰撞和解决方案

    哈希碰撞是指多个不同的键被哈希函数映射到哈希表的同一个位置。主要有以下三种处理哈希碰撞的方法:

    • 线性探测:当碰撞发生时,向后寻找下一个空位存放数据。
    • 开放寻址:同样是在发生碰撞时向后寻找,但会使用二次哈希等方法改变探测的步长。
    • 拉链法:使用链表处理碰撞,把同一位置的键值对存入一个链表。

    STL 中的 unordered_map 使用的是拉链法处理哈希碰撞。

    // 拉链法的实现示例
    int hashFunc(Key key) { /* 哈希函数... */ }
    
    void insert(Key key, Value value) {
        int index = hashFunc(key);
        data[index].push_back(make_pair(key, value));  // 在链表尾部添加键值对
    }
    
  3. 负载因子

    负载因子是散列表的实际元素数量和位置数量(桶的数量)的比值。当负载因子过高时,哈希碰撞的概率增加,查询效率降低。

  4. 重新哈希

    当负载因子大于预设阈值(通常为1)时,会发起重新哈希(rehash),即扩大位置数量,并重新分配所有元素。

    void rehash() {
        // 创建新的更大的哈希表,把所有元素重新分配到新的哈希表
    }
    

1.2 STL 中的 unordered_map 的实现

  1. 哈希表的模板参数

    unordered_map 采用了复杂的模板参数设计,主要包括键类型 _Key、值类型 _Value、内存分配器 _Alloc、提取键的函数 _ExtractKey、比较键是否相等的函数 _Equal、哈希函数 _H1、映射函数 _H2、调用哈希函数和映射函数的函数 _Hash、重新哈希策略 _RehashPolicy 以及内存相关的 _Traits 等。

  2. 数据成员

    • _M_buckets:哈希表的指针数组,数组的每个元素是一个链表。
    • _M_bucket_count:数组长度,即哈希表的位置数量。
    • _M_element_count:实际存储的元素数量。
    • _M_before_begin:一个特殊的节点,它的下一个节点是哈希表的第一个节点。
    • _M_rehash_policy:重新哈希的策略对象,它决定何时需要重新哈希。
    • _M_single_bucket:一个临时的单节点桶,用于临时存储元素。
  3. 节点定义

    每个节点是一个结构体,包含了键值对的存储空间 _M_storage 和其他需要的信息。

    struct _Hash_node : _Hash_node_base {
        __stored_ptr _M_storage;
    };
    
  4. 主要的接口

    unordered_map 主要的接口包括插入、查找、删除等操作,它们的实现都是调用哈希表的对应函数。其中,插入操作 _M_insert_bucket_begin 是先确定插入的位置,然后在这个位置的链表头部插入新的键值对。

1.3 unordered_map

  1. 定义

    unordered_map 是一个模板类,它的模板参数包括键类型、值类型、哈希函数、比较函数和内存分配器。它的主要数据成员是一个 _Hashtable 对象。

    template<typename _Key, typename _Tp, typename _Hash = std::hash<_Key>, typename _Pred = std::equal_to<_Key>, typename _Alloc = std::allocator<std::pair<const _Key, _Tp>>>
    class unordered_map {
        // 使用 __umap_hashtable 作为底层的哈希表实现
        typedef __umap_hashtable<_Key, _Tp, _Hash, _Pred, _Alloc> _Hashtable;
        _Hashtable _M_h;
        // 其他函数成员...
    };
    
  2. 插入操作

    插入操作 insert 实际上是调用 _M_insert_bucket_begin 函数,把新的键值对插入到哈希表的对应位置。

    pair<iterator, bool> insert(const value_type& __x) {
        return _M_h._M_insert_bucket_begin(__x, _M_h._M_bucket_index(__x));
    }
    
  3. 特点

    unordered_map 是无序的,它不保证元素的顺序。它的搜索、插入和删除操作的时间复杂度都接近 O(1),这是通过哈希表实现的。当哈希碰撞很少时,这些操作的时间复杂度可以认为是常数时间。

2. 迭代器底层实现原理及种类

一句话概括:迭代器提供了一种访问容器内部元素,同时不会暴露容器内部实现的方式。

2.1 主要作用

  1. 解引用和成员访问

    迭代器主要用于遍历和访问容器中的元素。通过解引用迭代器(例如 *it),可以访问当前迭代器所指向的元素。通过成员访问(例如 it->member),可以访问当前元素的成员。

  2. 统一不同容器的访问方式

    迭代器可以统一不同容器的访问方式,使得算法可以对不同类型的容器进行操作。例如,findmin_elementupper_boundreversesort 等算法,它们都接收迭代器作为参数,用于访问容器的元素。

    vector<int> vec = {1, 2, 3, 4, 5};
    auto it = find(vec.begin(), vec.end(), 3);  // find 在 vector 中使用
    

2.2 底层原理

  1. 考虑哪些问题

    在设计迭代器时,需要考虑的主要问题有:

    • 泛型编程:迭代器不是基于面向对象的思想编程,而是基于泛型编程的思想。泛型编程注重代码的可复用性,可以编写出容器无关的代码。
    • 通用算法问题:需要确定迭代器的类型,然后根据迭代器的类型来实现相应的算法。因此,需要定义迭代器的类型别名,这样可以通过类型别名来获取迭代器的类型。对于指针类型,由于不能定义类型别名,因此需要通过泛型特化的方式来处理。
  2. 定义了迭代器的5种类型

    STL 定义了5种迭代器,分别是:

    • 输入迭代器(Input Iterator):只读,只能向前移动。
    • 输出迭代器(Output Iterator):只写,只能向前移动。
    • 前向迭代器(Forward Iterator):可读写,只能向前移动。
    • 双向迭代器(Bidirectional Iterator):可读写,可以向前和向后移动。
    • 随机访问迭代器(Random Access Iterator):可读写,可以随机访问。

    这五种迭代器之间存在包含关系,具体为:输入迭代器 < 前向迭代器 < 双向迭代器 < 随机访问迭代器,输出迭代器独立于此关系。

  3. 迭代器萃取

    迭代器萃取是指从迭代器类型中提取出迭代器的特性,例如迭代器的类别、值类型、指针类型、引用类型等。这是通过模板的特化来实现的。

    template<class Iterator>
    struct iterator_traits {
        typedef typename Iterator::iterator_category iterator_category;
        typedef typename Iterator::value_type        value_type;
        typedef typename Iterator::difference_type   difference_type;
        typedef typename Iterator::pointer           pointer;
        typedef typename Iterator::reference         reference;
    };
    

    对于指针类型,需要进行特化:

    template<class T>
    struct iterator_traits<T*> {
        typedef std::random_access_iterator_tag iterator_category;
        typedef T                               value_type;
        typedef ptrdiff_t                       difference_type;
        typedef T*                              pointer;
        typedef T&                              reference;
    };
    
  4. 函数重载

    可以通过函数重载,根据不同的迭代器类型选择不同的算法实现。例如,对于随机访问迭代器,可以使用更高效的算法。

    template <class RandomAccessIterator>
    inline void sort(RandomAccessIterator first, RandomAccessIterator last, random_access_iterator_tag) {
        // 高效的排序算法
    }
    
    template <class BidirectionalIterator>
    inline void sort(BidirectionalIterator first, BidirectionalIterator last, bidirectional_iterator_tag) {
        // 低效的排序算法
    }
    

2.3 迭代器类型属性

  1. 输入迭代器

    输入迭代器只读并且只能读取一次,常见的例子是 istream_iterator。在迭代器传递过程中,上一个迭代器会失效。

    istream_iterator<int> it(cin);
    
  2. 输出迭代器

    输出迭代器只写并且只能写入一次,常见的例子是 ostream_iterator。在迭代器传递过程中,上一个迭代器会失效。

    ostream_iterator<int> it(cout, " ");
    
  3. 前向迭代器

    前向迭代器可以读写并且可以多次读写,可以保存迭代器。例如 forward_listunordered_mapunordered_set 的迭代器。

    forward_list<int> lst = {1, 2, 3};
    auto it = lst.begin();
    
  4. 双向迭代器

    双向迭代器可以读写并且可以多次读写,可以保存迭代器,并且可以向前和向后移动。例如 listmapsetmultimapmultiset 的迭代器。

    list<int> lst = {1, 2, 3};
    auto it = lst.begin();
    ++it;
    --it;
    
  5. 随机访问迭代器

    随机访问迭代器可以读写并且可以多次读写,可以保存迭代器,并且可以进行随机访问。例如 vectordeque 的迭代器。

    vector<int> vec = {1, 2, 3};
    auto it = vec.begin() + 2;
    
  6. 属性图
    在这里插入图片描述

3. 迭代器失效

迭代器失效是指在对容器进行一些操作(如插入、删除元素)后,之前获取的迭代器可能不再有效。失效的迭代器不应再被使用,否则可能导致未定义的行为。

3.1 容器类别

  1. 序列型容器

    这些容器维护一个严格的线性序列,例如 vectordequequeue。对这类容器进行插入或删除操作时,需要注意迭代器的失效情况。

  2. 关联型容器

    这些容器维护的是一个排序的关键字集合,例如 setmapmultisetmultimap。在这些容器中插入或删除元素,可能会导致迭代器失效。

  3. 链表型容器

    链表型容器包括 forward_listlist、以及unordered_* 系列容器。由于这些容器底层采用了链表结构,所以在插入或删除元素时,除了涉及操作的迭代器外,其他迭代器通常都不会失效。

3.2 失效情况

  1. 单独删除或插入

    当单独对某个位置进行插入或删除操作时,会影响到一部分迭代器的有效性。

    • 插入

      插入操作的方法有 insertemplacepush_backpush_front 等。在 vectordeque 中,如果在中间位置插入元素,可能会导致所有迭代器失效;如果在尾部插入元素,可能会导致所有迭代器失效,因为可能引发容器的扩容。在 listforward_list、以及 unordered_* 系列容器中,插入元素不会导致其他迭代器失效。

    • 删除

      删除操作的方法有 erasepop_backpop_frontclear 等。在 vectordeque 中,删除元素会导致从删除位置到尾部的所有迭代器失效。在 listforward_list、以及 unordered_* 系列容器中,删除元素只会让指向被删除元素的迭代器失效。

  2. 遍历删除

    在遍历过程中删除元素需要特别注意,一般需要在删除元素后及时更新迭代器。

    • 序列型容器

      对于 vectordeque 等序列型容器,可以使用以下方式在遍历中删除元素:

      for (auto it = container.begin(); it != container.end(); ) {
          if (shouldDelete(*it
      
      )) {
              it = container.erase(it);
          } else {
              ++it;
          }
      }
      
    • 关联型容器

      对于 setmapmultisetmultimap 等关联型容器,在 C++11 之前,通常采用以下方式在遍历中删除元素:

      for (auto it = container.begin(); it != container.end(); ) {
          if (shouldDelete(*it)) {
              container.erase(it++);
          } else {
              ++it;
          }
      }
      

      在 C++11 之后,可以像处理序列型容器一样,直接使用 erase 方法的返回值更新迭代器:

      for (auto it = container.begin(); it != container.end(); ) {
          if (shouldDelete(*it)) {
              it = container.erase(it);
          } else {
              ++it;
          }
      }
      
    • 链表型容器

      对于 forward_listlistunordered_* 系列容器,由于其底层为链表结构,因此可以采用与关联型容器在 C++11 之后相同的方式在遍历中删除元素。在这些容器中,除了被删除元素的迭代器会失效外,其他迭代器不会失效。

3.3 C++11容器类别

  1. 连续存储容器

    对于如 vectorstringdeque 这类连续存储的容器,插入或删除元素可能会导致所有迭代器失效。在遍历过程中删除元素时,需要更新迭代器:

    it = container.erase(it);
    
  2. 非连续存储容器

    对于如 listsetmapunordered_* 这类非连续存储的容器,在插入或删除元素时,只有指向被插入或删除元素的迭代器会失效。在遍历过程中删除元素时,也需要更新迭代器:

    it = container.erase(it);
    

    或者:

    container.erase(it++);
    

4. STL容器的线程安全

在多线程环境下,如果多个线程同时操作同一个容器,那么就需要考虑线程安全问题。STL中的容器并不是线程安全的,也就是说,它们并没有内部机制来防止并发操作带来的数据竞争或其他问题。

4.1 背景知识

STL容器的内部实现是已经固定的,它们没有加锁机制,也不能在其源码中添加锁。当多个线程并发操作同一容器时,可能会引发数据竞争或者其它未定义的行为。

  1. 容器内部实现原理

    • 扩缩容

      对于vectordeque以及基于deque的容器(如priority_queuequeuestack),当容器空间不足以容纳新的元素时,就需要进行扩容,即重新分配更大的内存空间,并将原来的元素复制到新的内存空间中。在多线程环境下,如果有一个线程在对容器进行扩容操作,而另一个线程试图访问或者修改容器的元素,那么就可能发生错误。

    • rehash

      对于unordered_*系列容器,当容器内元素数量增多时,为了保持良好的查找性能,它们会进行rehash操作,即增加哈希表的桶数量,并将原来的元素重新进行哈希放入新的桶中。这同样可能引起多线程下的问题。

    • 节点关系

      对于vector,当在其中间位置插入或删除元素时,会引起该位置之后的所有元素移动,改变它们与容器的关系。对于基于红黑树的容器(如setmap等),插入或删除元素可能会引起树的rebalance操作,改变节点间的关系。这两种情况在多线程环境下都可能造成问题。

4.2 解决方案

  1. 加锁

    一个直接的解决方案就是加锁,即在对容器进行操作前先获得锁,操作完成后再释放锁。

    • 互斥锁

      互斥锁可以防止两个线程同时对同一个容器进行操作。例如,在C++中可以使用std::mutex

      std::mutex mtx;
      // ...
      {
          std::lock_guard<std::mutex> lock(mtx);
          // 在此区域内对容器进行操作
      }
      
    • 读写锁

      如果一个容器主要用于读取操作,只偶尔进行写入操作,那么可以使用读写

      锁来提高效率。在C++中可以使用std::shared_mutex

      std::shared_mutex smtx;
      // ...
      {
          std::shared_lock<std::shared_mutex> lock(smtx);
          // 在此区域内进行读取操作
      }
      // ...
      {
          std::unique_lock<std::shared_mutex> lock(smtx);
          // 在此区域内进行写入操作
      }
      
  2. 不加锁,避免加锁

    除了使用锁,还可以通过设计避免在多线程中对同一个容器进行操作。一种方法是预先分配足够的节点,并将数据分成多份,每个线程只操作专属的那份数据。这样可以避免线程之间的冲突,但可能会引入新的问题,比如线程操作的不均衡等。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/677017.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Git分支使用方法

目录 前言 一、查看可用分支 二、创建新分支 三、切换到新分支 四、在新分支上进行工作 五、提交更改 六、切换回主分支 前言 分支是指在同一个代码仓库中的不同版本线。它们可以被用来同时开展不同的开发任务、修复bug或实现新功能&#xff0c;而不会影响到主要的代码…

【数据结构】栈和队列的应用

&#x1f387;[数据结构]栈和队列的应用&#x1f387; &#x1f31f; 正式开始学习数据结构啦~此专栏作为学习过程中的记录&#x1f31f; 文章目录 &#x1f387;[数据结构]栈和队列的应用&#x1f387;&#x1f370;一.栈在括号表达式中的应用&#x1f680;1.原理&#x1f680…

Linux1.基础指令(上)

1.Linux系统可创建多个用户。 2.创建用户:adduser 用户名 设置密码:passwd 用户名 (系统会提示再次输入密码&#xff0c;注意密码不回显)。 3.删除用户首先要在root权限下&#xff0c;输入指令:userdel -r 用户名。 4.ls指令 ls -a(显示所有文件&#xff0c;包括隐藏文件) :…

【软件设计师暴击考点】计算机组成原理与体系结构高频考点暴击系列【二】

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a;软件…

【P3】kali 最强渗透工具 - metasploit(安装配置及使用教程详解)

文章目录 一、metasploit 是什么&#xff1f;二、metasploit 攻击 windows 操作系统三、metasploit 攻击永恒之蓝全流程四、metasploit 攻击成功后能做什么4.1、操作步骤4.2、攻击示例 五、msfvenom 绕过杀毒软件技巧5.1、捆绑木马5.2、加壳&#xff1a;压缩壳、加密壳5.2.1、T…

分片和一致性哈希

在设计大规模分布式系统时&#xff0c;你可能会遇到两个概念——分片&#xff08;sharding&#xff09;和一致性哈希&#xff08;consistent hashing&#xff09;。虽然我在网上找到了很多关于这些术语的解释&#xff0c;但它们让我感到有些困惑。我觉得分片和一致性哈希本质上…

Web网页制作-知识点(2)——常用文本标签、列表标签、表格标签、Form表单、块元素与行内元素(内联元素)

目录 常用文本标签 列表标签 有序列表 无序列表 定义列表 表格标签 表格组成与特点 表格标签 表格属性 ​​​合并表格单元格 Form表单 属性说明 表单元素 文本框 密码框 提交按钮 块元素与行内元素&#xff08;内联元素&#xff09; 内联元素和块级元素…

Flink JdbcSink.sink源码解析及常见问题

文章目录 源码入口我们看下flush方法干了什么flush方法至此走完了&#xff0c;但是什么时机写入的数据呐&#xff1f;补充总结&#xff1a; 常见问题1. 为什么会出现JdbcSink.sink方法插入Mysql无数据的情况&#xff1f;2. JdbcSink.sink写Phoenix无数据问题 参考 基于Flink 1.…

设计模式之组合模式笔记

设计模式之组合模式笔记 说明Composite(组合)目录组合模式示例类图菜单组件抽象类菜单类菜单项类测试类 说明 记录下学习设计模式-组合模式的写法。JDK使用版本为1.8版本。 Composite(组合) 意图:将对象组合成树型结构以表示“部分-整体”的层次结构。Composite使得用户对单…

Linux网络-网络层IP协议

目录 IP协议 计算机网络分层 IP协议头格式 IP数据报 - 数据分片 数据报为什么要分片&#xff1f; 数据报分片是什么&#xff1f; 如何做到IP数据报分片&#xff1f; 分片demo示例 并不推荐分片&#xff0c;能不分片则不分片。 网段划分 前置了解 网络号和主机号 为…

如何监测和优化阿里云服务器的性能?有哪些性能分析工具和指标?

如何监测和优化阿里云服务器的性能&#xff1f;有哪些性能分析工具和指标&#xff1f;   阿里云服务器性能监测与优化是云计算服务中一个非常重要的环节。为了确保服务器稳定、高效地运行&#xff0c;我们需要对其性能进行监测&#xff0c;并在监测的基础上进行优化。本文将为…

Packet Tracer - 综合技能练习(配置 VLAN、中继、DHCP 服务器、DHCP 中继代理,并将路由器配置为 DHCP 客户端)

Packet Tracer - 综合技能练习 地址分配表 设备 接口 IP 地址 子网掩码 默认网关 R1 G0/0.10 172.31.10.1 255.255.255.224 不适用 G0/0.20 172.31.20.1 255.255.255.240 不适用 G0/0.30 172.31.30.1 255.255.255.128 不适用 G0/0.40 172.31.40.1 255.255…

MySQL权限控制及日志管理

MySQL权限控制及日志管理 用户权限管理 创建用户 CREATE USER 用户名IP地址 [ IDENTIFIED BY 密码 ]&#xff1b;GRANT SELECT ON *.* TO 用户名’IP地址’ IDENTIFIED BY "密码"&#xff1b;--创建一个用户名为Usr1 密码为 Usr1.mysql的用户 并授权 CREATE USER…

无忧行:突破网络封锁、跨境访问国外的网站和应用程序(安装注册及使用教程详解)

文章目录 步骤一&#xff1a;注册微软账号步骤二&#xff1a;修改账号的国家/地区步骤三&#xff1a;在Edge Dev浏览器中安装无忧行插件步骤四&#xff1a;创建 无忧行 账户步骤五&#xff1a;无忧行使用教程 包括注册微软账号、在Edge Dev浏览器中安装无忧行插件、创建 无忧行…

Python基础篇(六):组织管理代码—模块和包

组织管理代码—模块和包 前言模块(Module)创建模块使用模块 包(Package)创建包使用包 前言 在Python中&#xff0c;模块和包是组织和管理代码的重要概念。模块是一个包含 Python 定义和语句的文件&#xff0c;而包则是一组相关模块的目录。它们是组织和管理代码的强大工具&…

【跟小嘉学 Rust 编程】四、理解 Rust 的所有权概念

系列文章目录 【跟小嘉学 Rust 编程】一、Rust 编程基础 【跟小嘉学 Rust 编程】二、Rust 包管理工具使用 【跟小嘉学 Rust 编程】三、Rust 的基本程序概念 【跟小嘉学 Rust 编程】四、理解 Rust 的所有权概念 文章目录 系列文章目录前言一、所有权(Ownership)1.1.、所有权(Ow…

【unity每日一记】 Camera相机+ Screen屏幕+动画机

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a;uni…

Flutter开发——图片加载与缓存源码解析

在Flutter中有个图片组件&#xff1a;Image,通常会使用它的Image.network(src)、Image.file(src)、Image.asset(src)来加载图片。 下面是Image的普通构造方法&#xff1a; const Image({super.key,required this.image,this.frameBuilder,this.loadingBuilder,this.errorBuilde…

第四章 机器学习

文章目录 第四章 决策树4.1基本流程4.2划分选择4.2.1信息增益4.2.2增益率4.2.3基尼指数 4.3剪枝处理4.3.1预剪枝4.3.2后剪枝 4.4连续与缺失值4.4.1连续值处理4.4.2缺失值处理 4.5多变量决策树 第四章 决策树 4.1基本流程 决策过程&#xff1a; 基本算法&#xff1a; 4.2划…

git——使用ssh连接远程仓库

文章目录 前言一. 获取邮箱和密码1. 本地配置你的名字和邮箱2. 使用命令获取你本地的邮箱和密码 二、生成ssh公钥1.任意一个文件夹路径打开Git Bash Here并输入以下命令连按三次回车2. 根据上面红框部分的地址打开文件夹3. 打开并查看id_rsa.pub 文件 三、在GitHub上连接ssh1. …