C++ STL - deque

deque

与list类似支持在常数时间内对前后端进行增删操作,同时又可以支持根据索引获得元素
deque的内存大小也是动态调整的,并且在增删操作时会保证迭代器的有效性。
内存局部性:deque内部利用了多个缓冲区,有助于提高内存局部性,从而在某些情况下提供更好的性能

工作原理

成员变量

  • elements: 动态数组存储队列元素
  • capacity: 数组容量
  • size: 数组长度
  • frontIndex和backIndex: 指向队首和队尾的元素,类似begin()和end(),frontIndex指向的数据是已经存在的,而backIndex指向的是当前末尾元素的下一个数据

循环数组

通过模运算来实现数组的循环效果,使得队列可以在数组的任意一端进行插入和删除操作。

动态调整

和vector一样,size > capacity后double

索引计算

通过模运算来正确计算新的frontIndex和backIndex,无论是添加和删除的操作

实现

增加操作

对于在头部添加元素,那么操作的就是frontIndex对应的位置,那么frontIndex应该在哪一个位置呢?

frontIndex = (frontIndex-1+capacity) % capacity;

对于在尾部添加元素,操作的就是backIndex对应的位置,但需要注意的是backIndex指向的是最后一个有效位置的后一个位置,所以应该先对backIndex位置赋值,然后改变backIndex

backIndex = (backIndex+1) % capacity

相关代码

void push_front(const T& value)
{
  if(size == capacity)
  {
    resize();
  }
  frontIndex = (frontIndex - 1 + capacity) % capacity;
  elements[frontIndex] = value;
  size++;
}
void push_back(const T& value)
{
  if(size == capacity)
  {
    resize();
  }
  elements[backIndex] = value;
  backIndex = (backIndex+1)%capacity;
  size++;
}

删除元素

删除就和添加刚好相反

frontIndex = (frontIndex+1)%capacity;
backIndex = (backIndex-1+capacity)%capacity;

相关代码

void pop_front()
{
  if(size == 0)
  {
    throw std::out_of_range("empty");
  }
  frontIndex = (frontIndex+1)%capacity;
  size--;
}
void pop_back()
{
  if(size == 0)
  {
    throw std::out_of_range("empty");
  }
  backIndex = (backIndex-1+capacity)%capacity;
  size--;
}

随机访问元素

传入的下标并不是元素的真实下标,而front+index才是元素的真实索引,由于是循环数组控制所以需要mod capacity

elements[(frontIndex+index)%capacity];

相关代码

T& operator[](int index)
{
  if(index >= size || index < 0)
  {
  	throw std::out_of_range("Index out of range");
  }
  return elements[(frontIndex + index) % capacity];
}

扩容函数

主要是旧deque的元素如何赋值到新deque

void resize()
{
	size_t newcapacity = (capacity==0) ? 1 : 2*capacity;
	T* newelements = new T[newcapacity];
	size_t index = frontIndex;
	for(size_t i = 0;i<size;i++)
	{
		newelements[i] = elements[index];
		index = (index+1)%capacity;
	}
	delete[] elements;
	elements = newelements;
	capacity = newcapacity;
	//重置frontIndex和backIndex
	frontIndex = 0;
	backIndex = size;
}

总结

pop_front和pop_back不会互相影响,即即使pop_back后backindex=0,frontindex仍然不变。

deque与vector

  1. 内存分配:由于deque是多个分散的内存块而vector是一个连续的内存块,所以deque可以在两端进行高效的插入删除操作
  2. 插入效率:deque同样无法应对中间插入的或者删除元素效率低的问题,因为可能会移动多个内存块
  3. 随机访问:vector由于是连续的内存所以随机访问效率更高
  4. 内存消耗:在扩容时,vector消耗较大

deque实现固定大小的递减滑动窗口

//原始数组 nums,窗口大小k
deque<int> dq; // 存储的是下标
for(int i = 0;i<nums.size();i++)
{
	if(!dq.empty() && dq.front() == i-k)
	{
		dq.pop_front();
	}
	//保证dq内部元素的排列 dq.back()
	while(!dq.empty() && nums[i] > nums[dq.back()])
	{
		dq.pop_back();
	}
	dq.push_back(i);
	//从第一个滑动窗口开始打印当前最大值
	if(i >= k-1)
	{
		cout << nums[dq.front()] <<" ";
	}
}

deque在前端或者尾端插入元素,迭代器的变化

迭代器会全部失效,由于插入后内部可能会进行重新分配,比如容量不够的情况,需要增加MAP对应的元素,导致迭代器失效,但是指针或者引用仍然有效
如果在中间位置插入,会全部失效
但是删除元素,如果只是在头尾其他元素不会失效,但是删除中间位置的会失效

deque中的[]索引和at()

[]不会提供边界检测,而at()会抛出异常当越界时

deque的内部工作原理

deque内部维护了一系列定长数组,而有一个map用来管理这些块,每个块是独立的,这也导致了如果插入元素,导致数组扩容的化,迭代器失效的情况。但是虽然迭代器失效了,但需要扩容的也只是增加或者删除的那个块而不是所有块

  1. 在前端插入:如果第一个块有空间会直接插入,如果没有会产生一个新块然后加入到map中
  2. 在后端插入:如果最后一个块有空间会直接插入,如果没有会产生一个新块然后加入到map中
  3. 在前端删除:如果删除后第一个块为空,会释放,然后更新map
  4. 在后端删除:如果删除后最后一个块为空,会释放,然后更新map

deque内部的构造函数

  1. 默认构造函数 deque<int> dq
  2. 填充构造函数 deque<int> dq(10,5)//10个值为5的元素
  3. 范围构造函数
vector<int> vec{123,23,3243};
deque<int> dq(vec.begin(),vec.end());
  1. 拷贝构造函数 deque<int> dq1(dq)
  2. 移动构造函数,涉及到移动语义move deque<int> dq1(move(dq)) 类似深拷贝
  3. 初始化列表 deque<int> dq{1,2,3,4,5}
  4. 带有分配器的 -- 待补充

作者:XTG111

出处:https://www.cnblogs.com/XTG111/p/18147622

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   XTG111  阅读(18)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
more_horiz
keyboard_arrow_up light_mode palette
选择主题
点击右上角即可分享
微信分享提示