首页 > 编程语言 >C++双端队列deque源码的深度学习(stack,queue的默认底层容器)

C++双端队列deque源码的深度学习(stack,queue的默认底层容器)

时间:2024-06-16 14:58:39浏览次数:30  
标签:__ deque map 双端 源码 内存 ._ impl size

什么是deque ?

deque是C++标准模板库(STL)中的一个容器,代表“双端队列”(double-ended queue)。deque支持在其前端(front)和后端(back)进行快速插入和删除操作,并且它在序列的中间插入和删除元素时通常比vectorlist更高效。

deque的特点

  1. 双端插入和删除:你可以在deque的头部和尾部快速地添加或移除元素,时间复杂度为O(1)。
  2. 随机访问:与vector类似,你可以通过索引(即下标)直接访问deque中的任何元素,时间复杂度也为O(1)。
  3. 动态数组deque是一个动态数组,它可以根据需要增长或缩小。
  4. 内部实现:虽然deque在外部接口上看起来像一个连续的内存块,但它的内部实现可能不同。它通常是由多个固定大小的内存块组成,这些内存块以某种方式链接在一起,以便在需要时能够动态地分配和释放内存。
  5. 迭代器稳定性:在deque的头部或尾部插入或删除元素时,指向容器其它部分的迭代器仍然有效。但是,如果你在中间插入或删除元素,指向这些元素的迭代器可能会失效。


下面我们依旧从stl的六个方面来学习 

容器和迭代器
template<class T>
class deque              //双端队列             
{

 protected:    
     iterator start;
     iterator finish;
     map_pointer map;         //二级指针,指向内存块地址数组的指针
     size_t map_size;         //内存块的个数吧
};

template<class T,class ref,class pointer,size_t buf_size>     buf_size 是内存块的大小
{
     T* cur;                  //尾部迭代器指向元素的pointer
     T* first;                //当前内存块的起始地址
     T* last;                 //尾部
     map_pointer node;        //当前迭代器所在的内存块
}

从上面代码我们可以看到在deque中有 begin迭代器,end迭代器,一个二级指针map表示一个指向每块内存块起始地址数组的指针,map_pointer表示内存块的个数

需要注意的是deque是许多非连续连续内存块来实现连续,有点拗口,也就是说它是由多块内存块构成的,内存块内部连续,但内存块之间就不一定连续了 ,如下图所示

 内存块和迭代器初始化
 template<typename _Tp, typename _Alloc>
    void
    _Deque_base<_Tp, _Alloc>::
    _M_initialize_map(size_t __num_elements)
    {
      const size_t __num_nodes = (__num_elements/ __deque_buf_size(sizeof(_Tp))
				  + 1);//这里表示的是需要的内存块,你需要1000个int,每个内存块128则需要9块

      this->_M_impl._M_map_size = std::max((size_t) _S_initial_map_size,  
					   size_t(__num_nodes + 2));//默认为8个,+2的原因是给头和尾多加一块方便扩充
      this->_M_impl._M_map = _M_allocate_map(this->_M_impl._M_map_size);

      // For "small" maps (needing less than _M_map_size nodes), allocation
      // starts in the middle elements and grows outwards.  So nstart may be
      // the beginning of _M_map, but for small maps it may be as far in as
      // _M_map+3.
      
      _Map_pointer __nstart = (this->_M_impl._M_map               //内存块首尾的初始化
			       + (this->_M_impl._M_map_size - __num_nodes) / 2);
      _Map_pointer __nfinish = __nstart + __num_nodes;

      __try                         //尝试开辟内存
	{ _M_create_nodes(__nstart, __nfinish); }                 
      __catch(...)
	{
	  _M_deallocate_map(this->_M_impl._M_map, this->_M_impl._M_map_size);
	  this->_M_impl._M_map = _Map_pointer();
	  this->_M_impl._M_map_size = 0;
	  __throw_exception_again;
	}

      this->_M_impl._M_start._M_set_node(__nstart);              //迭代器的初始化 
      this->_M_impl._M_finish._M_set_node(__nfinish - 1);
      this->_M_impl._M_start._M_cur = _M_impl._M_start._M_first;
      this->_M_impl._M_finish._M_cur = (this->_M_impl._M_finish._M_first
					+ __num_elements
					% __deque_buf_size(sizeof(_Tp)));
    }

 这里对头迭代器的初始化很简单

尾迭代器__num_elements % __deque_buf_size(sizeof(_Tp)))是因为1000个元素能装满7个128剩下一个并未装满所以取模

 添加数据

deque支持双端插入即,向尾部插入push_back,向头部插入元素push_front

下面以push_back为例子

 void
      push_back(const value_type& __x)
      {
	if (this->_M_impl._M_finish._M_cur
	    != this->_M_impl._M_finish._M_last - 1)
	  {
	    _Alloc_traits::construct(this->_M_impl,
				     this->_M_impl._M_finish._M_cur, __x);
	    ++this->_M_impl._M_finish._M_cur;
	  }
	else
	  _M_push_back_aux(__x);
      }

这里我们如果如果尾部的当前_M_cur还没到尾内存块的尾部就可以直接插入,否则内存不够_M_push_back_aux(__x)

template<class _Tp,class _Alloc>
void deque<_Tp,Alloc>::M_push_back_aux()
{
	_M_reserve_map_at_back();                     //向尾部添加一个内存块
	*(_M_finish._M_node+1)=_M_allocate_node();    
	__STL_TRY{                                    //更新尾部迭代器的当前指向元素,和尾部内存块
	   construct(_M_finish._M_cur);
	   _M_finish._M_set_node(_M_finish._M_node+1);
	   _M_finish._M_cur=_M_finish._M_first;  
	}
    __STL_UNWIND(_M_deallocate_node(*(_M_finish._M_node+1)));
}

然后push_front也类似

如果一直从尾部插入元素会使得前面的内存块导致失衡未被使用、所以在每次插入数据时还有个检测以方便重新分配map,这里我就不再说了大家感兴趣的可以去看看

删除数据

pop_front和pop_back

 void  pop_back() _GLIBCXX_NOEXCEPT
 {
	__glibcxx_requires_nonempty();                  
	if (this->_M_impl._M_finish._M_cur
	    != this->_M_impl._M_finish._M_first)        //说明尾部还有元素可以删除
	  {
	    --this->_M_impl._M_finish._M_cur;                 
	    _Alloc_traits::destroy(this->_M_impl,     
				   this->_M_impl._M_finish._M_cur);       
	  }
	else                                           //为空
	  _M_pop_back_aux();
 }

 void pop_back() _GLIBCXX_NOEXCEPT
这声明了一个不抛出异常的pop_back函数(通过_GLIBCXX_NOEXCEPT宏,它可能是GCC标准库实现中的一个宏,用于指示该函数不会抛出异常)。

if语句判断的是容器是否为空


迭代器的运算

一般我们计算整个deque的大小时都是通过计算头尾迭代器的差值

我们知道迭代器中包含当前的指针,以及当前内存块的头和尾,以及当前内存块

也就是    (尾迭代器内存块-头迭代器内存块)*buf_size+头内存块尾-头迭代器当前元素+尾迭代器当前元素-尾内存块的头  结合上面的图大家可以弄懂

其中源码对迭代器的+,-,-=,++,--,等重载都是基于+=重载实现的,比如++就调用+=1,-=10相当于+=-10

_self& operator+=(difference_type __n) _GLIBCXX_NOEXCEPT
      {
	const difference_type __offset = __n + (_M_cur - _M_first);
    /*计算加完__n之后的偏移量,因为要判断是否越过当前内存块*/

if (__offset >= 0 && __offset < difference_type(_S_buffer_size()))//偏移量大于等于0,未越界
	  _M_cur += __n;      //单个内存块内部时连续的可以直接运算
	
	else                  //偏移量越界
	  {
	    const difference_type __node_offset =
		 __offset > 0 ? __offset / difference_type(_S_buffer_size())  
		 : -difference_type((-__offset - 1) / _S_buffer_size()) - 1;
        /*计算向先或者向后移动多少个内存块*/
	    
        
        _M_set_node(_M_node + __node_offset);           //设置迭代器中内存块的地址
	    _M_cur = _M_first + (__offset - __node_offset   //更新当前指向元素的指针
				 * difference_type(_S_buffer_size()));
	  }
	return *this;
      }

大家可以自己仔细琢磨下可以自己画图

分配器

分配和管理相应的内存等其他资源

 算法

比如sort,find等算法很简单这里不再展示代码

仿函数

比较仿函数

std::less<T>:对元素进行小于比较。

std::greater<T>:对元素进行大于比较。

std::equal_to<T>:检查两个元素是否相等。

std::not_equal_to<T>:检查两个元素是否不相等。

适配器
stack,queue的默认适配器都是deque

标签:__,deque,map,双端,源码,内存,._,impl,size
From: https://blog.csdn.net/weixin_72492465/article/details/139720351

相关文章

  • Springboot计算机毕业设计远程在线诊疗系统小程序【附源码】开题+论文+mysql+程序+部
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景随着信息技术的飞速发展,远程医疗作为一种新兴的医疗模式,正逐渐受到广泛关注和认可。特别是在疫情等突发公共卫生事件的影响下,远程在线诊疗系统小程序......
  • Springboot计算机毕业设计远景民宿酒店预订小程序【附源码】开题+论文+mysql+程序+部
    本系统(程序+源码)带文档lw万字以上 文末可获取一份本项目的java源码和数据库参考。系统程序文件列表开题报告内容研究背景随着旅游业的快速发展和消费者对旅游体验要求的提升,民宿作为一种独特的住宿方式,越来越受到游客的青睐。然而,传统的民宿预订方式存在着信息不对称、预......
  • Demo | 利用机器学习构建作物模型的Python源码
    作物模型提出很早,但应用有限。看起来复杂,其实解决的是环境与表型间的关联,可参考前期推文:作物生长模型CropGrow。环境组的复杂,关键在于数据的准确性获取。对于数据分析人员来说,如果不care数据准确性,分析其实很简单的,就是经典的机器学习流程。这里提供一段伪代码仅供参考。1.导库......
  • Chromium源码阅读:从页面加载到元素展示(1)
    ​从<p>helloworld</p>.html到界面上的helloworld今天,我们一起来看看一个html元素,是如何绘制到界面上。我们选择了最简单的场景,便于快速掌握总体的流程,加深之前阅读知识的印象。准备环境首先,我们保存这段html:<html><body><p>Helloworld</p></body><......
  • 基于单片机智能停车系统的设计与实现(论文+源码)_kaic
     基于单机片智能停车系统摘要:随着私家车数量的迅速增加,大中城市的停车问题越来越严重,人们早已习惯了将问题交给手机来解决。因此,迫切需要设计一个智能停车管理系统来支持移动终端,使用户能够通过移动终端实现停车查询、预订和支付。与传统的停车管理系统相比,智能停......
  • 高校毕业设计管理系统的设计与实现(论文+源码)_kaic
    目  录第1章绪  论1.1课题研究目的和意义1.2国外发展状况1.3开发环境1.4本文研究的主要内容第2章技术概述2.1设计原理2.2系统设计模式选定2.3数据库介绍2.4Struts介绍2.5系统中所应用的脚本和Ajax技术第3章需求分析3.1项目概述3.1.1......
  • 美发店管理系统(C++ 课程设计)含源码,设计文档
    目录一、成员分工1二、需求分析2三、总体设计3四、详细设计4五、系统测试30六、总结32七、参考文献33一成员分工我们小组成员共有两名,分别是李书文、卢增凌、张晗,为了能按时圆满的完成这次C++课程设计,我们小组进行了详细的分工,以确保设计能按时完成。经过周密的考虑......
  • 自动驾驶 Apollo 源码分析:ProcessMonitor
    自动驾驶 Apollo 源码分析:ProcessMonitor本篇文章分析 Apollo 中监控模块中监控进程状态的相关代码。附赠自动驾驶最全的学习资料和量产经验:链接1. ProcessMonitorProcessMonitor 是一个普通的定时器组件,内部函数也只是常规的 RunOnce 和 UpdateStatus,所以,......
  • ZooKeeper源码解读
    ZooKeeper源码分析1.服务器构成群首(leader),追随者(follower),观察者(observer)本质上都是服务器。在实现服务器主要抽象概念是请求处理器。请求处理器是对处理流水线上不同阶段的抽象,每个服务器实现一个请求处理器的序列。zookeeper服务端有两种模式:单机的独立模式和集群的仲裁模式,......
  • 《并发编程系列01》从底层源码剖析AQS的来龙去脉!(通俗易懂)
    前言本文是作者的第一篇文章,目的就是可以分享自己个人的一些技术上的心得体会以及找寻志同道合的人来共同讨论技术。个人学习难免会有一些理解上的错误,所以写博客也是为了记录和反思自己的学习过程,进一步加深对技术的理解和掌握。希望通过这篇博客,能够帮助到一些和我一样......