C++ deque 全面解析与实战指南
在C++标准模板库(STL)中,deque(double-ended queue,双端队列)是一种兼具vector和list部分特性的序列容器。它支持在队列的两端进行高效的插入和删除操作,同时也能像vector一样随机访问元素,是日常开发中处理动态数据的重要工具。本文将从deque的核心原理出发,详细讲解其常用接口,并结合实战案例演示如何在C++中使用deque,最后补充使用注意事项,帮助大家彻底掌握这一容器。
一、deque 核心原理剖析
要理解deque的特性,首先需要搞清楚它的底层实现。vector是基于连续的动态数组实现的,这使得它随机访问高效,但在头部插入/删除元素时需要移动大量数据,效率低下;list是基于双向链表实现的,头部和尾部插入/删除高效,但随机访问需要遍历链表,效率极低。而deque则采用了"分段连续存储"的设计,巧妙地平衡了两者的优缺点。
deque的底层结构可以理解为:一个指向多个连续内存块(缓冲区)的指针数组(也称为"中控器")。每个缓冲区存储一段连续的元素,中控器中的指针依次指向这些缓冲区。当需要在deque的两端插入元素时,如果当前缓冲区还有剩余空间,直接在对应端插入;如果缓冲区已满,则分配一个新的缓冲区,将其指针加入中控器,再进行插入操作。对于随机访问,deque会先通过中控器找到元素所在的缓冲区,再在缓冲区内部进行偏移访问,因此时间复杂度为O(1)。
核心优势总结:
-
两端插入/删除效率高(O(1)时间复杂度);
-
支持随机访问(operator[]、at()),效率接近vector;
-
内存分配更灵活,避免了vector在扩容时可能出现的大量数据拷贝。
二、C++ deque 常用接口详解
deque的接口设计与vector类似,同时新增了针对双端操作的接口。使用deque前,需要包含头文件 <deque>,并使用std命名空间(或显式指定std::deque)。以下是最常用的接口分类讲解:
2.1 构造与析构
| 接口原型 | 功能说明 | 示例 |
|---|---|---|
| deque(); | 默认构造函数,创建空deque | std::deque d; |
| deque(size_t n, const T& val = T()); | 创建包含n个val的deque | std::deque d(5, 10); // 5个10 |
| deque(InputIterator first, InputIterator last); | 迭代器构造,拷贝[first, last)区间元素 | std::vector v{1,2,3}; std::deque d(v.begin(), v.end()); |
| deque(const deque& other); | 拷贝构造函数 | std::deque d1{1,2}; std::deque d2(d1); |
| ~deque(); | 析构函数,释放所有资源 | - |
2.2 迭代器相关
deque支持双向迭代器,可通过迭代器遍历容器。需要注意的是,当deque进行插入/删除操作时,除了被操作元素所在的缓冲区外,其他缓冲区的迭代器不会失效(这一点与vector不同,vector扩容时所有迭代器都会失效)。
| 接口 | 功能说明 |
|---|---|
| begin() | 返回指向第一个元素的迭代器(非const) |
| end() | 返回指向最后一个元素下一个位置的迭代器(非const) |
| rbegin() | 返回指向最后一个元素的反向迭代器 |
| rend() | 返回指向第一个元素前一个位置的反向迭代器 |
| cbegin()/cend() | 返回const迭代器,不可修改元素 |
| 迭代器遍历示例: |
cpp
#include <deque>
#include <iostream>
using namespace std;
int main() {
deque<int> d{1,2,3,4,5};
// 正向遍历
for (auto it = d.begin(); it != d.end(); ++it) {
cout << *it << " "; // 输出:1 2 3 4 5
}
cout << endl;
// 反向遍历
for (auto it = d.rbegin(); it != d.rend(); ++it) {
cout << *it << " "; // 输出:5 4 3 2 1
}
return 0;
}
2.3 容量相关
| 接口 | 功能说明 |
|---|---|
| size() | 返回当前元素个数 |
| empty() | 判断容器是否为空(空返回true) |
| resize(size_t n, const T& val = T()) | 调整容器大小为n:n>当前size则补val;n<当前size则删除多余元素 |
| capacity() | 返回当前可容纳的元素个数(不同编译器实现可能不同,一般不常用) |
2.4 元素访问
deque支持随机访问,访问方式与vector一致,但需要注意:deque没有reserve()接口,无法提前预留内存。
| 接口 | 功能说明 | 注意事项 |
|---|---|---|
| operator[](size_t pos) | 访问索引pos处的元素(不做越界检查) | pos越界会导致未定义行为 |
| at(size_t pos) | 访问索引pos处的元素(做越界检查) | pos越界会抛出out_of_range异常 |
| front() | 返回第一个元素的引用(非空容器) | 容器为空时访问会导致未定义行为 |
| back() | 返回最后一个元素的引用(非空容器) | 容器为空时访问会导致未定义行为 |
2.5 插入与删除(核心特性)
deque的核心优势在于两端的插入/删除操作高效,同时也支持在中间插入/删除(但效率较低,因为需要移动对应缓冲区的元素)。
| 接口 | 功能说明 | 时间复杂度 |
|---|---|---|
| push_front(const T& val) | 在头部插入元素val | O(1) |
| push_back(const T& val) | 在尾部插入元素val | O(1) |
| pop_front() | 删除头部元素(不返回值) | O(1) |
| pop_back() | 删除尾部元素(不返回值) | O(1) |
| insert(iterator pos, const T& val) | 在迭代器pos位置插入val | O(n)(需移动元素) |
| erase(iterator pos) | 删除迭代器pos位置的元素 | O(n)(需移动元素) |
| clear() | 清空容器,删除所有元素(size变为0) | O(n) |
| 插入/删除示例: |
cpp
#include <deque>
#include <iostream>
using namespace std;
int main() {
deque<int> d;
// 两端插入
d.push_back(10);
d.push_front(20);
d.push_back(30);
d.push_front(40);
// 此时d:40 20 10 30
for (auto x : d) cout << x << " "; // 输出:40 20 10 30
cout << endl;
// 两端删除
d.pop_front();
d.pop_back();
// 此时d:20 10
for (auto x : d) cout << x << " "; // 输出:20 10
cout << endl;
// 中间插入
auto it = d.begin() + 1;
d.insert(it, 15);
// 此时d:20 15 10
for (auto x : d) cout << x << " "; // 输出:20 15 10
cout << endl;
// 中间删除
d.erase(it); // it仍指向原位置(此时对应15)
// 此时d:20 10
for (auto x : d) cout << x << " "; // 输出:20 10
cout << endl;
d.clear();
cout << "size after clear: " << d.size() << endl; // 输出:0
return 0;
}
三、deque 实战案例
下面通过两个经典场景,演示deque的实际应用:
3.1 场景1:实现一个简单的队列(先进先出)
队列的核心操作是"尾插首删",deque的push_back和pop_front接口恰好完美适配,效率远高于vector(vector.pop_front()为O(n))。
cpp
#include <deque>
#include <iostream>
#include <string>
using namespace std;
template <typename T>
class MyQueue {
private:
deque<T> dq;
public:
// 入队
void enqueue(const T& val) {
dq.push_back(val);
}
// 出队
void dequeue() {
if (!dq.empty()) {
dq.pop_front();
} else {
cout << "队列空,无法出队!" << endl;
}
}
// 获取队首元素
T front() const {
if (!dq.empty()) {
return dq.front();
}
throw runtime_error("队列空,无队首元素!");
}
// 判断队列是否为空
bool empty() const {
return dq.empty();
}
// 获取队列大小
size_t size() const {
return dq.size();
}
};
int main() {
MyQueue<string> q;
q.enqueue("苹果");
q.enqueue("香蕉");
q.enqueue("橙子");
cout << "队列大小:" << q.size() << endl; // 3
cout << "队首元素:" << q.front() << endl; // 苹果
q.dequeue();
cout << "出队后队首:" << q.front() << endl; // 香蕉
q.dequeue();
q.dequeue();
q.dequeue(); // 队列空,无法出队!
return 0;
}
3.2 场景2:滑动窗口最大值(LeetCode 239)
这是deque的经典算法题,利用deque维护一个单调递减的队列,可在O(n)时间复杂度内解决问题。核心思路:
-
deque中存储的是元素的索引,而非元素本身,用于判断元素是否超出窗口范围;
-
对于每个新元素,删除deque中所有小于当前元素的索引(保证deque单调递减);
-
将当前元素索引加入deque;
-
如果deque队首元素的索引超出窗口左边界,则删除队首;
-
当窗口大小达到要求时,队首元素即为当前窗口的最大值。
cpp
#include <deque>
#include <vector>
#include <iostream>
using namespace std;
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
vector<int> res;
deque<int> dq; // 存储索引,保持队列单调递减
for (int i = 0; i < nums.size(); ++i) {
// 1. 移除队列中小于当前元素的索引(保证单调递减)
while (!dq.empty() && nums[i] > nums[dq.back()]) {
dq.pop_back();
}
// 2. 加入当前元素索引
dq.push_back(i);
// 3. 移除窗口外的索引(窗口左边界为i - k + 1)
while (!dq.empty() && dq.front() < i - k + 1) {
dq.pop_front();
}
// 4. 当窗口大小达到k时,记录最大值(队首)
if (i >= k - 1) {
res.push_back(nums[dq.front()]);
}
}
return res;
}
int main() {
vector<int> nums = {1,3,-1,-3,5,3,6,7};
int k = 3;
vector<int> res = maxSlidingWindow(nums, k);
cout << "滑动窗口最大值:";
for (int x : res) {
cout << x << " "; // 输出:3 3 5 5 6 7
}
return 0;
}
四、deque 使用注意事项
-
避免在中间频繁插入/删除:虽然deque支持中间插入/删除,但效率较低(O(n)),如果需要频繁在中间操作,建议使用list。
-
迭代器失效问题:deque插入/删除元素时,只有被操作元素所在的缓冲区的迭代器会失效,其他缓冲区的迭代器仍有效;而vector扩容时所有迭代器都会失效。
-
无reserve()接口:deque的内存分配是分段的,无法像vector那样提前预留连续内存,因此resize()和push_back()可能会触发多次小内存分配,但不会像vector那样出现大量数据拷贝。
-
与vector、list的选择:
-
需要随机访问 + 两端插入/删除高效 → 选deque;
-
需要随机访问 + 主要在尾部插入/删除 → 选vector(效率更高);
-
不需要随机访问 + 频繁在任意位置插入/删除 → 选list。
-
-
线程安全性:STL容器均不保证线程安全,多线程环境下操作deque需要手动加锁(如使用mutex)。
五、总结
deque是C++ STL中一种平衡了随机访问和双端操作效率的序列容器,其底层的分段连续存储设计使其兼具vector和list的部分优点。在实际开发中,deque常用于实现队列、滑动窗口等场景,尤其适合需要频繁在两端插入/删除且需要随机访问的需求。掌握deque的核心原理和常用接口,能帮助我们更灵活地处理动态数据,提升程序效率。