首页 > 编程语言 >[C++初阶]deque的讲解

[C++初阶]deque的讲解

时间:2024-07-19 23:24:58浏览次数:14  
标签:deque 初阶 back C++ 插入 vector 数组 buff

1.deque介绍  

         Deque是双端队列的不规则缩写。双端队列是具有动态大小的序列容器,可以在两端扩展或收缩。特定的库可能以不同的方式实现deque,通常是某种形式的动态数组。在任何情况下,它们都允许通过随机访问迭代器直接访问单个元素,并根据需要通过扩展和收缩容器来自动处理存储。因此,它们提供了类似于向量的功能,但在序列的开始,而不仅仅是在序列的末尾,也可以有效地插入和删除元素。但是,与vector不同,deque不能保证将其所有元素存储在连续的存储位置:通过偏移指向另一个元素的指针来访问deque中的元素会导致未定义的行为。vector和deque都提供了非常相似的接口,可以用于类似的目的,但两者在内部的工作方式却完全不同:vector使用单个数组,偶尔需要为增长重新分配,而deque的元素可以分散在不同的存储块中,容器内部保留必要的信息,以便在恒定时间内使用统一的顺序接口(通过迭代器)直接访问其任何元素。因此,deque在内部比vector更复杂,但这使得它们在某些情况下更有效地增长,特别是对于非常长的序列,重新分配变得更加昂贵。对于涉及频繁插入或删除除开始或结束位置以外的元素的操作,deque的性能更差,迭代器和引用的一致性也不如列表和前向列表。

2.deque的接口

如下所示,是deque的接口,我们可以发现,它似乎同时具有list和vector的接口。而且它的迭代器还是随机迭代器。

deque的接口像是vector和list的合体。但是它看似很强,实际上效率不是很高。单论头插头删,尾插尾删效率还是不错的,但是综合性不是很好。

3.deque的基本使用

void test_deque()
{
	deque<int> dq;
	dq.push_back(1);
	dq.push_back(2);
	dq.push_back(3);
	dq.push_back(4);

	for (int i = 0; i < dq.size(); i++)
	{
		cout << dq[i] << " ";
	}
	cout << endl;

4.deque的效率

我们在前面说过,deque的综合效率是不高的。我们可以用下面的代码来看出

void test_op()
{
	srand(time(0));
	const int N = 1000000;
	vector<int> v1;
	vector<int> v2;
	v1.reserve(N);
	v2.reserve(N);

	deque<int> dq1;
	deque<int> dq2;


	for (int i = 0; i < N; ++i)
	{
		auto e = rand();
		//v1.push_back(e);
		//v2.push_back(e);
		dq1.push_back(e);
		dq2.push_back(e);
	}
	// 拷贝到vector排序,排完以后再拷贝回来
	int begin1 = clock();
	// 先拷贝到vector
	for (auto& e : dq1)
	{
		v1.push_back(e);
	}

	// 排序
	sort(v1.begin(), v1.end());

	// 拷贝回去
	size_t i = 0;
	for (auto& e : dq1)
	{
		e = v1[i++];
	}

	int end1 = clock();

	int begin2 = clock();
	sort(dq2.begin(), dq2.end());
	int end2 = clock();


	printf("deque copy vector sort:%d\n", end1 - begin1);
	printf("deque sort:%d\n", end2 - begin2);
}

deque效率慢的原因主要就是因为它的随机访问[]的效率太低

5.deque的原理

1.对于数组,可以下标随机访问,但是存在扩容问题,中间和头部插入效率低下

2.对于链表,任意位置插入删除效率合适,按需申请释放,但是不支持随机访问

而现在,我们使用的deque的结构是这样,它是一段一段的开空间,每段空间都是一样大的,然后通过一个中控数组(指针数组)进行连接起来。想要扩容就在连接一块空间即可。当指针数组满了,就中控数组扩容即可。这样一来扩容的代价就很低。不需要拷贝原来的数组。对于头插尾插也很简单,就用专门的两个空间进行头插尾插即可

它相比vector极大的缓解了扩容、头插头删问题。但是它的[]运算符不够极致。它的[]需要计算在哪个buff数组,在哪个buff数组的第几个。

举个例子:

假设我们这里访问第i个,也就是[i]

  1. 看在不在第一个buff数组里面,如果在,就直接访问
  2. 不在第一个buff数组里面,i=i-第一个buff数组的size

因此我们可以得出以下结论:

第几个buff=i/buff.size()

在这个buff的第几个=i%buff.size()

所以他的访问就是这样的,=并不是很方便。

而且他的insert和earse也 存在一些问题,首先,我们知道它具有vector和list的特点,如果我们这边的insert是参考list的开辟新空间,改变中控数组的指针顺序,这样子插入的效率高吧。但是,如果这么做会发生什么?很明显buff数组的大小不再保持一致了,所以这样子会导致我们上面的几个结论直接没用了,我们只能一个buff,一个buff的减。

如果我们是这样插入我们的[]访问效率降低,但是如果我们不像list那样插入呢?如果我们不用list那样的插入,那么我们只能用vector那样的插入,这样子就导致了我们需要对存储的数据进行移动,这样会使我们的insert效率很低,

不过在库里,buff的大小是固定的,因此我们能够确定insert和earse函数使用的是类似vector的方法

但是头尾插入,我们就可以使用list那样的插入,这个效率很高。

总结:

deque相比list,可以支持随机访问,cpu高速缓存访问效率不错,头插尾插删除不错,但是中间位置插入删除效率低下。因为我们需要扩容或者挪动buff的数据。无论哪一种,效率都很低。

根据deque的底层原理,其实对于高频的头插头删,尾插尾删来说,deque还很适合,所以deque用于适配stack和queue来说是很合适的,因为它们只涉及到头部和尾部的插入删除,不涉及中间位置的插入删除

际上在库里面的deque是更加复杂的,它的迭代器由四个指针组成,这使得deque更加复杂,首先由node指向中控,即指向当前的buff数组,cur指向当前buff数组中的某个数据,first和last指向当前数组的头和尾

标签:deque,初阶,back,C++,插入,vector,数组,buff
From: https://blog.csdn.net/2301_80017277/article/details/140535390

相关文章

  • Windows图形界面(GUI)-DLG-C/C++ - 工具栏(ToolBar)
    公开视频-> 链接点击跳转公开课程博客首页-> ​​​​​​链接点击跳转博客主页目录工具栏(ToolBar)创建工具栏-CreateWindowEx初始工具栏-TB_BUTTONSTRUCTSIZE工具栏图标-TBADDBITMAP-TB_ADDBITMAP工具栏按钮-TB_ADDBUTTONS示例代码工具栏(ToolBar)......
  • Windows图形界面(GUI)-DLG-C/C++ - 滑动条(Trackbar)
    公开视频-> 链接点击跳转公开课程博客首页-> ​​​​​​链接点击跳转博客主页目录滑动条(Trackbar)使用场景初始控件控件消息示例代码滑动条(Trackbar)使用场景音量控制亮度调节视频播放进度控制任何需要用户在特定范围内选择值的场景初始控件TBM_......
  • 【C++进阶学习】第七弹——AVL树——树形结构存储数据的经典模块
    二叉搜索树:【C++进阶学习】第五弹——二叉搜索树——二叉树进阶及set和map的铺垫-CSDN博客目录一、AVL树的概念二、AVL树的原理与实现AVL树的节点AVL树的插入AVL树的旋转AVL树的打印AVL树的检查三、实现AVL树的完整代码四、总结前言:在前面我们学习二叉搜索树的......
  • c++的继承
    目录一、什么是继承二、继承的格式三、子类和父类一、子类对父类的赋值二、子类与父类的同名成员变量 三、子类和父类的同名成员函数四、子类的默认成员函数一、构造函数二、析构函数三、拷贝构造四、赋值运算符重载一、什么是继承定义:继承(inheritance)机制......
  • C++类和对象(二)
    目录默认成员函数一、构造函数二、析构函数三、拷贝构造函数四、赋值运算符重载五、取地址运算符重载六、const成员函数七、日期类实现默认成员函数默认成员函数就是用户没有显式实现,编译器会自动⽣成的成员函数称为默认成员函数。⼀个类,我们不写的情况下编译器会默......
  • 线程池(C++11)
    已经有现成的实现,本博客摘抄讲解附源码链接。参考的博客质量已经非常高,避免找来找去。1、避免频繁创建、销毁线程,实现复用。思路如下:2、线程函数多种多样,如何封装成统一的函数类型void()第一次封装我们使用bind()函数将多个参数的函数封装为没有形参的package_task对象,因为p......
  • 奇妙的 c++ 混合运算式
    先来看看如下的式子:a*b+c当你在c++中运行它时,你很清楚它是先计算*再计算+的。那么请再来看看这个式子:a+b+c请问它是先执行第一个+,还是先执行第二个+呢?这个问题看上去无解,但实际上我们可以解答:#definelllonglonginta=INT_MAX,b=INT_MAX;llc......
  • C++类和对象 后篇
    C++类和对象后篇构造函数的初始化列表......
  • c++一句话求前缀和,不用循环
    partial_sum是C++标准库中的一个函数,用于计算给定范围内元素的部分和。它接受三个参数:起始迭代器(包含在计算范围内的第一个元素)结束迭代器(不包含在计算范围内的最后一个元素)输出迭代器(存储部分和结果的起始位置)在这个例子中,a.begin()+1表示从数组a的第二个元素开始计......
  • C++ 定义静态数据成员简单测试
    #include<iostream>#include<string>namespace{classA{public:voidaddCount(){++sumCount;}staticintgetSumCount(){returnsumCount;}private:......