当前位置: 首页 > news >正文

c++编程(17)——deque的模拟实现(1)迭代器篇

欢迎来到博主的专栏——c++编程
博主ID:代码小豪

博主模拟STL中的容器时,参考的是SGI版本的STL,如果你对STL的源码感兴趣,请私聊博主。

文章目录

    • deque的底层原理
    • deque的迭代器
      • deque迭代器的操作
      • 迭代器的随机访问操作

deque的底层原理

deque是线性表的一种,可以像vector一样,实现随机访问,也可以向链表一样,进行头部的插入和删除(并不是vector不能进行头部的数据插入与删除,而是vector进行头部和删除的效率太低了,时间复杂度是O(N),而deque是O(1)),这是deque特殊的数据结构导致的。

在这里插入图片描述

deque有着像list那样高效率的头尾插入,也能像vector那样随机访问,这是由于deque的空间并不像list那样无序,又不像vector那样的顺序空间,而是一块块零散的顺序空间来存储数据。由于deque并非完全连续,而是以分段的连续空间组成,这就导致deque可以不像vector那样繁杂的复杂的重新配置空间的操作。

deque是由分段的连续空间组成的,其结构如下:
在这里插入图片描述

由于不像vector那样是完全连续的数据结构,因此deque虽然能随机访问,但是效率要比vector低很多(因为会出现跨区访问的操作,这需要一定的时间开销)如果deque的前端空间或者后端空间满了,就会在deque的前端或后端增加新空间,以保持deque顺序存储的假象。

在这里插入图片描述

如果你对数据结构具有一定理解,那么你一定会对这个数据结构进行质疑:仅凭一个begin和end指针,是怎么做到管理分段的连续数组呢?当然,如果只是两个指针当然做不到。实际上deque的数据结构是这样子的。

deque中存在一个map数组,其元素是指向各个分段空间的指针,这么一来就好理解了。如果map数组拥有所有分段空间的指针,那么管理这些分段数组确实可行的,由于这个map数组有点像中央控制器,因此我们就将map称为中控数组吧。

在这里插入图片描述
中控数组map指向空间称为缓冲区(buffer),每个缓冲区可容纳的元素个数都是一致的,如果我们在deque的模板上定义一个非类型模板参数,那么这一点还是很好做到的。

template<class T,size_t bufsize=25>//bufsize是deque缓冲区的元素个数
class deque	{private:
};

但是单靠两个指针还是不能做到遍历整个deque容器啊,你想想,如果我们让begin指针++,那么begin就会越界访问,除非我们能让begin进行空间上的跳跃。
在这里插入图片描述
但是指针++只会让指针访问下一个元素,它是绝对不会进行跳跃的。那么我们就要设计deque的专属迭代器了。我们先不管deque的迭代器是什么样的,我们假设它的名字叫做deque_iterator。那么deque的定义如下:

	template<class T,class ptr,class ref,size_t bufsize>struct deque_iterator {//deque的迭代器};template<class T,size_t bufsize=25>//bufsize是deque缓冲区的元素个数class deque{typedef T* pointer;typedef pointer* map_poniter;//中控数组的类型private:deque_iterator first;//指向数组第一个元素的迭代器deque_iterator last;//指向数组最后一个元素后一位的迭代器map_poniter map;//中控数组size_t mapsize;//数组的个数};

deque的迭代器

deque实现随机访问的操作,其代价就是迭代器设计被设计的非常复杂。我们先来看看deque的迭代器需要完成哪些操作。
在这里插入图片描述
假设现在迭代器first指向第一个元素,如过访问first的下一个元素,first就会指向当前顺序空间的下一个元素,而如果要访问first的下两个元素,即first+2,那么就要让first实现空间上的跳跃。因此deque的迭代器应该具有以下的结构:

  1. 首先迭代器需要知道当前所在的缓冲区在哪里
  2. 迭代器需要知道当前是否处于当前缓冲区的边缘,这样才能判断访问的元素是否超过了当前的缓冲区
  3. 如果超过了当前的缓冲区,就要跳跃到下一个缓冲区,因此迭代器需要访问到中控中心map来获得下一个缓冲区的位置

而SGI版本的STL是这么设计deque的迭代器的
deque的迭代器拥有四个成员,分别是first,last,node,以及cur,其各个成员的作用如下:

first指向当前的缓冲区的起始地址
last指向当前缓冲区的末尾地址
node指向map中的当前缓冲区,便于跳跃到下一个缓冲区。
cur指向缓冲区的当前元素(即访问的当前元素)

在这里插入图片描述

template<class T,class ptr,class ref,size_t bufsize>
struct deque_iterator {//deque的迭代器
typedef T* pointer;
typedef pointer* map_pointer;
typedef deque_iterator self;//这个self是迭代器的别名//data member
T* first;
T* last;
T* cur;
map_pointer node;
};

根据c++规定,每个容器的迭代器都要放在类中,并且起一个同一的别名,因此我们还要在deque当中typedef这个迭代器

template<class T,size_t bufsize=25>//bufsize是deque缓冲区的元素个数
class deque
{
//省略
public:typedef deque_iterator<T,T*,T&,bufsize> iterator;//为迭代器起一个别名typedef deque_iterator<T,const T*,const T&,bufsize> const_iterator;//为迭代器起一个别名
//省略
};

deque迭代器的操作

deque的迭代器有一个很重要的操作就是让迭代器获得跳跃缓冲区的能力,实际上也就是从map数组中的一个位置来到另一个位置,比如
在这里插入图片描述
如果我们要访问下一个缓冲区,就让node指向map数组中的下一个元素即可
在这里插入图片描述
当然了,迭代器的first和last必须保持指向node缓冲区的起始地址和结束地址。因此完成这个操作的函数我们用setnode()表示,该函数的定义如下:

void setnode(map_poniter newnode)
{node = newnode;first = *node;last = first + bufsize;
}

在这里插入图片描述
deque的迭代器支持两个迭代器进行相减得到之间的元素个数,这也是deque可以实现随机访问的重要原因(list就不支持)。

int operator-(const self& x)
{return int(node - x.node - 1) + cur - first + x.last - x.cur;
}

在这里插入图片描述
迭代器2减去迭代器1,其蓝色部分就是两个迭代器相减的元素个数。两个迭代器之间的元素个数等于,两个迭代器之间的缓冲区(不包括本身的缓冲区)的所有元素,加上迭代器2到起始为止的元素,再加上迭代器1到末尾的元素个数。

self& operator++(){++cur;//访问下一个元素if(cur==last){//如果下一个元素来到了缓冲区的结尾setnode(node + 1);//就来到下一个缓冲区cur = first;//cur指向新缓冲区的第一个元素}return *this;
}self operator++(int) {//后置++self tmp = *this;(*this)++;return tmp;
}

当迭代器访问下一个元素时,可能会出现以下两种情况

  1. 如果访问的下一个元素还在缓冲区内(cur!=last-1),就让cur访问顺序结构的下一个元素
  2. 如果访问的下一个元素不在缓冲区内,就让迭代器跳到下一个缓冲区,且cur指向缓冲区的起始地址

在这里插入图片描述
在这里插入图片描述

self& operator--(){if (cur == first){//判断cur是否来到了当前缓冲区的起始位置setnode(node - 1);//让node跳跃到上一个缓冲区cur = last - 1;//让cur指向末尾位置}else {cur--;}return *this;
}self operator--(int){//后置--self tmp = *this;(*this)--;return tmp;
}

让迭代器访问上一个元素的原理和operator++类似

  1. 如果迭代器指向的元素所处的缓冲区前面还有元素,就让迭代器访问顺序结构的上一个元素
  2. 如果迭代器指向的元素来到了缓冲区的起始地址,就要跳往上一个缓冲区,并且让迭代器的cur指向缓冲区的末尾元素(cur=last-1)
    在这里插入图片描述
    在这里插入图片描述

迭代器的其他操作

bool operator==(const self& x)const
{cur == x.cur;
}bool operator!=(const self& x)const
{cur != x.cur;
}bool operator<(const self& x)const
{return node == x.node ? cur < x.cur : node < x.node;
}

迭代器的随机访问操作

self& operator+=(int n)
{int offset = n + (cur - first);if (offset >= 0 && offset < bufsize)//判断是否在缓冲区内{cur += n;}else//目标不在缓冲区内{int offset_node =offset > 0 ? offset / int(bufsize) : ((offset + 1) /(int) bufsize) - 1;setnode(node + offset_node);//切换到正确的缓冲区cur = first + offset - offset_node * bufsize;//切换到正确的位置}
}

这段代码中最难以理解的就是变量offset了,这个offset简单理解为"偏移量",指的是随机访问的元素与first之间数据。比如,我们假设cur指向第一个元素,让cur+=8;
在这里插入图片描述
首先我们先获得偏移量,offset=4+8=12;由于偏移量大于bufsize(6),因此我们确定cur+=8的结果不在本缓冲区内。然后让offset/bufsize=12/6=2,于是确定cur+=的结果在后两个缓冲区当中,于是setnode让迭代器来到后两个迭代器。最后计算offset-offset_node*bufsize=0。说明要查找的元素在当前缓冲区的后两个缓冲区中相对于first的第0个元素。因此cur+=8的结果为。

![![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/2e068d1bc9314517a7a6a1c1a522dde4.png)

为什么相对于first的第0个元素在这里呢?这是因为C的数组下标总是从0开始计算的。

其他的操作只需要复用operator+=即可。因为我们已经允许+=对负数n进行处理。


self operator+(int n)
{self tmp = *this;return tmp += n;
}self& operator-=(int n)
{return *this += (-n);
}self operator-(int n)
{self tmp = *this;return tmp -= (-n);
}ref operator[](int n)
{return *(*this + n);
}

到此为止,我们的迭代器已经设计好了,由于篇幅原因,博主将deque容器的实现放在下一篇博客当中叙述(因为两篇加起来大约2w字,实在是有点难啃)。

链接如下:
deque_iterator的实现代码

相关文章:

  • vuex是什么?如何使用?使用他的功能场景?
  • [大模型]XVERSE-MoE-A4.2B Transformers 部署调用
  • 大数据同步方案怎么选,才能提高企业的业务效率?
  • 1832javaERP管理系统之车间计划管理Myeclipse开发mysql数据库servlet结构java编程计算机网页项目
  • 【菜狗学前端】uniapp(vue3|微信小程序)实现外卖点餐的左右联动功能
  • Linux C编译器从零开发一
  • Web前端开发主题:深入探索、挑战与创新的四个维度
  • 机器 reboot 后 kubelet 目录凭空消失的灾难恢复
  • 文心智体 - 健身达人 | 一秒创建属于你的 “贾维斯“
  • 算法 | 剪枝函数以及几种形式回溯法和分支限界法的区别算法特性分支限界法的思想分支限界法的基本步骤Prim和Kruscal回溯法的效率
  • DELL服务器插入新磁盘、创建虚拟磁盘、挂载磁盘步骤
  • tcp协议机制的总结(可靠性,提高性能),基于tcp的应用层协议,用udp如何实现可靠传输
  • 系统编程:管道
  • 驱动开发(四):Linux内核中断
  • 【学习笔记】MySQL(Ⅲ)
  • css布局,左右固定中间自适应实现
  • express + mock 让前后台并行开发
  • golang 发送GET和POST示例
  • JavaScript异步流程控制的前世今生
  • MySQL数据库运维之数据恢复
  • python3 使用 asyncio 代替线程
  • Vue2.0 实现互斥
  • 基于 Babel 的 npm 包最小化设置
  • 猫头鹰的深夜翻译:Java 2D Graphics, 简单的仿射变换
  • 前端攻城师
  • 消息队列系列二(IOT中消息队列的应用)
  • scrapy中间件源码分析及常用中间件大全
  • 如何在招聘中考核.NET架构师
  • 整理一些计算机基础知识!
  • ​2021半年盘点,不想你错过的重磅新书
  • ​LeetCode解法汇总2583. 二叉树中的第 K 大层和
  • #14vue3生成表单并跳转到外部地址的方式
  • #基础#使用Jupyter进行Notebook的转换 .ipynb文件导出为.md文件
  • (7)摄像机和云台
  • (C#)Windows Shell 外壳编程系列9 - QueryInfo 扩展提示
  • (delphi11最新学习资料) Object Pascal 学习笔记---第13章第6节 (嵌套的Finally代码块)
  • (pojstep1.1.2)2654(直叙式模拟)
  • (Redis使用系列) Springboot 使用redis的List数据结构实现简单的排队功能场景 九
  • (笔试题)分解质因式
  • (汇总)os模块以及shutil模块对文件的操作
  • (十六)一篇文章学会Java的常用API
  • . ./ bash dash source 这五种执行shell脚本方式 区别
  • . NET自动找可写目录
  • ./configure,make,make install的作用
  • .md即markdown文件的基本常用编写语法
  • .NET 反射的使用
  • .Net 高效开发之不可错过的实用工具
  • .Net 应用中使用dot trace进行性能诊断
  • .net 怎么循环得到数组里的值_关于js数组
  • .pyc文件是什么?
  • @RunWith注解作用
  • @在php中起什么作用?
  • [ C++ ] template 模板进阶 (特化,分离编译)
  • []我的函数库
  • [2018/11/18] Java数据结构(2) 简单排序 冒泡排序 选择排序 插入排序