8. deque容器:双端队列的全面解析
deque,全称是 double-ended queue,中文叫双端队列。说实话,我第一次接触这个容器时,觉得它就是个“加强版 vector”。但用久了才发现,它有自己的脾气和性格。今天我就带你彻底搞懂它。
8.1 deque的构造与基本操作
deque 的构造方式和 vector 非常像。我习惯用下面这几种方式:
#include <deque>
#include <iostream>
using namespace std;
int main() {
// 1. 默认构造
deque<int> d1;
// 2. 指定大小和初始值
deque<int> d2(5, 10); // 5个10
// 3. 通过迭代器构造
int arr[] = {1, 2, 3, 4, 5};
deque<int> d3(arr, arr + 5);
// 4. 拷贝构造
deque<int> d4(d3);
// 5. 初始化列表(C++11起)
deque<int> d5 = {1, 2, 3, 4, 5};
return 0;
}
基本操作也很直观。我个人最常用的是 push_back、push_front、pop_back、pop_front 这四个。你看名字就知道意思了。
deque<int> d;
d.push_back(10); // 尾部插入
d.push_front(20); // 头部插入
d.pop_back(); // 删除尾部
d.pop_front(); // 删除头部
d[2] 或 d.at(2) 来获取元素。但 at() 会做边界检查,性能稍差一点。
8.2 deque与vector的区别
很多初学者会问:既然有了 vector,为什么还要 deque?这个问题问得好。我当年也困惑过。
说白了,核心区别就两点:头部操作效率和内存管理方式。
| 特性 | vector | deque |
|---|---|---|
| 头部插入/删除 | O(n),需要移动所有元素 | O(1),直接操作 |
| 尾部插入/删除 | O(1) 均摊 | O(1) |
| 随机访问 | O(1) | O(1) |
| 中间插入/删除 | O(n) | O(n) |
| 内存连续性 | 连续内存块 | 分段连续 |
| 迭代器失效 | 重新分配时全部失效 | 只在操作点附近失效 |
嗯,这里要注意:deque 的随机访问虽然也是 O(1),但常数比 vector 大。因为 vector 是真正的连续内存,直接指针偏移就行。deque 需要先定位到哪个内存块,再偏移。我在项目中做过性能测试,百万级随机访问,vector 比 deque 快 20% 左右。
8.3 deque的双端操作
双端操作是 deque 的看家本领。我举个例子,模拟一个简单的任务队列:
#include <deque>
#include <iostream>
#include <string>
using namespace std;
int main() {
deque<string> tasks;
// 从尾部添加任务
tasks.push_back("任务A:数据清洗");
tasks.push_back("任务B:特征提取");
tasks.push_back("任务C:模型训练");
// 突然来了个紧急任务,插到头部
tasks.push_front("紧急任务:数据校验");
// 处理任务:从头部取
while (!tasks.empty()) {
cout << "正在处理: " << tasks.front() << endl;
tasks.pop_front();
}
return 0;
}
你想想看,如果用 vector 实现同样的功能,每次在头部插入都要移动所有元素,效率差太多了。deque 就是为这种场景设计的。
另外,deque 还提供了 emplace_front 和 emplace_back,可以直接在头部或尾部构造元素,避免临时对象的拷贝。我建议你在性能敏感的场景下优先使用它们。
8.4 deque的内部原理
这部分可能是最让人头疼的,但理解了它,你才能真正用好 deque。
deque 的内部结构,说白了就是一个中控器 + 多个连续内存块。中控器是一个指针数组,每个指针指向一个固定大小的内存块(通常是 512 字节)。
为什么这样设计?我解释一下:
- 头部插入:如果当前第一个内存块还有空间,直接在前面插入。如果满了,就新分配一个内存块,更新中控器指针。不需要移动任何已有元素。
- 尾部插入:同理,最后一个内存块满了就新分配一个。
- 随机访问:先通过中控器找到对应的内存块,再在块内偏移。两步操作,所以比 vector 慢一点。
我记得有一次面试,面试官问我:deque 的迭代器失效规则是什么?我当时就画了这个图来解释。deque 的迭代器失效规则比 vector 温和:只有在操作点附近的内存块中的迭代器才会失效,其他块的不受影响。而 vector 一旦重新分配,所有迭代器全部失效。
最后说一句,我在实际项目中,deque 最常用的场景就是双端队列和滑动窗口。比如实现一个固定大小的缓冲区,需要从头部取出旧数据,从尾部加入新数据,deque 就是最佳选择。vector 做不到,list 又太慢。deque 刚刚好。