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

文章目录

    • 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 可以存储具有相同键值的元素。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Java-API简析_java.lang.SecurityManager类(基于 Latest JDK)(浅析源码)

【版权声明】未经博主同意&#xff0c;谢绝转载&#xff01;&#xff08;请尊重原创&#xff0c;博主保留追究权&#xff09; https://blog.csdn.net/m0_69908381/article/details/131346082 出自【进步*于辰的博客】 其实我的【Java-API】专栏内的博文对大家来说意义是不大的。…

Python入门(二十七)测试(二)

测试&#xff08;二&#xff09; 1.测试类2.各种断言方法3.一个要测试的类4.测试AnonymousSurvey类5.方法setUp() 1.测试类 前面我们编写了针对单个函数的测试&#xff0c;下面来编写针对类的测试。很多程序中都会用到类&#xff0c;因此证明我们的类能够正确工作大有裨益。如…

学了那么长时间的编程,C语言的各种操作符都搞不懂?点开这里有详细的介绍—>

目录 前言 一、原码、反码、补码的基础概念 1.原码 2.反码 3.补码 二、原码、反码、补码的计算方法 1.原码 2.反码 3.补码 三、算术操作符 四、移位操作符 1. 左移操作符 移位规则&#xff1a; 2. 右移操作符 移位规则&#xff1a; &#xff08;1&#xff09; …

电脑怎么录屏?推荐2款录制电脑屏幕的软件!

案例&#xff1a;我经常需要把电脑上的内容分享给别人&#xff0c;一般通过手机拍摄的方式。这就导致视频十分模糊&#xff0c;给人的观感不太好&#xff0c;有没有什么方法可以实现在电脑上直接录屏&#xff1f; 【我想录制我的电脑屏幕上的内容分享给别人&#xff0c;但是我…

几个SQL的高级写法

一、ORDER BY FLELD() 自定义排序逻辑 MySql 中的排序 ORDER BY 除了可以用 ASC 和 DESC&#xff0c;还可以通过 ORDER BY FIELD(str,str1,...) 自定义字符串/数字来实现排序。这里用 order_diy 表举例&#xff0c;结构以及表数据展示&#xff1a; ORDER BY FIELD(str,str1,..…

【Neo4j教程之CQL函数基本使用】

&#x1f680; Neo4j &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;C…

3d重建+神经渲染

3d重建 基于深度相机(结构光、TOF、双目摄像头)的三维重建基于图像的三维重建&#xff1a;深度学习基于视觉几何的传统三维重建&#xff1a;这种三维重建方法研究时间比较久远&#xff0c;技术相对成熟。主要通过多视角图像对采集数据的相机位置进行估计&#xff0c;再通过图像…

一种对不同类型齐格勒-尼科尔斯 P-I-D 控制器调谐算法研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

常用工具类之AJ-Captcha入门

1.引入MAVEN依赖 若依官方引入的是1.2.7版本。我选择了目前最常用的1.3.0版本。 在项目中给的 ruoyi-framework\pom.xml 添加依赖 <!-- anji滑块验证码 --><dependency><groupId>com.anji-plus</groupId><artifactId>spring-boot-starter-captc…

通过调整图像hue值并结合ImageEnhance库以实现色调增强

前言 PIL库中的ImageEnhance类可用于图像增强&#xff0c;可以调节图像的亮度、对比度、色度和锐度。 通过RGB到HSV的变换加调整可以对图像的色调进行调整。 两种方法结合可以达到更大程度的图像色调增强。 调整hue值 __author__ TracelessLe __website__ https://blog…

vue2中引入天地图及相关配置

前言 项目中需要引入特殊用途的地图&#xff0c;发现天地图比高德地图、百度地图要更符合需求&#xff0c;于是看了看天地图。 正文 vue2项目中如何引入天地图并对相关的配置进行修改使用呢&#xff1f;官方给的4.0版本的使用说明。 引入&#xff1a; 进入到public/index.html中…

Cocos Creator3D:制作可任意拉伸的 UI 图像

推荐&#xff1a;将 NSDT场景编辑器 加入你的3D工具链 3D工具集&#xff1a; NSDT简石数字孪生 制作可任意拉伸的 UI 图像 UI 系统核心的设计原则是能够自动适应各种不同的设备屏幕尺寸&#xff0c;因此我们在制作 UI 时需要正确设置每个控件元素的尺寸&#xff08;size&#…

TypeScript ~ TS 掌握自动编译命令 ③

作者 : SYFStrive 博客首页 : HomePage &#x1f4dc;&#xff1a; TypeScript ~ TS &#x1f4cc;&#xff1a;个人社区&#xff08;欢迎大佬们加入&#xff09; &#x1f449;&#xff1a;社区链接&#x1f517; &#x1f4cc;&#xff1a;觉得文章不错可以点点关注 &…

【CV 向】OpenCV 图形绘制指南

文章目录 引言1. 创建画布2. 绘制线段3. 绘制矩形4. 绘制圆5. 绘制椭圆6. 绘制多边形7. 绘制字体结论 引言 Python OpenCV 是一个功能强大的计算机视觉库&#xff0c;除了图像处理和计算机视觉任务外&#xff0c;它还提供了丰富的功能来绘制各种图形。无论是在计算机视觉应用中…

Springboot程序开启远程DEBUG

一、远程debug的原理 Spring Boot程序远程debug的原理主要是通过在启动时指定JVM参数来启用远程调试模式&#xff0c;并在调试器中连接到程序所在的调试地址&#xff0c;从而实现对程序的远程调试。 具体步骤如下&#xff1a; 在运行Spring Boot程序时&#xff0c;在启动命令…

软考高级系统架构设计师(四) 计算机网络2磁盘阵列

目录 磁盘阵列RAID RAID级别 ​IPV6 网络接入技术 综合布线 磁盘阵列RAID 磁盘阵列&#xff08;Redundant Arrays of Independent Disks&#xff0c;RAID&#xff09;&#xff0c;有"数块独立磁盘构成具有冗余能力的阵列”之意。 磁盘阵列是由很多块独立的磁盘&#…

P35[10-5]硬件IIC配置+读写MPU6050(软)(此处注意与软件iic区别)

接线图如下: 注:硬件读写iic的连接位置固定,可参考引脚定义表(如下) 声明:I2C1重映射时,有一次更换机会,但是此面包板由于OLED的该引脚无法接线,因此只能接在PB10 PB11的I2C2上 软件iic初始化部分:(此处即可替代掉整个软件iic.c初始化的底层) void MPU6050_Init(vo…

MyCat总结

目录 什么是mycat 核心概念 逻辑库 逻辑表 分片节点 数据库主机 用户 mycat原理 目录结构 配置文件 读写分离 搭建读写分离 搭建主从复制&#xff1a; 搭建读写分离&#xff1a; 分片技术 垂直拆分 实现分库&#xff1a; 水平拆分 实现分库&#xff1a; ER表 全局表 分…

IDEA新UI速览,成了VS Code的样子?

IntelliJ IDEA 2023.1 现已发布。此版本包括对新 UI 的改进&#xff0c;根据从用户那里收到的反馈进行了彻底改造。此外还实现了性能增强&#xff0c;从而在打开项目时更快地导入 Maven 和更早地使用 IDE 功能。由于采用了 background commit checks&#xff0c;新版本提供了简…

【学习学习】NLP理解层次模型

NLP&#xff08;Neuro-Linguistic Programming&#xff0c;神经语言程序学&#xff09;&#xff0c;由两位美国人理查得.班德勒&#xff08;Richard Bandler&#xff09;与约翰.葛瑞德&#xff08;John Grinder&#xff09;于1976年创办&#xff0c;并在企业培训中广泛使用。美…