C++ 双端队列 deque
双端队列简介
双端队列是一种具有队列和栈的性质的数据结构。双端队列中的元素可以从队列的两端推入,两端弹出。通过合理使用双端队列,可以做到单独使用栈或队列的功能,也可以完成更加复杂的任务。
事实上,C++ 中的队列 queue 和 栈 stack 的底层实现就是 std::deque。
C++ deque 简介
std::deque 是有索引的序列容器,允许在它的首尾两端快速插入及删除。与 std::vector 不同,deque 的元素不是连续存储的:典型实现采用一系列单独分配的固定尺寸数组,外加额外的簿记数据。
此外,deque 的存储按需自动扩张及收缩。扩张 deque 比扩张 std::vector 更轻便,因为它不涉及将既存元素复制到新内存位置。另一方面,deque 是典型的拥有较大的最小内存开销的结构(可能是存储对象尺寸的 8 倍或 16 倍,取决于具体实现),因为就算只保存一个元素, deque 也必须分配它的整个内部数组。
deque 上常见操作的复杂度(效率)如下:
随机访问——常数 O(1)。
在结尾或起始插入或移除元素——常数 O(1)。
插入或移除元素——线性 O(n)。
使用 deque
导入 deque
deque 在 < deque > 中定义为:
// C++ 17 之前的定义
template<
class T,
class Allocator = std::allocator<T>
> class deque;
// C++ 17 之后的定义
namespace pmr {
template< class T >
using deque = std::deque<T, std::pmr::polymorphic_allocator<T>>;
}
使用时要加入头文件 < deque >,同时,作为标准库,使用时还要使用名称空间 std。
// 头文件
#include <deque>
// 使用名称空间 std
using namespace std;
// 或者
using std::deque;
创建 deque 容器
deque 容器的创建依赖其的构造函数,下面用 int 类型举例,介绍几个常用的构造函数:
- 默认构造函数。构造拥有默认构造的分配器的空容器。
// 函数声明原型为:deque();
deque<int> deq;
- 构造拥有 count 个值 value 的元素的容器。
// 函数声明原型为:deque( size_type count, const T& value = T(), const Allocator& alloc = Allocator() );
deque<int> deq(8653, 1);
// 如果不填入第二个参数,值 value 将使用默认值
- 复制构造函数。构造拥有 other 内容副本的容器。
// 函数声明原型为:deque( const deque& other );
deque<int> deq_1(100, 1);
deque<int> deq_2(deq_1);
元素访问
- 访问指定的元素(不进行边界检查)
deque<int> deq(8);
// 函数声明原型为:reference operator[]( size_type pos );
// 返回位于指定位置 pos 的元素的引用。不进行边界检查。
deq[3];
- 访问第一个元素
deque<int> deq(78);
// 函数声明原型为:reference front();
// 返回到容器首元素的引用。
// 注意:在空容器上调用 front 将导致出现未定义行为(未知风险)!
deq.front();
- 访问最后一个元素
deque<int> deq(78);
// 函数声明原型为:reference back();
// 返回到容器中最后一个元素的引用。
// 注意:在空容器上调用 back 将导致出现未定义行为(未知风险)!
deq.back();
遍历容器
遍历 deque 容器有三种方式:下标遍历,迭代器遍历,范围 for 循环遍历。
下标遍历
由于双端队列下标与双端队列中的元素一一对应,所以,通过控制下标的值,即可完成对 deque 的遍历。
下面是使用示例:
deque<int> deq(78);
// 正向遍历
for (size_t i = 0; i < deq.size(); ++i)
{
// 内部循环体代码(省略)
}
// 反向遍历
for (long long i = deq.size() - 1; i >= 0; --i)
{
// 内部循环体代码(省略)
}
迭代器遍历
C++ deque 类为我们提供了迭代器,我们可以借助它方便快捷的完成对deque 容器的遍历。
下面是几个常用的迭代器和使用示例:
deque<int> deq(78);
// 函数声明原型为:const_iterator begin() const;
// 返回指向 deque 首元素的迭代器。
// 如果 deque 为空,那么返回的迭代器等于 end()。
deq.begin();
// 函数声明原型为:const_iterator end() const;
// 返回指向 deque 末元素后一元素的迭代器。
// 注意:此元素为占位符;不要试图访问它!
deq.end();
// 函数声明原型为:const_reverse_iterator rbegin() const;
// 返回指向逆向的 deque 的首元素的逆向迭代器。它对应非逆向 deque 的末元素。
// 如果 deque 为空,那么返回的迭代器等于 rend()。
deq.rbigin()
// 函数声明原型为:const_reverse_iterator rend() const;
// 返回指向逆向的 deque 末元素后一元素的逆向迭代器。它对应非逆向 deque 首元素的前一元素。
// 注意:此元素表现为占位符,不要试图访问它!
deq.rend();
// 正向遍历
for (auto it = deq.begin(); it != deq.end(); ++it)
{
// 内部循环体代码(省略)
}
// 反向遍历
for (auto it = deq.rbigin(); it != deq.rend(); ++it)
{
// 内部循环体代码(省略)
}
范围 for 循环遍历
范围 for 循环可以方便快捷的遍历容器中的元素,这里不过多介绍,我们直接看使用示例:
deque<int> deq(78);
// 正向遍历
for (auto &it : deq)
{
// 内部循环体代码(省略)
}
// 等价于
for (auto it = deq.begin(); it != deq.end(); ++it)
{
// 内部循环体代码(省略)
}
// 其中,上式中的 it 等价于下式中的 *it
容器容量相关
- 检查容器是否为空
deque<int> deq(78);
// 函数声明原型为:bool empty() const;
// 检查容器是否无元素
deq.empty();
- 返回容器元素数量
deque<int> deq(78);
// 函数声明原型为:size_type size() const;
// 返回容器中的元素数
deq.size();
容器内容修改相关
插入元素
- 插入元素
deque<int> deq(78);
// 函数声明原型为:iterator insert( const_iterator pos, const T& value );
// 在 pos 前插入 value。(pos 是vector 的迭代器)
deq.insert(deq.begin(), 20);
// 函数声明原型为:iterator insert( const_iterator pos, size_type count, const T& value );
// 在 pos 前插入 value 的 count 个副本。(pos 是vector 的迭代器)
deq.insert(deq.begin() + 7, 21, 3);
- 原位构造元素
deque<int> deq(78);
// 使用方法大致与 insert 相同,函数原型略
// 效率在一般情况下优于 insert
deq.emplace(deq.begin(), 23);
- 将元素添加到容器末尾
deque<int> deq(78);
// 函数声明原型为:void push_back( const T& value );
// 追加给定元素 value 到容器尾。
deq.push_back(18);
- 在容器末尾原位构造元素
deque<int> deq(78);
// 添加新元素到容器尾。使用方法大致与 push_back 相同,函数原型略
// 效率在一般情况下优于 push_back
deq.emplace_back(18);
- 插入元素到容器起始
deque<int> deq(78);
// 函数声明原型为:void push_front( const T& value );
// 前附给定元素 value 到容器起始。
deq.push_front(12);
- 在容器头部原位构造元素
deque<int> deq(78);
// 添加新元素到容器头。使用方法大致与 push_front 相同,函数原型略
// 效率在一般情况下优于 push_front
deq.emplace_front(12);
注意:上述所有插入操作均会导致插入前的迭代器失效!但引用不会失效。
清除元素
- 清除内容
deque<int> deq(78);
// 函数声明原型为:void clear();
// 从容器擦除所有元素。此调用后 size() 返回零。
// 使任何指代所含元素的引用、指针和迭代器失效。
deq.clear();
- 擦除元素
deque<int> deq(78);
// 函数声明原型为:iterator erase( iterator pos );
// 移除位于 pos 的元素。(pos 是vector 的迭代器)
// 注意:pos 不能等于 vector::end() 所指向的迭代器
deq.erase(deq.begin() + 9);
// 函数声明原型为:iterator erase( iterator first, iterator last );
// 移除范围 [first, last) 中的元素。(first, last 是 vector 的迭代器)
deq.erase(deq.begin(), deq.begin() + 78);
// 注意:使用以上函数后,所有迭代器和引用都会失效,除非被擦除的元素在容器的开头或末尾,此时只有指向被擦除元素的迭代器或引用会失效。
- 移除末元素
deque<int> deq(78);
// 函数声明原型为:void pop_back();
// 移除容器的末元素。不要在空容器上调用此函数!
// 指向被擦除元素的迭代器和引用会失效。其他引用和迭代器不受影响。
deq.pop_back();
- 移除首元素
deque<int> deq(78);
// 函数声明原型为:void pop_front();
// 移除容器的首元素。不要在空容器上调用此函数!
// 指向被擦除元素的迭代器和引用会失效。其他迭代器和引用不受影响。