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

news2024/9/20 22:58:13

文章目录

    • 1. deque底层实现原理
      • 1.1 概述
      • 1.2 原理图
      • 1.3 类结构
      • 1.4 操作函数
    • 2. 什么时候使用vector、list、deque
      • 2.1 vector
      • 2.2 list
      • 2.3 deque
    • 3. priority_queue的底层实现原理
      • 3.1 一句话概括:用堆来实现优先级队列
      • 3.2 堆结构
      • 3.3 底层容器
      • 3.4 STL对堆结构提供的接口
      • 3.5 支持的接口
    • 4. multiset的底层实现原理
      • 4.1 一句话描述
      • 4.2 红黑树
      • 4.3 STL 中红黑树的实现
      • 4.4 multiset 的实现

1. deque底层实现原理

1.1 概述

一句话描述:deque的目的是实现一个双端数组,既可以从队列头部插入和删除元素,也可以从队列尾部插入和删除元素。

底层实现:deque的底层实现是一个双向开口的连续线性空间,可以理解为一个动态数组,其内部维护了多个连续的存储空间,并通过一个索引数组(称为map)来管理这些连续的存储空间。

1.2 原理图

在这里插入图片描述

1.3 类结构

deque的内部类结构主要由以下几个部分构成:

  1. class deque : protected _Deque_base

    这是deque的主体类,它包含了一些基本操作函数如push_back,push_front,pop_back等。deque类继承了_Deque_base类。

    template <class T, class Alloc = alloc>
    class deque {
    public:
        // deque的迭代器
        typedef _Deque_iterator<T, T&, T*>             iterator;
        // 省略其他typedef定义...
    
    protected:
        // deque的数据表示
        typedef pointer*                               map_pointer;
        // 省略其他typedef定义...
        
        // deque的数据成员
        map_pointer _M_map; // 指向map
        size_type _M_map_size; // map大小
        iterator _M_start; // deque开始的位置
        iterator _M_finish; // deque结束的位置
    };
    
  2. _Deque_base._Deque_impl

    _Deque_base._Deque_impl是deque的底层实现类,它包含了deque的内部结构和元数据:

    • _M_map:一个指针数组,数组中每一个元素都是一个指向连续存储空间的指针。

    • _M_map_size:记录_M_map的容量,即map中最多可以容纳的指针数量。

    • _M_start:记录map数组中首个连续空间的信息,它是一个迭代器,指向当前deque的首元素。

    • _M_finish:记录map数组中最后一个连续空间的信息,它是一个迭代器,指向当前deque的末元素。

  3. _Deque_iterator

    _Deque_iterator是deque的迭代器类,它用于遍历deque中的元素:

    • _M_cur:指向当前正在遍历的元素。

    • _M_first:指向当前连续空间的首地址。

    • _M_last:指向当前连续空间的末尾地址。

    • _M_node:用于指向map数组中存储的指向连续空间的指针。

    template <class T, class Ref, class Ptr, size_t BufSiz>
    struct _Deque_iterator {
        // 一些类型定义,省略...
    
        T* _M_cur; // 指向buffer中的当前元素
        T* _M_first; // 指向buffer的头
        T* _M_last; // 指向buffer的尾
        T** _M_node; // 指向map中的控制中心
        // ...
    };
    
  4. _deque_buf_size

    _deque_buf_size函数决定了每个连续空间中能容纳元素的个数,它的设计原则是如果元素的大小较小,则可以存储更多的元素,如果元素的大小较大,则只存储少数几个元素。

    inline size_t _deque_buf_size(size_t __size) {
        return __size < 512 ? size_t(512 / __size) : size_t(1);
    }
    
  5. _M_initialize_map

    _M_initialize_map函数用于创建map,并配置缓冲区。一开始,_M_start和_M_finish指向map的中间的位置,这样可以公平地往上或者向下扩展空间。

    void _M_initialize_map(size_t __num_elements) {
        // 计算需要多少个节点(至少需要一个)
        size_t __num_nodes = __num_elements / _S_buffer_size() + 1;
    
        // 一个map管理最少8个,最多是“所需节点数加2”(前后各预留一个,扩充用)
        _M_map_size = max((size_t) InitialMapSize, __num_nodes + 2);
        _M_map = _M_allocate_map(_M_map_size);
    
        // 让nstart和nfinish指向map所拥有之全部节点的最中央区段
        // 保持在中央,可以使头尾两端的扩充能量一样大。每个节点对应一个缓冲区。
        map_pointer __nstart = _M_map + (_M_map_size - __num_nodes) / 2;
        map_pointer __nfinish = __nstart + __num_nodes;
    
        __STL_TRY {
            // 为这些节点配置缓冲区。这些缓冲区对应deque的内容初始空间
            for (map_pointer __cur = __nstart; __cur < __nfinish; ++__cur)
                *__cur = _M_allocate_node();
        }
        __STL_UNWIND(_M_deallocate_map(_M_map, _M_map_size), _M_map = 0);
        // 更新迭代器start, finish
        _M_start._M_set_node(__nstart);
        _M_start._M_cur = _M_start._M_first;
        _M_finish._M_set_node(__nfinish - 1);
        _M_finish._M_cur = _M_finish._M_first + __num_elements % _S_buffer_size();
    }
    

1.4 操作函数

  1. push_back

    push_back函数用于在deque的尾部添加元素,它首先会检查当前连续空间是否有足够的空间存储新的元素,如果没有,则需要在map

    中分配一个新的连续空间。同时,还需要检查map空间是否足够,如果不够,则需要重新分配一个更大的map。

    void push_back(const value_type& __t) {
        if (_M_finish._M_cur != _M_finish._M_last - 1) {
            // 最后缓冲区还有两个(含)以上的备用空间
            _Construct(_M_finish._M_cur, __t);
            ++_M_finish._M_cur;
        } else
            // 最后缓冲区只剩一个备用空间
            _M_push_back_aux(__t);
    }
    
  2. pop_back

    pop_back函数用于删除deque的尾部元素,如果删除后当前连续空间没有数据了,则需要释放该连续空间,并且更新map和_M_finish。

    void pop_back() {
        if (_M_finish._M_cur != _M_finish._M_first) {
            // 最后缓冲区有多于一个元素
            --_M_finish._M_cur;
            _Destroy(_M_finish._M_cur);
        } else
            // 最后缓冲区仅剩一个元素
            _M_pop_back_aux();
    }
    

2. 什么时候使用vector、list、deque

2.1 vector

  • 使用场景:如果需要高效地快速访问(随机存取),并且不在乎插入和删除的效率,使用vector。vector在内存中是连续的,所以可以利用缓存,访问效率高。而插入和删除需要移动大量元素,所以效率较低。

  • 例子:一个典型的应用场景是,如果你有一个学生列表,并且需要频繁地按照索引查询学生信息,但是很少需要添加或删除学生,那么这种情况下使用vector是最合适的。

#include <vector>

struct Student {
    int id;
    std::string name;
};

std::vector<Student> students;

// 查询学生信息
Student getStudent(int index) {
    return students[index];
}

2.2 list

  • 使用场景:如果需要大量的插入和删除,而且不关心快速访问(随机存取),使用list。list的插入和删除只需要改变几个指针,所以效率很高。但是由于数据不连续,无法直接通过索引访问,所以访问效率低。

  • 例子:一个典型的应用场景是,如果你正在实现一个任务队列,并且需要频繁地添加和删除任务,但是不需要按照索引查询任务,那么这种情况下使用list是最合适的。

#include <list>

struct Task {
    int id;
    std::string description;
};

std::list<Task> tasks;

// 添加任务
void addTask(const Task& task) {
    tasks.push_back(task);
}

// 删除任务
void deleteTask(std::list<Task>::iterator it) {
    tasks.erase(it);
}

2.3 deque

  • 使用场景:如果需要快速访问(随机存取),并且关心两端数据插入和删除,使用deque。deque既可以快速地访问元素,又可以在两端高效地插入和删除元素。

  • 例子:一个典型的应用场景是,如果你正在实现一个双端任务队列,并且需要频繁地从两端添加和删除任务,同时也需要按照索引查询任务,那么这种情况下使用deque是最合适的。

#include <deque>

struct Task {
    int id;
    std::string description;
};

std::deque<Task> tasks;

// 添加任务到队列头部
void addTaskToFront(const Task& task) {
    tasks.push_front(task);
}

// 添加任务到队列尾部
void addTaskToBack(const Task& task) {
    tasks.push_back(task);
}

// 查询任务


Task getTask(int index) {
    return tasks[index];
}

3. priority_queue的底层实现原理

3.1 一句话概括:用堆来实现优先级队列

优先队列的一种常见实现方式是使用堆,这是因为堆具有“大致有序”的特性,这使得堆非常适合用来实现优先队列。堆数据结构可以快速地找出最大值(最大堆)或最小值(最小堆)。

3.2 堆结构

堆是一种特殊的完全二叉树,它具有以下特性:

  1. 完全二叉树

    堆总是一棵完全二叉树,这意味着树的每一层都有左侧子节点,并且只有最后一层可以没有右侧子节点。此外,最后一层的节点必须从左到右填充。这些特性使得堆可以用数组来存储,数组中每个索引表示的节点和其父节点、子节点之间的关系可以用以下公式表示:

    • 给定一个索引 i(0-indexed)
      • 其父节点的索引为 (i - 1) / 2
      • 左子节点的索引为 2 * i + 1
      • 右子节点的索引为 2 * i + 2
    • 代码实现:
    int parent(int i) { return (i - 1) / 2; }
    int left_child(int i) { return 2 * i + 1; }
    int right_child(int i) { return 2 * i + 2; }
    
  2. 最大堆、最小堆

    最大堆和最小堆是两种常见的堆结构。

    • 在最大堆中,每个节点的值都大于或等于其子节点的值。因此,最大的元素存储在根节点(即堆顶)。
    • 在最小堆中,每个节点的值都小于或等于其子节点的值。因此,最小的元素存储在根节点(即堆顶)。

    注意,这里的“大于”和“小于”关系只限于父子节点之间,而与兄弟节点之间无关。

  3. 增加节点和删除节点

    在堆中增加和删除节点的过程需要保持堆的特性。当添加节点时,节点首先被添加到数组的末尾,然后通过一种称为"上浮"的过程调整到正确的位置。当删除节点时(通常是删除根节点),最后一个元素被移动到堆顶,然后通过一种称为"下沉"的过程调整到正确的位置。

    void push_heap(vector<int>& heap, int value) {
        heap.push_back(value);  // 将新元素放到数组的末尾
        int i = heap.size() - 1;
        while (i > 0 && heap[parent(i)] < heap[i]) {
            // 如果新添加的元素大于其父节点,则交换它们
            swap(heap[i], heap[parent(i)]);
            i = parent(i);  // 将索引移动到父节点
        }
    }
    
    void pop_heap(vector<int>& heap) {
        heap[0] = heap.back();  // 将最后一个元素移动到堆顶
        heap.pop_back();  // 删除最后一个元素
    
        // 下沉过程
        int i = 0;
        while (true) {
            int left = left_child(i);
            int right = right_child(i);
            int largest = i;
    
            if (left < heap.size() && heap[left] > heap[largest]) {
                largest = left;
            }
            if (right < heap.size() && heap[right] > heap[largest]) {
                largest = right;
            }
    
            if (i != largest) {
                swap(heap[i], heap[largest]);
                i = largest;
            } else {
                break;
            }
        }
    }
    

3.3 底层容器

priority_queue通常用vector作为底层容器,但也可以使用deque。选择哪种容器主要取决于以下因素:

  • 提供front操作,用于访问队首元素(即堆顶元素)。
  • 提供push_back操作,用于在队尾添加元素。
  • 提供pop_back操作,用于删除队尾元素。

3.4 STL对堆结构提供的接口

STL为堆提供了一些接口,如下:

  • make_heap(first, last, comp):根据比较函数comp,把区间[first, last)的元素构建成一个堆。
  • push_heap(first, last, comp):把区间[first, last)中最后一个元素插入到前面的堆中,形成新的堆。
#include <algorithm>
#include <vector>
#include <functional>

std::vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};

std::make_heap(v.begin(), v.end(), std::greater<>());  // 创建一个小根堆
v.push_back(8);  // 添加一个元素到数组末尾
std::push_heap(v.begin(), v.end(), std::greater<>());  // 将新元素添加到堆中

3.5 支持的接口

priority_queue支持的接口主要有以下几种:

  • empty():检查队列是否为空。
  • size():返回队列中元素的个数。
  • top():返回队首元素(即堆顶元素)。
  • push(elem):向队尾添加元素,然后重新调整堆。
  • pop():删除队首元素(即堆顶元素),然后重新调整堆。
#include <queue>

std::priority_queue<int> pq;  // 默认为最大堆
pq.push(3);
pq.push(1);
pq.push(4);
std::cout << pq.top() << std::endl;  // 输出:4
pq.pop();
std::cout << pq.top() << std::endl;  // 输出:3

4. multiset的底层实现原理

4.1 一句话描述

multiset 是一个基于红黑树实现的集合,它允许存储重复的键值,且元素始终按照排序顺序进行组织。

4.2 红黑树

红黑树是一种自平衡的二叉搜索树,它通过颜色和特定的属性来确保平衡,从而使得在最坏情况下,基本的操作(如插入、删除和查找)的时间复杂度仍为 O ( log ⁡ n ) O(\log n) O(logn)

中序遍历红黑树可以得到一个有序序列,这是由红黑树作为二叉搜索树的性质决定的。它通过对节点的键值进行比较来维持树的有序性。

4.3 STL 中红黑树的实现

在 STL 中,红黑树的实现模板是 _Rb_tree,其主要结构如下:

template<typename _Key, typename _Val, typename _KeyOfValue, typename _Compare, typename _Alloc = allocator<_Val> >
struct _Rb_tree { /* ... */ };

其中:

  • _Key 是键值的类型。
  • _Val 是节点数据的类型,通常是键值和实值的组合。
  • _KeyOfValue 是一个函数或者函数对象,用于从 _Val 类型中提取 _Key 类型的值。
  • _Compare 是一个函数或者函数对象,用于比较键值。
  • _Alloc 是内存分配器。

每个 _Rb_tree 节点是 _Rb_tree_node 结构,其中包含一个 _M_storage 成员,用于存储实际的值。

struct _Rb_tree_node : public _Rb_tree_node_base
{
  typedef _Rb_tree_node* _Link_type;
  _Val _M_storage;
};

原理图:
在这里插入图片描述

红黑树提供了两个重要的接口:

  • _M_insert_unique:插入数据的时候,保证键值唯一。
  • _M_insert_equal:插入数据的时候,允许键值重复。

4.4 multiset 的实现

multiset 是在红黑树 _Rb_tree 的基础上实现的,其定义如下:

template<typename _Key, typename _Compare = less<_Key>, typename _Alloc = allocator<_Key> >
class multiset
{
  typedef _Rb_tree<key_type, value_type, _Identity<value_type>, key_compare, _Key_alloc_type> _Rep_type;
  _Rep_type _M_t;  // Red-black tree representing multiset.
  // ...
};

其中,value_type 存储的就是 keykey 是不允许被修改的,所以 multiset 中的元素是不可修改的(常常称为"immutable"

)。

在插入元素时,multiset 会调用 _M_insert_equal 方法,这使得 multiset 可以存储具有相同键值的元素。

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

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

相关文章

计算机网络-物理层

目录 一、物理层的基本概念 二、物理层下的传输媒体 &#xff08;一&#xff09;引导型传输媒体 &#xff08;二&#xff09;非引导型传输媒体 三、传输方式 &#xff08;一&#xff09;串行传输和并行传输 &#xff08;二&#xff09;同步传输和异步传输 &#xff08;…

STL之位图(bitset)

目录 位图bitset介绍bitset使用 模拟实现位图的应用——题目总结 位图 bitset介绍 参考文档:bitset 在 C STL 中&#xff0c;std::bitset 是一个固定大小的容器类&#xff0c;用于表示二进制位序列。它可以被看作是一个长度固定为 N 的布尔数组&#xff0c;其中每个元素只有两…

[Selenium] 通过Java+Selenium查询文章质量分

文章目录 前言一、环境准备二、查询文章质量分2.1、修改pom.xml配置2.2、配置Chrome驱动2.3、引入浏览器配置2.4、设置无头模式2.5、启动浏览器实例&#xff0c;添加配置信息2.6、访问质量分地址2.7、窗口设置2.8、定位到输入框并输入博文地址2.9、定位到查询按钮并点击2.10、强…

三、Docker的基本组成和常用命令(二)

文章目录 容器命令创建并启动容器列举运行的容器退出容器命令启动容器重启容器停止容器强制停止容器删除容器 常用其他命令后台启动容器查看运行日志查看容器中的进程信息查看容器或镜像的元数据进入当前正在运行的容器从容器内拷贝文件到主机上 容器命令 说明&#xff1a;有镜…

【Linux】常用指令(二)

Yan-英杰的主页 悟已往之不谏 知来者之可追 C程序员&#xff0c;2024届电子信息研究生 目录 man指令 echo指令 补充: echo和cat的区别&#xff1f; CP指令 mv命令 ctrlc 指令 which指令 学习中遇到得问题: 1.如何看待指令&#xff1f; 2.在执行指令之前&#xf…

【MySQL】不就是MySQL——子查询

前言 今天我们来学习多表查询的下一个模块——子查询&#xff0c;子查询包括了标量子查询、列子查询、行子查询、表子查询&#xff0c;话不多说我们开始学习。 目录 前言 目录 一、子查询 1. 子查询的概念 2. 子查询语法格式 2.1 根据子查询结果不同可以分为&#xff1a;…

全志V3S嵌入式驱动开发(基于usb otg的spi-nor镜像烧入)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 说到了用usb otg来实现spi nand flash的烧入&#xff0c;这中间主要用到了PhoenixSuit软件。那么怎么用usb otg来实现spi nor flash的烧入呢&#…

从零开始 Spring Boot 47:缓存

从零开始 Spring Boot 47&#xff1a;缓存 图源&#xff1a;简书 (jianshu.com) Spring 提供一个简单但使用的缓存&#xff08;Cache&#xff09;机制&#xff0c;我们可以利用它来优化代码执行效率。 简单示例 老规矩&#xff0c;我们从一个简单示例开始&#xff1a; Serv…

RedHat红帽认证---RHCSA

&#x1f497;wei_shuo的个人主页 &#x1f4ab;wei_shuo的学习社区 &#x1f310;Hello World &#xff01; RHCSA node1 1.配置网络设置 将 node1 配置为具有以下网络配置&#xff1a;主机名&#xff1a;node1.domain250.example.comIP 地址&#xff1a;172.25.250.100子网…

基于Java+Swing实现聊天室

基于JavaSwing实现聊天室 一、系统介绍二、功能展示三、其它1.其他系统实现 四、获取源码 一、系统介绍 Java聊天室系统主要用于实现在线聊天&#xff0c;基本功能包括&#xff1a;服务端和客户端。本系统结构如下&#xff1a; &#xff08;1&#xff09;服务端&#xff1a; 1…

SpringBoot中使用Tomcat、Undertow、jetty等容器

文章目录 SpringBoot中使用Tomcat、Undertow、jetty等容器&#xff1b;1. 默认使用Tomcat容器&#xff0c;直接运行项目即可&#xff1a;Java -jar xxx.jar2. 使用undertow容器2-1 引入Maven依赖&#xff0c;同时屏蔽内置Tomcat2-2 Undertow容器的常用配置参考2-3 一个特别的报…

人工智能---D分离

D分离&#xff08;D-Separation&#xff09;是一种用来判断变量是否条件独立的图形化方法。相比于非图形化方法&#xff0c;D-Separation更加直观&#xff0c;且计算简单。对于一个DAG&#xff08;有向无环图&#xff09;E&#xff0c;D-Separation方法可以快速的判断出两个节点…

c++11 标准模板(STL)(std::basic_streambuf)(三)

定义于头文件 <streambuf> template< class CharT, class Traits std::char_traits<CharT> > class basic_streambuf; 类 basic_streambuf 控制字符序列的输入与输出。它包含下列内容并提供到它们的访问&#xff1a; 1) 受控制字符序列&#xff…

在Android手机上安装kali Linux 的 QA

本文仅提供在安装时遇到问题的解决方案&#xff0c;查看安装链接请进入教程页面。 教程 https://www.hestudio.net/posts/install-kali-on-android-renew.html 一些命令 这里只说明我的教程涉及到的命令&#xff0c;查看其他博主教程的命令请转到对应博主的文档里查看&…

Linux 学习记录38(C高级篇)

Linux 学习记录38(C高级篇) 本文目录 Linux 学习记录38(C高级篇)一、shell中的分支语句1. case...in语句2. shell中的通配符 二、shell中的循环结构1. while循环2. shell中的printf3. for循环(1. 类似C中的格式(2. shell中的格式(3. 连续列表 4. select ...in 语句5. 辅助控制关…

PyQt学习(二)-----图形的建立(柱状图,折线图,堆叠柱状图,饼图)

数据库列表如图所示&#xff0c;如何对其进行绘图&#xff1a; &#xff08;一&#xff09;柱状图的建立 (1)柱状图初始化 self.__iniBarChart() pyqtSlot()def __iniBarChart(self):chart QChart()chart.setTitle("Barchart 演示")chart.setAnimationOptions(QChar…

md5的特点以及加密原理

MD5的特点及加密原理 简介特点1.长度固定2.结果不可逆3.高度离散性4.抗碰撞性 适用场景1.用户密码保护2.文件传输完整性校验3.数字签名4.云盘秒传 加密原理一.1.对输入的内容进行补位使其长度变成 N * 512 448 &#xff08;即长度对512取余之后 余数为448&#xff09;2.再往后补…

【数据可视化】大作业(意向考研高校的数据可视化)

文章目录 前言一、数据介绍1.1 基本信息1.2 考研信息1.3 导师信息 二、预处理及分析2.1 数据预处理2.1.1 考研信息预处理2.1.2 导师信息预处理 2.2 数据分析 三、可视化方法及结果3.1 可视化方法3.2 可视化结果展示3.2.1 基本信息3.2.2 考研信息3.2.3 导师信息 四、总结五、附录…

pip install安装CPM-Bee出现ModuleNotFoundError: No module named ‘torch‘的解决方案

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…

WPF开发txt阅读器18:用json格式存储配置文件

文章目录 json参数保存 txt阅读器系列&#xff1a; 需求分析和文件读写目录提取类&#x1f48e;列表控件与目录&#x1f48e;快捷键翻页字体控件绑定&#x1f48e;前景/背景颜色书籍管理系统&#x1f48e;用树形图管理书籍语音播放&#x1f48e;播放进度显示&#x1f48e;快进…