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_backpush_frontpop_backpop_front 这四个。你看名字就知道意思了。

deque<int> d;
d.push_back(10);   // 尾部插入
d.push_front(20);  // 头部插入
d.pop_back();      // 删除尾部
d.pop_front();     // 删除头部
小提示:deque 支持随机访问,你可以用 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% 左右。

避坑指南:我曾经在做一个消息队列时,用了 deque 来存储待处理的消息。一开始觉得双端操作很方便,但后来发现频繁在中间位置插入数据,性能急剧下降。deque 的中间插入和 vector 一样是 O(n),千万别被它“双端高效”的外表迷惑了。

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_frontemplace_back,可以直接在头部或尾部构造元素,避免临时对象的拷贝。我建议你在性能敏感的场景下优先使用它们。

8.4 deque的内部原理

这部分可能是最让人头疼的,但理解了它,你才能真正用好 deque。

deque 的内部结构,说白了就是一个中控器 + 多个连续内存块。中控器是一个指针数组,每个指针指向一个固定大小的内存块(通常是 512 字节)。

deque 内部结构示意图 中控器(指针数组) 内存块1 元素[0]~[N-1] 内存块2 元素[N]~[2N-1] 内存块3 元素[2N]~[3N-1] 内存块4 元素[3N]~[4N-1] 每个内存块大小固定(通常512字节),块内元素连续存储 块与块之间不连续,通过中控器指针连接 push_front push_back 双端操作只影响首尾内存块,无需移动其他块内元素

为什么这样设计?我解释一下:

  • 头部插入:如果当前第一个内存块还有空间,直接在前面插入。如果满了,就新分配一个内存块,更新中控器指针。不需要移动任何已有元素。
  • 尾部插入:同理,最后一个内存块满了就新分配一个。
  • 随机访问:先通过中控器找到对应的内存块,再在块内偏移。两步操作,所以比 vector 慢一点。

我记得有一次面试,面试官问我:deque 的迭代器失效规则是什么?我当时就画了这个图来解释。deque 的迭代器失效规则比 vector 温和:只有在操作点附近的内存块中的迭代器才会失效,其他块的不受影响。而 vector 一旦重新分配,所有迭代器全部失效。

核心总结:deque 用空间换时间。它通过分段连续的内存布局,实现了双端 O(1) 的插入删除,同时保留了随机访问的能力。代价就是内存管理更复杂,随机访问常数更大。

最后说一句,我在实际项目中,deque 最常用的场景就是双端队列滑动窗口。比如实现一个固定大小的缓冲区,需要从头部取出旧数据,从尾部加入新数据,deque 就是最佳选择。vector 做不到,list 又太慢。deque 刚刚好。


公众号:蓝海资料掘金营,微信deep3321