1.引言
在 C++
标准模板库(STL
)中,deque
(双端队列)是一种非常重要且灵活的序列式容器。与vector
类似,它支持快速的随机访问,并且在头尾两端都能高效地插入和删除元素。
1.1什么是 deque
?
deque
是 "double-ended queue" 的缩写,翻译为"双端队列"。它是一种可以在容器头部和尾部快速进行插入和删除操作的顺序容器。
1.2deque
与 vector
的异同
特性 | deque | vector |
---|---|---|
内存结构 | 分段连续(块+映射表) | 完全连续 |
头部插入/删除 | O(1) | O(n)(需要移动所有元素) |
尾部插入/删除 | O(1) | O(1)(均摊) |
中间插入/删除 | O(n) | O(n) |
随机访问性能 | O(1),稍慢于vector | O(1),最优 |
内存稳定性 | 可能导致realloc | 较稳定(不连续) |
迭代器失效 | 头尾操作可能局部失效,中间操作全失效 | 扩容时全失效 |
关键区别:
deque
适合高频头尾操作(如任务队列、滑动窗口)。
vector
适合尾部操作+随机访问(如动态数组、数据缓存)。
1.3deque 的适用场景和优势
适用场景
任务调度系统:需频繁从队列头部取出任务,尾部添加新任务。
滑动窗口算法:如 LeetCode 题目中的双指针问题(高效移除/添加两端元素)。
撤销操作栈:支持从历史记录的前后两端操作(如 Ctrl+Z 和 Ctrl+Y)。
缓存实现:结合 push_front
和 pop_back
实现 LRU
缓存淘汰策略。
优势总结
灵活性:双端操作无需移动大量元素。
平衡的性能:随机访问接近 vector
,头尾操作优于 vector
。
内存效率:分段存储避免 vector
扩容时的全量拷贝。
2. deque
的常见构造方式
2.1默认构造
创建一个空的 deque
,不分配内存,直到首次插入元素时才分配初始块。
deque<int> dq1;// 空 deque,size=0,capacity=0
适用场景:
元素数量未知,后续动态添加。
2.2填充构造
创建包含 n
个元素,每个元素初始化为指定值。
deque<string> dq2(5, "hello");
// dq2: ["hello", "hello", "hello", "hello", "hello"]
底层行为:
一次性分配足够块存储 n
个元素,避免多次扩容。
适用场景:
预先分配空间并初始化默认值(如游戏中的初始对象池)。
2.3范围构造(迭代器)
通过其他容器的迭代器范围初始化 deque
。
cpp
vector<int> vec = { 1, 2, 3, 4, 5 };
deque<int> dq3(vec.begin(), vec.end());
// dq3: [1, 2, 3, 4, 5]
性能优势:
提前计算元素数量,优化块分配策略。
适用场景:
从数组、vector
或文件数据快速构建deque
。
2.4拷贝构造
深拷贝另一个 deque
的所有元素。
deque<int> dq4 = { 1, 2, 3 };
deque<int> dq5(dq4); // dq5: [1, 2, 3]
注意:
对包含对象的deque
,会调用元素的拷贝构造函数。
2.5移动构造(C++11+)
"窃取"另一个 deque 的资源(避免深拷贝)。
cpp
deque<int> dq6 = { 10, 20, 30 };
deque<int> dq7(std::move(dq6));
// dq7: [10, 20, 30], dq6 变为空
适用场景:
函数返回deque
时优化性能(如工厂模式创建队列)。
2.6初始化列表构造(C++11+)
直接用花括号初始化。
deque<int> dq8 = { 7, 8, 9 }; // dq8: [7, 8, 9]
底层机制:
先计算元素数量,分配连续块,再初始化值。
3. deque
常用成员函数
3.1增加元素:
push_back
/emplace_back
功能:在尾部插入元素
区别:
push_back
:拷贝或移动已有对象。
emplace_back
:原地构造对象(避免临时对象拷贝)。
cpp
deque<std::string> dq;
dq.push_back("hello"); // 拷贝构造
dq.emplace_back("world"); // 原地构造,效率更高
push_front
/ emplace_front
功能:在头部插入元素(vector
不具备的能力)。
dq.push_front("first"); // 头部插入
dq.emplace_front("second"); // 原地构造
性能:
头尾插入均为 O(1)
(分摊时间复杂度)。
3.2删除元素:
pop_back
/ pop_front
功能:删除尾/头部元素(无返回值)。
dq.pop_back(); // 删除尾部
dq.pop_front(); // 删除头部
erase
功能:删除指定位置或范围的元素。
cpp
deque<int>::iterator it = dq.begin() + 1;
dq.erase(it); // 删除第2个元素
dq.erase(dq.begin(), it); // 删除前两个元素
注意:
中间删除需移动元素,时间复杂度为O(n)
。
clear
功能:清空所有元素,释放内存块。
dq.clear();// size=0,但可能保留底层块(实现依赖)
3.3访问元素:
operator[]
vs at()
方法 | 越界行为 | 性能 |
---|---|---|
operator[] | 未定义行为(可能崩溃) | 更快 |
at() | 抛出out_of_range | 稍慢 |
cpp
deque<int> dq = { 10, 20, 30 };
int a = dq[1]; // 20(不检查越界)
int b = dq.at(1); // 20(越界时抛出异常)
front()
/ back()
功能:直接访问首/尾元素(效率优于 dq[0]
)。
cpp
int first = dq.front(); // 10
int last = dq.back(); // 30
迭代器访问
cpp
for (deque<int>::iterator it = dq.begin(); it != dq.end(); ++it) {
std::cout << *it << " "; // 10 20 30
}
3.4容量与大小:
size()
/ empty()
cpp
if (!dq.empty()) {
cout << "元素数量: " << dq.size(); // 3
}
resize(n)
功能:调整元素数量,多删少补默认值。
dq.resize(5); // 扩容为5,新增元素默认初始化为0
dq.resize(2); // 截断后保留前两个元素
shrink_to_fit()
功能:请求释放未使用的内存块(非强制)。
dq.shrink_to_fit(); // 减少内存占用(实现可能忽略)
4. deque
的底层实现与性能分析
与 vector
不同,deque
(双端队列)采用分段连续内存结构,不依赖单一大块内存,这使它在首尾两端插入/删除元素时拥有更优性能表现。
4.1底层结构:分段连续存储
核心组件
1.中控映射表(Map
):
存储指向各个内存块的指针(类似指针数组)。
动态扩容时,中控表重新分配(通常加倍增长)。
2.固定大小的内存块(Chunk
):
每个块存储若干元素(如 512 字节块存储 64 个 int
)。
块之间内存地址不连续,但块内连续。
优势:
头尾插入无需移动所有元素,只需分配新块或复用空闲块。
随机访问通过计算块偏移实现。
4.2 动态扩容机制
扩容场景
1.头部插入时首个块已满:
分配新块插入头部,更新中控表。
2.尾部插入时末个块已满:
分配新块插入尾部,更新中控表。
与vector扩容对比
行为 | deque | vector |
---|---|---|
扩容触发 | 单个块满时 | 整体容量不足时 |
重新分配成本 | 仅扩展中控表,拷贝指针 | 拷贝所有元素到新内存 |
迭代器失效 | 局部失效(操作点附近) | 全部失效 |
4.3时间复杂度总结
操作 | deque | vector | 说明 |
---|---|---|---|
push_front | O(1) | O(n) | vector 需移动所有元素 |
push_back | O(1) | O(1) | 两者均高效 |
随机访问 | O(1) | O(1) | deque 有固定偏移计算开销 |
中间插入/删除 | O(n) | O(n) | 均需移动元素 |
4.4关键问题
Q1:为什么 deque
不提供 capacity()
?
deque
的容量由中控表和块数量决定,无连续内存概念,capacity()
无意义。
Q2:deque
的内存碎片问题如何解决?
频繁小块分配可能导致碎片,可通过自定义分配器(Allocator
)优化。
5. 算法库协作(STL
算法)
STL
中的算法(如 sort、find、count、transform 等)并不专门为某种容器设计,而是通过迭代器 操作容器元素。因此,deque
作为支持随机访问迭代器的容器,可以很好地与大多数 STL 算法配合使用。
5.1排序算法(sort
)
基本用法
cpp
#include<deque>
#include<algorithm>
using namespace std;
int mian(){
deque<int> dq = { 5, 3, 1, 4, 2 };
sort(dq.begin(), dq.end()); // 升序排序
// dq: [1, 2, 3, 4, 5]
}
输出内容:

性能陷阱
内存局部性影响:
deque
的分段存储导致排序时缓存命中率低于 vector
,实测慢 10%-30%。
cpp
// 测试:对 100,000 个元素排序
sort(vec.begin(), vec.end()); // vector: ~15ms
sort(dq.begin(), dq.end()); // deque: ~20ms
优化建议
1.对全量排序需求,优先转存到 vector
:
cpp
vector<int> tmp(dq.begin(), dq.end());
sort(tmp.begin(), tmp.end());
- 使用
std::stable_sort
保持相等元素顺序(deque
支持随机访问,兼容该算法)。
5.2查找算法
std::find
线性查找
cpp
auto it = find(dq.begin(), dq.end(), 3);
if (it != dq.end()) {
cout << "Found: " << *it; // 输出: Found: 3
}
时间复杂度:O(n)
,与 vector
性能相当。
std::lower_bound
二分查找
前提:deque
必须已排序。
优势:O(log n)
时间复杂度。
cpp
sort(dq.begin(), dq.end());
auto it = lower_bound(dq.begin(), dq.end(), 3);
std::count
统计容器中某个"特定值"出现的次数。
cout << "数值2的个数:" << count(dq.begin(), dq.end(), 2) << endl;
std::any_of
判断是否至少存在一个元素满足某个"条件"。
cpp
//是否存在偶数
bool has_even = any_of(dq.begin(), dq.end(), [](int x) {return x % 2 == 0; });
cout << "存在否:" << (has_even? "yes" : "no" )<< endl;
5.3函数式编程(for_each
/transform
)
遍历元素
cpp
for_each(dq.begin(), dq.end(), [](int x) {
cout << x << " "; // 打印所有元素
});
元素转换
cpp
deque<int> dq = { 1,2,3,4,5 };
deque<int> squared(dq.size());
transform(dq.begin(), dq.end(), squared.begin(),
[](int x) { return x * x; });
// squared: [1, 4, 9, 16, 25]
性能提示
deque
的遍历性能接近 vector
(块内连续内存优势)。
避免在 transform
中频繁扩容,预分配结果容器大小。
6. 使用 deque
的注意事项
虽然 deque
是一个功能强大的双端队列容器,但在实际使用中也有一些易忽略的细节和潜在的坑点。
6.1迭代器失效规则
失效场景分类
操作类型 | 失效范围 | 示例 |
---|---|---|
头尾插入/删除 | 可能使部分迭代器失效 | push_front()、pop_back() |
中间插入/删除 | 所有迭代器失效 | insert()、erase() |
排序/扩容 | 所有迭代器失效 | std::sort()、resize() |
危险代码示例
cpp
deque<int> d = { 1, 2, 3 };
auto it = d.begin() + 1;
d.push_front(0); // it 失效!
cout << *it; // 未定义行为(崩溃)
安全实践
操作后重置迭代器:
it = dq.begin() + 2; // 重新计算位置
改用索引访问:
cpp
size_t index = 1;
dq.push_front(0);
int val = dq[index]; // 安全
6.2插入/删除性能与位置有关
中间操作性能对比:
cpp
deque<int> d(100000, 0);
auto start = chrono::high_resolution_clock::now();
d.insert(d.begin() + 50000, 42); // 需移动 50,000 个元素
auto end = chrono::high_resolution_clock::now();
// 实测耗时:~200μs(vector 约 ~150μs)
结论
高频中间操作场景应选择list
(O(1)
插入/删除)。
批量删除优化
使用 erase-remove
惯用法:
dq.erase(remove(dq.begin(), dq.end(), 0), dq.end());
6.3对象拷贝问题
深浅拷贝需求示例
cpp
class FileHandle {
FILE* file;
public:
FileHandle(const char* filename) { file = fopen(filename, "r"); }
~FileHandle() { if (file) fclose(file); }
// 必须实现深拷贝
FileHandle(const FileHandle&) = delete; // 禁止拷贝
FileHandle& operator=(const FileHandle&) = delete;
};
int main() {
deque<FileHandle> dq;
// dq.push_back(FileHandle("a.txt")); // 错误!需要移动语义
dq.emplace_back("a.txt"); // 正确:原地构造
}
解决方案
1.禁用拷贝:使用 =delete
显式禁止拷贝构造。
2.移动语义:实现移动构造函数和移动赋值运算符。
3.智能指针:存储 std::unique_ptr
而非直接对象。
6.4多线程安全
竞态条件示例
cpp
deque<int> dq;
void unsafe_push() {
for (int i = 0; i < 10000; ++i) {
dq.push_back(i); // 多线程下导致数据竞争
}
}
线程安全方案
方案 | 适用场景 | 示例 |
---|---|---|
互斥锁 | 低并发简单操作 | std::mutex + lock_guard |
读写锁 | 读多写少(C++17) | std::shared_mutex |
无锁队列 | 高性能场景 | tbb::concurrent_queue |
代码示例(互斥锁 ):
cpp
mutex mtx;
void safe_push(int i) {
lock_guard<std::mutex> lock(mtx);
dq.push_back(i);
}
6.5 内存碎片问题
问题描述
deque
的分块存储可能导致内存碎片,尤其在频繁动态增长时。
解决方案
预分配块:
cpp
deque<int> dq;
dq.resize(1000); //预分配固定数量元素
7. 实际应用场景
本章通过三个典型场景,展示 deque
如何解决实际问题。
场景 1:缓存系统的实现(LRU缓存淘汰策略)
需求分析
需要快速访问最近使用的数据。
当缓存满时,自动淘汰最久未使用的数据。
deque实现方案
cpp
#include <iostream>
#include<deque>
#include<string>
#include<unordered_map>
using namespace std;
template <typename K, typename V>
class LRUCache {
private:
deque<K> access_order; // 保存键的访问顺序
unordered_map<K, V> cache; // 实际缓存内容,键值对存储
size_t max_size; // 缓存最大容量
// 将访问过的 key 移动到队列前端(表示最近使用)
void moveToFront(K key) {
// 从 access_order 中删除 key 的旧位置(如果存在)
// remove 并不真的删除元素,而是将所有 != key 的元素"前移",返回新逻辑结尾的迭代器
// 然后用 deque 的 erase 将末尾被标记"移除"的元素真正删除
access_order.erase(
remove(access_order.begin(), access_order.end(), key),
access_order.end());
access_order.push_front(key);
}
public:
LRUCache(size_t size) : max_size(size) {}
void put(K key, V value) {
//如果缓存已满,淘汰 access_order 末尾的键(最久未访问)。
if (cache.size() >= max_size) {
K old_key = access_order.back(); // 淘汰最久未使用的
cache.erase(old_key);
access_order.pop_back();
}
//将新数据写入缓存,并将键放到 deque 前端。
cache[key] = value;
access_order.push_front(key);
}
V* get(K key) {
auto it = cache.find(key);
if (it == cache.end()) return nullptr;
moveToFront(key); // 更新访问顺序
return &(it->second);
}
//按照 access_order 顺序(最近→最久)打印当前缓存内容。
void print() {
for (const auto& key : access_order) {
cout << "[" << key << ": " << cache[key] << "] ";
}
cout << endl;
}
};
测试用例
cpp
int main() {
LRUCache<int, string> cache(3);
cache.put(1, "A");
cache.put(2, "B");
cache.put(3, "C");
cache.print(); // 输出: [3: C] [2: B] [1: A]
cache.get(2); // 访问键2
cache.put(4, "D"); // 淘汰键1
cache.print(); // 输出: [4: D] [2: B] [3: C]
}
输出内容:

场景 2:任务调度系统(动态优先级队列)
需求分析
支持从队列头部取出高优先级任务。
允许从尾部插入普通任务。
deque
实现方案
cpp
#include <iostream>
#include<deque>
#include<string>
#include<algorithm>
using namespace std;
struct Task {
string name;
int priority; //优先级数值越小,优先级越高
//重载运算符,用于排序
bool operator<(const Task& other)const {
return priority < other.priority;
}
};
class PriorityTaskDueue {
private:
deque<Task> tasks;
bool is_sorted = false; //标记是否需要重新排序
public:
//添加任务(头部添加高优先级任务,尾部添加普通任务)
void addTask(const Task& task, bool is_high_priority = false) {
if (is_high_priority) {
tasks.push_front(task);
} else {
tasks.push_back(task);
}
is_sorted = false;
}
//处理最高优先级任务
Task processNext() {
if (tasks.empty()) {
throw runtime_error("队列为空");
}
//按需要排序
if (!is_sorted) {
sort(tasks.begin(), tasks.end());
is_sorted = true;
}
Task next_task = tasks.front();
tasks.pop_front();
return next_task;
}
//打印当前队列状态
void printDeque() const {
for (const auto& task : tasks)
cout << "[" << task.priority << "]" << task.name << endl;
}
};
int main()
{
PriorityTaskDueue dq;
dq.addTask({ "常规处理", 3 });
dq.addTask({ "紧急处理",1 });
dq.addTask({ "数据备份",2 });
//处理任务
cout << "当前队列:\n";
dq.printDeque();
auto task = dq.processNext();
cout << "\n正在处理:" << task.name << endl;
cout << "\n剩余队列:\n";
dq.printDeque();
}
输出内容:

性能优化策略
操作 | 时间复杂度 | 优化手段 |
---|---|---|
addTask | O(1) | 头部/尾部直接插入 |
processNext | O(n log n) | 惰性排序(仅在实际需要时排序) |
空间占用 | O(n) | 分段存储减少扩容开销 |
关键优化点:
-
惰性排序:仅在
processNext()
时触发排序,避免频繁全队列排序。 -
双端插入:高优先级任务直接
push_front
,减少排序压力。 -
动态任务队列管理(例如:任务队列的前后操作)
场景 3:滑动窗口最大值问题
需求分析
给定一个整数数组和窗口大小 k
,返回每个窗口中的最大值。
要求时间复杂度优于暴力解法(O(nk)
)。
deque
实现方案(O(n)
解法)
cpp
#include <deque>
#include <vector>
#include<iostream>
using namespace std;
vector<int> maxSlidingWindow(const vector<int>& nums, int k) {
deque<int> dq; // 存储索引,维护递减序列
vector<int> result;
for (int i = 0; i < nums.size(); ++i) {
// 移除超出窗口范围的元素
while (!dq.empty() && dq.front() <= i - k) {
dq.pop_front();
}
// 维护递减序列
while (!dq.empty() && nums[dq.back()] < nums[i]) {
dq.pop_back();
}
dq.push_back(i);
// 记录当前窗口最大值
if (i >= k - 1) {
result.push_back(nums[dq.front()]);
}
}
return result;
}
int main() {
vector<int> nums = { 1, 3, -1, -3, 5, 3, 6, 7 };
auto result = maxSlidingWindow(nums, 3);
for (int num : result) {
cout << num << " "; // 输出: 3 3 5 5 6 7
}
}
输出内容:

8. 总结
deque 核心优势
1.双端操作高效:push_front
/pop_front
和 push_back
/pop_back
均为 O(1)
。
2.平衡的访问性能:随机访问 O(1)
(稍慢于 vector
),比 list
快 10
倍以上。
3.动态分段存储:避免 vector
扩容时的全量拷贝,适合大规模数据。
何时选择 deque
?
高频头尾操作:消息队列、滑动窗口、撤销历史记录。
需要随机访问的双端队列:替代 list
(如游戏中的事件处理)。
内存敏感场景:避免 vector
的扩容代价(如嵌入式设备)。
何时避免 deque
?
纯尾部操作 → 用 vector
。
高频中间插入/删除 → 用 list
。
严格内存连续 → 用 vector
(如 C
接口交互)。
一句话决策
"头尾操作多,还要随机访,就用 deque
不会错;中间操作多,直接上 list
准没错。"
终极建议:
先测后选:用性能测试(如 Google Benchmark)验证实际场景。
保持简单:默认用 vector
,遇到头插瓶颈再切 deque
。