deque(双端队列)是C++标准模板库(STL)中一个非常重要的序列容器,它允许在头部和尾部高效地进行插入和删除操作。与vector相比,deque在头部插入元素的性能更优;与list相比,deque支持随机访问且内存使用更紧凑。
在实际项目中,我经常使用deque来处理需要频繁在两端操作数据的场景。比如最近开发的一个实时交易系统中,就用deque实现了价格缓冲区,既要在前端接收最新报价,又要在后端处理历史数据,deque完美满足了这种需求。
deque的底层实现通常采用分段连续空间的方式,由多个固定大小的数组块(称为缓冲区)组成,通过一个中央映射表(map)来管理这些缓冲区。这种设计使得:
注意:不同编译器的实现可能有差异,但基本原理相同。例如GCC的实现中,默认缓冲区大小为512字节。
通过基准测试可以明显看出各容器的差异:
| 操作 | deque | vector | list |
|---|---|---|---|
| 头部插入 | O(1) | O(n) | O(1) |
| 尾部插入 | O(1) | O(1)* | O(1) |
| 中间插入 | O(n) | O(n) | O(1) |
| 随机访问 | O(1) | O(1) | O(n) |
| 内存连续性 | 部分 | 完全 | 无 |
*注:vector的尾部插入在未扩容时为O(1),扩容时需要O(n)时间移动元素
deque提供多种构造方式:
cpp复制#include <deque>
using namespace std;
// 1. 默认构造
deque<int> dq1;
// 2. 指定初始大小
deque<string> dq2(10); // 10个空字符串
// 3. 带初始值的构造
deque<char> dq3(5, 'A'); // 5个'A'
// 4. 通过迭代器构造
int arr[] = {1,2,3,4,5};
deque<int> dq4(arr, arr+5);
// 5. C++11列表初始化
deque<float> dq5 = {1.1f, 2.2f, 3.3f};
deque提供多种访问元素的方式,需特别注意边界检查:
cpp复制deque<int> dq = {10,20,30,40,50};
// 1. 使用[]运算符(不检查边界)
int val1 = dq[2]; // 30
// 2. 使用at()方法(会检查边界)
int val2 = dq.at(3); // 40
// dq.at(10); // 抛出std::out_of_range异常
// 3. 访问首尾元素
int front = dq.front(); // 10
int back = dq.back(); // 50
// 4. 使用迭代器
for(auto it = dq.begin(); it != dq.end(); ++it) {
cout << *it << " ";
}
提示:在调试阶段建议多用at()而非[],可以及早发现越界问题。
deque最强大的特性就是两端操作的高效性:
cpp复制deque<string> dq = {"C++", "Java"};
// 头部插入
dq.push_front("Python");
// dq: ["Python", "C++", "Java"]
// 尾部插入
dq.push_back("Go");
// dq: ["Python", "C++", "Java", "Go"]
// 头部删除
dq.pop_front();
// dq: ["C++", "Java", "Go"]
// 尾部删除
dq.pop_back();
// dq: ["C++", "Java"]
// 任意位置插入
dq.insert(dq.begin()+1, "Rust");
// dq: ["C++", "Rust", "Java"]
// 删除指定位置元素
dq.erase(dq.begin());
// dq: ["Rust", "Java"]
cpp复制deque<int> dq(100, 0);
cout << dq.size(); // 100
cout << dq.max_size(); // 理论最大元素数
cout << dq.empty(); // 0 (false)
dq.resize(50); // 缩小到50个元素
dq.resize(150); // 扩大到150个元素,新增元素值为0
deque可以与STL算法完美配合:
cpp复制#include <algorithm>
deque<int> dq = {5,3,7,1,9};
// 排序
sort(dq.begin(), dq.end()); // 1,3,5,7,9
// 查找
auto it = find(dq.begin(), dq.end(), 5);
if(it != dq.end()) {
cout << "Found at position: " << (it - dq.begin());
}
// 遍历并处理
for_each(dq.begin(), dq.end(), [](int& n){
n *= 2;
});
对于性能敏感的场景,可以自定义内存分配器:
cpp复制template<typename T>
class CustomAllocator {
// 实现allocator接口
};
deque<int, CustomAllocator<int>> customDeque;
cpp复制deque<int> dq;
dq.resize(1000); // 预先分配约1000个元素的空间
cpp复制vector<int> v = {1,2,3,4,5};
dq.insert(dq.end(), v.begin(), v.end());
deque的迭代器失效规则比vector简单但比list复杂:
cpp复制deque<int> dq = {1,2,3,4,5};
auto it = dq.begin() + 2;
dq.push_front(0); // it仍然有效
dq.pop_back(); // it仍然有效
dq.insert(it, 10); // 所有迭代器失效!
deque的内存使用特点:
cpp复制deque<int>(dq).swap(dq);
标准容器都不是线程安全的,使用时需要注意:
cpp复制mutex mtx;
deque<Message> msgQueue;
// 生产者线程
{
lock_guard<mutex> lock(mtx);
msgQueue.push_back(msg);
}
// 消费者线程
{
lock_guard<mutex> lock(mtx);
if(!msgQueue.empty()) {
auto msg = msgQueue.front();
msgQueue.pop_front();
}
}
在算法题中,deque常用于实现滑动窗口最大值:
cpp复制vector<int> maxSlidingWindow(vector<int>& nums, int k) {
deque<int> dq;
vector<int> res;
for(int i = 0; i < nums.size(); ++i) {
// 移除超出窗口的元素
if(!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)
res.push_back(nums[dq.front()]);
}
return res;
}
在游戏开发中,可以用deque实现帧任务调度:
cpp复制class FrameTaskScheduler {
deque<function<void()>> taskQueue;
mutex queueMutex;
public:
void AddUrgentTask(function<void()> task) {
lock_guard<mutex> lock(queueMutex);
taskQueue.push_front(task);
}
void AddNormalTask(function<void()> task) {
lock_guard<mutex> lock(queueMutex);
taskQueue.push_back(task);
}
void ProcessTasks() {
function<void()> task;
{
lock_guard<mutex> lock(queueMutex);
if(taskQueue.empty()) return;
task = taskQueue.front();
taskQueue.pop_front();
}
task();
}
};
实现类似Photoshop的历史记录功能:
cpp复制class HistoryManager {
deque<unique_ptr<Command>> history;
size_t maxHistory = 50;
size_t current = 0;
public:
void Execute(unique_ptr<Command> cmd) {
cmd->Execute();
// 清除重做历史
while(history.size() > current) {
history.pop_back();
}
history.push_back(move(cmd));
current++;
// 限制历史记录数量
if(history.size() > maxHistory) {
history.pop_front();
current--;
}
}
bool Undo() {
if(current == 0) return false;
history[--current]->Undo();
return true;
}
bool Redo() {
if(current >= history.size()) return false;
history[current++]->Execute();
return true;
}
};
通过基准测试比较deque的各种操作(单位:纳秒/操作):
| 操作类型 | 元素数量=1K | 元素数量=1M |
|---|---|---|
| push_front | 15 | 18 |
| push_back | 12 | 14 |
| 随机访问 | 3 | 5 |
| 中间插入 | 120 | 150000 |
| 遍历所有元素 | 800 | 950000 |
测试环境:Intel i7-9700K, GCC 9.3, -O2优化
cpp复制deque<MyClass> dq;
dq.emplace_back(arg1, arg2); // 直接在容器内构造对象
考虑内存局部性:对于需要频繁遍历的场景,vector可能更合适
批量操作:尽量使用区间版本的insert/erase减少操作次数
了解其他语言中的类似结构有助于更好地理解deque:
| 语言 | 类似结构 | 主要差异 |
|---|---|---|
| Java | ArrayDeque | 没有分段存储,扩容时复制所有元素 |
| Python | collections.deque | 纯链表实现,不支持随机访问 |
| C# | LinkedList | 纯链表实现,API更丰富 |
| Rust | VecDeque | 与C++ deque实现类似,更安全 |
| JavaScript | Array | 使用数组模拟,两端操作效率较低 |
在实际跨平台项目中,我曾经遇到过因为不了解这些差异导致的性能问题。比如在Python中误以为deque支持高效随机访问,结果导致性能瓶颈。