C++迈向精通:STL的Deque复现

C++迈向精通:STL的Deque复现

最近忙着写一个其他的小玩意,好久没更新博客了,手痒更新一下:
本期来讲一讲C++中的STL中的deque的底层实现原理。

deque的地位

STL中的deque的地位很高
主要原因是由于泛型思想和对于其他容器的影响,
因为queue以及stack都是基于deque实现的。

实现deque需要思考的几点

这次我没有向上篇博客一样提前查看 deque 的源码,但是基本的实现方式我已经猜的七七八八了。

主要是下面亮点:

  • 数据的存储方式
  • 支持的基本操作

存储方式

双端队列,底层实现应当是一个双向链表,节点中应该有两个指针,指向相同类型的节点。
还有一个指针指向需要存储的数据区域。

支持的基本操作

  • 两头元素的压入与弹出
  • 队列清空
  • 迭代器的遍历访问和随机访问(这个似乎不需要实现,但是当时我给写出来了)

实现过程

程序的设计方法是从具体到抽象的,因此我们可以先来尝试将支持的操作写出来:

int main() {my::deque<int> dq; // 定义了一个双端队列_PrintLine_("input");  // 这个是一个宏,用于打印一个标题,测试时便于观察std::cout << "dq.init..." << std::endl;// 压入1到10的元素for (int i = 0; i < 10; ++i) {dq.push_back(i);}std::cout << "dq.init done." << std::endl;// 测试大小_PrintLine_("size");std::cout << "dq.size() = " << dq.size() << std::endl;// 队列的弹出测试_PrintLine_("pop and front");std::cout << "dq.pop..." << std::endl;for (int i = 0; i < 10; ++i) {std::cout << dq.front() << std::endl;dq.pop_front();}std::cout << "dq.pop done" << std::endl;// 再次压入_PrintLine_("init again");for (int i = 0; i < 10; ++i) {dq.push_back(i);}// 队列的判空测试和清空测试_PrintLine_("clear and empty");std::cout << "dq.clear() ..." << std::endl;dq.clear();std::cout << "dq.clear() done" << std::endl;if (dq.empty()) {std::cout << "dq is empty." << std::endl;} else {std::cout << "dq isn't empty." << std::endl;}std::cout << "init again..." << std::endl;for (int i = 0; i < 10; ++i) {dq.push_back(i);}if (dq.empty()) {std::cout << "dq is empty." << std::endl;} else {std::cout << "dq isn't empty." << std::endl;}_PrintLine_("auto text");// 迭代器测试for (auto x : dq) {std::cout << x << std::endl;}return 0;
}

下面是上面程序的那个标题输出宏的代码:

#define _PrintLine_(str) \for (int i = 0; i < 20; ++i) { \std::cout << '='; \} \std::cout << #str; \for (int i = 0; i < 20; ++i) { \std::cout << '='; \} \std::cout << std::endl;

接下来才是正题:
首先来实现底层的数据结构,双向链表的节点:

节点中有三个元素:

  • 指向上一个节点的指针
  • 指向下一个节点的指针
  • 指向数据区的指针

关于这个指向数据区域的指针,其实也可以写成静态成员,但是这样的话,数据就会被存放在栈区,我一个强迫症还是觉得栈区的空间太小,而且我个人比较喜欢对堆区进行操作(虽然有讨厌的内存泄漏问题),因此将其改成指针,实际上我猜测STL源码中的 deque 的数据应该是存放在栈区中的。

class _deque_node {typedef _deque_node _node;
public:_deque_node() : _data(new T()), _prev(nullptr), _next(nullptr) {}_deque_node(T &data) : _data(new T(data)), _prev(nullptr), _next(nullptr) {}_deque_node(T &&data) : _data(new T()), _prev(nullptr), _next(nullptr) {*_data = data;}~_deque_node() {delete _data;_prev = nullptr;_next = nullptr;}T *_data;_node *_prev;_node *_next;
};

为了方便,我没有将节点中的成员变量设置为私有权限。
另外,这个右值引用的有参构造应该是用不到的,可以删除,只不过我喜欢在写构造函数的时候喜欢写全一点。

具体实现方法不做详细解释,上面的代码相信有基础语法基础的人应该都能看懂。(毕竟我写的那么规范!)

迭代器先放在一边,我们先来写主要的内容,双端队列。

双端队列的成员应该有三个:

  • 指向头节点的指针
  • 指向尾节点的指针
  • 长度
  size_t _size;_node *_head; // 虚拟头节点 _node *_tail;

为什么要设置一个虚拟头节点呢?这是因为如果没有设置虚拟头节点,那么在定义这个容器时,两个指针就都会指向空地址,这样的话在插入之前就需要进行一次特殊判断了,对于大量数据的插入会降低速度。

构造函数:

  deque() : _head(new _node()), _tail(_head), _size(0) {}

接下来就是对应方法的实现:

首先是最简单的 size判空 方法:

  size_t size() const {return _size;}bool empty() const {return _size == 0;}

然后是,两端的压入方法:

  void push_front(T &data) {_node *new_node = new _node(data);new_node->_next = this->_head->_next;this->_head->_next->_prev = new_node;this->_head->_next = new_node;new_node->_prev = this->_head;_size += 1;}void push_back(T &data) {_node *new_node = new _node(data);this->_tail->_next = new_node;new_node->_prev = this->_tail;this->_tail = new_node;_size += 1;}

然后是两端的弹出方法:

  void pop_front() {if (empty()) return ;if (this->_size == 1) {delete this->_tail;this->_tail = this->_head;} else {_node *old_node = this->_head->_next;this->_head->_next = old_node->_next;old_node->_next->_prev = this->_head;delete old_node;}_size -= 1;}void pop_back() {if (empty()) return ;_node *old_node = this->_tail;this->_tail = old_node->_prev;this->_tail->_next = nullptr;delete old_node;_size -= 1;}

不要忘了从头插入的时候跳过第一个虚拟头节点

然后是 clear 方法:

  void clear() {_node *cur = this->_head->_next;_node *old = this->_head;while(!empty()) {pop_back();}

还有一个查看头尾节点的方法:

  _node &front() {return *_head->_next;}_node &back() {return *_tail;}

到了这里,我们需要输出他们返回头尾节点的值,因此我们需要重载一下输出运算符号:
一定是在类外呦:

template <typename T>
std::ostream &operator<<(std::ostream &out, _deque_node<T> &node) {out << *node._data;return out;
}

到这里我们的双端队列实现的就差不多了。此时,我们一开始写的代码应该只剩下最后的一个 for auto 遍历方法还在报错,这个是因为我们没有实现迭代器的一系列的方法,如果你不明白 for auto 的原理的话,需要参考其他的博客或者资料了,如果你感性的话,可以私信我,我会出一篇博客专门来写auto for的。

迭代器的实现:

迭代器的表现形式更像是指针,因此成员可以是一个指针。
我直接将整个代码放在这里,具体细节会在注释中提及


template <typename T>
class _deque_node_iterator {
typedef _deque_node<T> * iterator;
public:// 构造函数_deque_node_iterator() : _ptr(nullptr) {}// 用于类为迭代器的节点赋值,通常是iter = dq.begin()_deque_node_iterator(iterator ptr) : _ptr(ptr) {}// 用于类外迭代器的相互赋值操作_deque_node_iterator(_deque_node_iterator &ptr) : _ptr(ptr) {}// 下面的所有运算符的重载都是用于 auto for// 原理并不难。bool operator==(const _deque_node_iterator &obj) const {return _ptr == obj._ptr;}bool operator!=(const _deque_node_iterator &obj) const {return !this->operator==(obj);}iterator &operator++() {_ptr = _ptr->_next;return _ptr;}iterator operator++(int) {new iterator(_ptr);_ptr = _ptr->_next;return _ptr;}iterator &operator--() {_ptr = _ptr->_prev;return _ptr;}iterator operator--(int) {new iterator(_ptr);_ptr = _ptr->_prev;return _ptr;}T &operator*() {return *_ptr->_data;}
private:iterator _ptr;
};

实现之后,还差最后一步,那就是在我们的 deque 类中添加两个成员方法:

  • begin()
  • end()

众所周知迭代器是左闭右开的,因此我们返回虚拟头节点的下一个节点和尾节点的下一个节点:

  iterator begin() const {return this->_head->_next;}iterator end() const {return this->_tail->_next;}

完整代码

/*************************************************************************> File Name: deque.cpp> Author:Royi> Mail:royi990001@gmail.com> Created Time: Fri 07 Jun 2024 01:25:31 PM HKT> Describe:************************************************************************/#include <iostream>
#include <algorithm>
#include <list>
#include <vector>
#include <queue>
#include <stack>
#include <set>
#include <map>
#include <cstdio>
#include <cstdlib>
#include <ctype.h>
#include <cmath>
#include <string>
#include <sstream>
#include <functional>#define _PrintLine_(str) \for (int i = 0; i < 20; ++i) { \std::cout << '='; \} \std::cout << #str; \for (int i = 0; i < 20; ++i) { \std::cout << '='; \} \std::cout << std::endl;namespace my {template <typename T>
class _deque_node {typedef _deque_node _node;
public:_deque_node() : _data(new T()), _prev(nullptr), _next(nullptr) {}_deque_node(T &data) : _data(new T(data)), _prev(nullptr), _next(nullptr) {}_deque_node(T &&data) : _data(new T()), _prev(nullptr), _next(nullptr) {*_data = data;}~_deque_node() {delete _data;_prev = nullptr;_next = nullptr;}T *_data;_node *_prev;_node *_next;
};template <typename T>
std::ostream &operator<<(std::ostream &out, _deque_node<T> &node) {out << *node._data;return out;
}template <typename T>
class _deque_node_iterator {
typedef _deque_node<T> * iterator;
public:_deque_node_iterator() : _ptr(nullptr) {}_deque_node_iterator(iterator ptr) : _ptr(ptr) {}_deque_node_iterator(_deque_node_iterator &ptr) : _ptr(ptr) {}bool operator==(const _deque_node_iterator &obj) const {return _ptr == obj._ptr;}bool operator!=(const _deque_node_iterator &obj) const {return !this->operator==(obj);}iterator &operator++() {_ptr = _ptr->_next;return _ptr;}iterator operator++(int) {new iterator(_ptr);_ptr = _ptr->_next;return _ptr;}iterator &operator--() {_ptr = _ptr->_prev;return _ptr;}iterator operator--(int) {new iterator(_ptr);_ptr = _ptr->_prev;return _ptr;}T &operator*() {return *_ptr->_data;}private:iterator _ptr;
};template <typename T>
class deque {
typedef _deque_node<T> _node;
typedef _deque_node_iterator<T> iterator;
public:deque() : _head(new _node()), _tail(_head), _size(0) {}size_t size() const {return _size;}bool empty() const {return _size == 0;}_node &front() {return *_head->_next;}_node &back() {return *_tail;}void push_front(T &data) {_node *new_node = new _node(data);new_node->_next = this->_head->_next;this->_head->_next->_prev = new_node;this->_head->_next = new_node;new_node->_prev = this->_head;_size += 1;}void push_back(T &data) {_node *new_node = new _node(data);this->_tail->_next = new_node;new_node->_prev = this->_tail;this->_tail = new_node;_size += 1;}void pop_front() {if (empty()) return ;if (this->_size == 1) {delete this->_tail;this->_tail = this->_head;} else {_node *old_node = this->_head->_next;this->_head->_next = old_node->_next;old_node->_next->_prev = this->_head;delete old_node;}_size -= 1;}void pop_back() {if (empty()) return ;_node *old_node = this->_tail;this->_tail = old_node->_prev;this->_tail->_next = nullptr;delete old_node;_size -= 1;}iterator begin() const {return this->_head->_next;}iterator end() const {return this->_tail->_next;}void clear() {_node *cur = this->_head->_next;_node *old = this->_head;while(!empty()) {pop_back();}}private:size_t _size;_node *_head; // 虚拟头节点 _node *_tail;
};}int main() {my::deque<int> dq;_PrintLine_("input");std::cout << "dq.init..." << std::endl;for (int i = 0; i < 10; ++i) {dq.push_back(i);}std::cout << "dq.init done." << std::endl;_PrintLine_("size");std::cout << "dq.size() = " << dq.size() << std::endl;_PrintLine_("pop and front");std::cout << "dq.pop..." << std::endl;for (int i = 0; i < 10; ++i) {std::cout << dq.front() << std::endl;dq.pop_front();}std::cout << "dq.pop done" << std::endl;_PrintLine_("init again");for (int i = 0; i < 10; ++i) {dq.push_back(i);}_PrintLine_("clear and empty");std::cout << "dq.clear() ..." << std::endl;dq.clear();std::cout << "dq.clear() done" << std::endl;if (dq.empty()) {std::cout << "dq is empty." << std::endl;} else {std::cout << "dq isn't empty." << std::endl;}std::cout << "init again..." << std::endl;for (int i = 0; i < 10; ++i) {dq.push_back(i);}if (dq.empty()) {std::cout << "dq is empty." << std::endl;} else {std::cout << "dq isn't empty." << std::endl;}_PrintLine_("auto text");for (auto x : dq) {std::cout << x << std::endl;}return 0;
}

运行结果:

❯ ./deque
===================="input"====================
dq.init...
dq.init done.
===================="size"====================
dq.size() = 10
===================="pop and front"====================
dq.pop...
0
1
2
3
4
5
6
7
8
9
dq.pop done
===================="init again"====================
===================="clear and empty"====================
dq.clear() ...
dq.clear() done
dq is empty.
init again...
dq isn't empty.
===================="auto text"====================
0
1
2
3
4
5
6
7
8
9

:wq 拜拜~~~~

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

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

相关文章

2024年6月9日 (周日) 叶子游戏新闻

万能嗅探: 实测 网页打开 某视频号、某音、某红薯、某站&#xff0c;可以做到无水印的视频和封面下载功能哦&#xff0c;具体玩法大家自行发挥吧。 《Funko Fusion》发布新预告 20款影视作品齐聚一堂第三人称动作游戏新作《Funko Fusion》今日发布最新实机演示。该游戏融合了整…

Linxu: Dynamic debug 简介

文章目录 1. 前言2. 什么是 Dynamic debug (dyndbg) ?3. Dynamic debug (dyndbg) 的使用3.1 开启 Dynamic debug (dyndbg) 功能3.2 使用 Dynamic debug (dyndbg) 功能 4. Dynamic debug (dyndbg) 的实现4.1 内核接口 dynamic_pr_debug() 的实现4.2 debugfs 导出控制节点 contr…

力扣hot100学习记录(十二)

94. 二叉树的中序遍历 给定一个二叉树的根节点 root&#xff0c;返回它的中序遍历。 题意 给定一个二叉树&#xff0c;返回它的中序遍历 思路 采用递归的思想&#xff0c;只要根节点不为空&#xff0c;则一直递归遍历左子树&#xff0c;然后将根节点的值存入结果&#xff0c;…

AutoCAD Mechanical机械版专业的计算机辅助设计软件安装包下载安装!

AutoCAD机械版作为一款专业的计算机辅助设计软件&#xff0c;不仅具备卓越的二维绘图功能&#xff0c;更是拥有令人瞩目的3D建模工具&#xff0c;为机械设计师们提供了前所未有的创作空间。 在AutoCAD机械版的3D建模环境中&#xff0c;用户可以借助一系列简洁明了的命令&#…

数智融通 创新发展|亚信科技携AntDB、Data OS与隐私计算产品,赋能企业高质量发展

5月21日&#xff0c;亚信科技在云端举办了一场别开生面的研讨会——“数智融通 创新发展”&#xff0c;聚焦企业数智化升级的前沿话题。资深产品经理和技术架构师们面对面深入交流&#xff0c;分享创新成果与实战案例&#xff0c;共同探索企业数智化转型的新路径。 图1&#xf…

网络安全形势与WAF技术分享

我一个朋友的网站&#xff0c;5月份时候被攻击了&#xff0c;然后他找我帮忙看看&#xff0c;我看他的网站、网上查资料&#xff0c;不看不知道&#xff0c;一看吓一跳&#xff0c;最近几年这网络安全形势真是不容乐观&#xff0c;在网上查了一下资料&#xff0c;1、中国信息通…

基础数据结构 -- 栈

1. 简介 堆栈又名栈&#xff08;stack&#xff09;&#xff0c;他是计算机科学中最基础的数据结构之一。可以算是一种受限制的线性结构&#xff0c;&#xff0c;具有后进先出&#xff08;LIFO&#xff0c; Last In First Out&#xff09;的特性。由于此特性&#xff0c;堆栈常用…

在 Zustand 中管理状态能使用类(Class)吗

在 Zustand 中&#xff0c;通常不推荐使用类&#xff08;Class&#xff09;来管理状态&#xff0c;因为 Zustand 的设计理念是基于函数式编程和 React Hooks 的。然而&#xff0c;仍然可以在 Zustand 中间接地使用类&#xff0c;但这并不是 Zustand 的典型用法。 如果确实想要…

(第30天)二叉树阶段总结

目录 1.判断二叉树是否对称或相同统一思路遍历顺序递归和迭代 2.求二叉树的最大深度题目解读递归法逻辑迭代法逻辑 3.求二叉树的最小深度题目解读递归法逻辑遍历法逻辑 4.求二叉树的节点数递归法迭代法 5.判断二叉树是否是平衡二叉树题目解读递归法迭代法 6.求二叉树的所有路径…

OPenCV的重要结构体Mat

一 Mat Mat是什么&#xff1f; Mat有什么好处&#xff1f; class CV_EXPORTS Mat{ public: ... int dims;//维数 int rows,cols;//行列数 uchar *data;//存储数据的指针 int *refcount;//引用计数 ...};二 Mat属性 三 Mat拷贝 1 Mat浅拷贝 Mat A Aimread(file,IMREAD_COLOR) …

【TensorFlow深度学习】状态值函数Vπ与最优策略π∗的求解方法

状态值函数Vπ与最优策略π∗的求解方法 状态值函数Vπ与最优策略π*的求解方法&#xff1a;强化学习中的寻宝图鉴理论基础求解方法代码示例&#xff1a;Value Iteration代码示例&#xff1a;Policy Iteration结语 状态值函数Vπ与最优策略π*的求解方法&#xff1a;强化学习中…

http接口上传文件响应413:413 Request Entity Too Large

目录 一、场景简介二、异常展示三、原因四、解决 一、场景简介 1、服务端有经过nginx代理 2、上传文件超过5M时&#xff0c;响应码为413 3、上传文件小于5M时&#xff0c;上传正常 二、异常展示 三、原因 nginx限制了上传数据的大小 四、解决 扩大nginx上传数据的大小 步…

Linux之文件打包,压缩,解压

打包和压缩 Linux中对文件进行打包&#xff0c;压缩有两种命令 zip&#xff1a;将文件进行压缩 tar&#xff1a;将文件进行打包(通过和其他命令结合&#xff0c;也能实现压缩的功能) 1、tar打包命令 在Linux中&#xff0c;tar命令是一个常用的工具&#xff0c;用于打包和解…

Web前端的工作内容:深度解析与探索

Web前端的工作内容&#xff1a;深度解析与探索 Web前端&#xff0c;作为互联网世界中用户与网站之间的桥梁&#xff0c;承载着丰富的交互体验和视觉呈现。其工作内容涉及多个层面&#xff0c;从基础的页面构建到复杂的交互设计&#xff0c;都需要前端开发者精心打磨。下面&…

【详细的Kylin使用心得,什么是Kylin?】

&#x1f308;个人主页: 程序员不想敲代码啊 &#x1f3c6;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f44d;点赞⭐评论⭐收藏 &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff0c;让我们共…

Qwen2-MOE-57B-A14B模型结构解读

Qwen2-MOE-57B-A14B模型结构解读 模型代码文件下载 该模型总的参数为57B&#xff0c;激活参数为14B&#xff0c;推理速度比32B的快&#xff0c;而且性能更好。 Qwen2-MOE-57B-A14B模型总体结构 <class transformers.models.qwen2_moe.modeling_qwen2_moe.Qwen2MoeForCaus…

秋招突击——算法打卡——6/5——提高{(状态机模型)股票买卖、(单调队列优化DP)最大子序列和}——新做:{考试的最大困扰度}

文章目录 提高(状态机模型)股票买卖IV思路分析实现代码参考代码 新作考试的最大困扰度个人实现参考思路 总结 提高 (状态机模型)股票买卖IV 上一次的思路总结&#xff0c;上次写的时候忘记总结了&#xff0c;现在重新画一下图 思路分析 这道题是一个经典的状态机模型&#…

用动态IP采集数据总是掉线是为什么?该怎么解决?

动态IP可以说是做爬虫、采集数据、搜集热门商品信息中必备的代理工具&#xff0c;但在爬虫的使用中&#xff0c;总是会遇到动态IP掉线的情况&#xff0c;从而影响使用效率&#xff0c;本文将探讨动态IP代理掉线的几种常见原因&#xff0c;并提供解决方法&#xff0c;以帮助大家…

牛客网刷题 | BC119 最高分与最低分之差

目前主要分为三个专栏&#xff0c;后续还会添加&#xff1a; 专栏如下&#xff1a; C语言刷题解析 C语言系列文章 我的成长经历 感谢阅读&#xff01; 初来乍到&#xff0c;如有错误请指出&#xff0c;感谢&#xff01; 描述 输入n个成绩&#…

CleanMyMac2025破解版crack+keygen

【CleanMyMac】这款神奇的软件&#xff0c;让我彻底告别了电脑卡顿的困扰&#xff01;&#x1f62e;‍&#x1f4a8; CleanMyMac绿色免费版下载如下&#xff1a;记得保存哈&#xff0c;以防失效&#xff1a; https://pan.quark.cn/s/9b08114cf404 CleanMyMac X2024全新版下载…