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
- 内存分配:由于deque是多个分散的内存块而vector是一个连续的内存块,所以deque可以在两端进行高效的插入删除操作
- 插入效率:deque同样无法应对中间插入的或者删除元素效率低的问题,因为可能会移动多个内存块
- 随机访问:vector由于是连续的内存所以随机访问效率更高
- 内存消耗:在扩容时,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用来管理这些块,每个块是独立的,这也导致了如果插入元素,导致数组扩容的化,迭代器失效的情况。但是虽然迭代器失效了,但需要扩容的也只是增加或者删除的那个块而不是所有块
- 在前端插入:如果第一个块有空间会直接插入,如果没有会产生一个新块然后加入到map中
- 在后端插入:如果最后一个块有空间会直接插入,如果没有会产生一个新块然后加入到map中
- 在前端删除:如果删除后第一个块为空,会释放,然后更新map
- 在后端删除:如果删除后最后一个块为空,会释放,然后更新map
deque内部的构造函数
- 默认构造函数
deque<int> dq
- 填充构造函数
deque<int> dq(10,5)//10个值为5的元素
- 范围构造函数
vector<int> vec{123,23,3243};
deque<int> dq(vec.begin(),vec.end());
- 拷贝构造函数
deque<int> dq1(dq)
- 移动构造函数,涉及到移动语义move
deque<int> dq1(move(dq))
类似深拷贝 - 初始化列表
deque<int> dq{1,2,3,4,5}
- 带有分配器的 -- 待补充
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律