deque(double-ended queue,双端队列)就是"两头都能高效进出的 vector"。 它补上了 vector 唯一的短板——在头部插入要整体搬家。代价是:内部改成若干块连续内存 + 一张"块目录",所以按下标访问比 vector 稍慢一点点(多一次查目录),中间插入删除依然是要搬家的。判断标准:你的数据是不是两头都在动? (像"排队买票",两头都能排)——是就选 deque。
承接 13.1.1。vector 的强项是"尾部"(push_back 很快),弱项是"头部"——push_front 一个元素,后面几万个元素全得往后挪一格。
deque 专门治这个。
它和 vector 的关系可以这样看:两者都支持随机访问(都有 []、都能用 sort),差别只在"内存怎么摆"——vector 是"一整块",deque 是"若干块拼起来"。内存摆法决定了快慢。
这一节也会碰到 10.4.3 静态类成员式的"全局信息"思路(deque 内部那张块目录就像全类共用的登记表),以及 7.5.1 指针与一维数组里的"数组套数组"结构。
一、vector 的头插之痛(用数据说话)
先看看问题到底有多大:
#include <iostream>
#include <vector>
#include <deque>
#include <ctime>
using namespace std;
int main() {
const int N = 100000;
// ① vector 头部插入
vector<int> v;
clock_t t1 = clock();
for (int i = 0; i < N; i++) v.insert(v.begin(), i); // 每次都往最前面塞
clock_t t2 = clock();
cout << "vector 头插 " << N << " 次:" << (double)(t2 - t1) / CLOCKS_PER_SEC << " 秒" << endl;
// ② deque 头部插入
deque<int> d;
clock_t t3 = clock();
for (int i = 0; i < N; i++) d.push_front(i); // ★ 就是这一句
clock_t t4 = clock();
cout << "deque 头插 " << N << " 次:" << (double)(t4 - t3) / CLOCKS_PER_SEC << " 秒" << endl;
return 0;
}
典型的输出(你的机器上数字会不同,但差距的量级会一样):
vector 头插 100000 次:3.85 秒
deque 头插 100000 次:0.002 秒
差了三个数量级。 原因很简单:
vector头插:insert(begin(), ...)要把已有元素全部往后挪一格,每次 O(n),n 次就是 O(n²)。deque头插:在最前面那块的空位里放进去就行,每次 O(1)。
这就是"选容器"这件事的真实含义——不是风格问题,是算法复杂度问题。同样的需求,选错容器,慢一千倍。
二、deque 的内存长什么样
vector:一整块连续内存,快满时申请一块更大的,整体搬过去。
vector: [ 0 | 1 | 2 | 3 | 4 | 5 | 空 | 空 ]
^begin() ^end() 头插 → 全体右移
deque:维护一张"块目录"(中控数组),每个格子指向一块连续内存。数据从两端"往两头长"。
中控数组
┌────┬────┬────┬────┐
│ │ ● │ ● │ │
└────┴─┬──┴─┬──┴────┘
│ │
┌──────▼─┐ ┌▼──────┐
│ 5 6 7│ │ 8 9 │ ← 每块内部仍是连续数组
└────────┘ └───────┘
▲头插在这儿加块 ▲尾插在这儿加块
既能 push_front 也能 push_back —— 两头都不用搬家
这就解释了三件事:
| 属性 | 为什么 |
|---|---|
| 两头插入删除都是 O(1) | 两端有空位就用,没有就再挂一块(搬的只是目录,不是数据) |
支持 [] 和随机访问 |
目录 + 块内偏移两次计算就能定位(比 vector 多一次) |
| 中间插入删除还是要 O(n) | 中间的元素仍在某一块里排着,插进去照样要挪 |
一句话:
deque=vector的随机访问能力 + 两端的 O(1) 出入 + 一点点"多次查表"的代价。
三、deque 的操作:和 vector 几乎一样,多两个
#include <iostream>
#include <deque>
#include <string>
using namespace std;
void show(const char *title, const deque<string> &d) {
cout << title << " [size=" << d.size() << "]:";
for (size_t i = 0; i < d.size(); i++) cout << d[i] << " ";
cout << endl;
}
int main() {
deque<string> d;
// ── ① 两头加(这是 deque 的存在理由)────
d.push_back("B"); // 尾加
d.push_back("C");
d.push_front("A"); // ★ 头加(vector 做这个要搬家)
d.push_front("最前面"); // ★ 再来一个
show("两头加之后", d);
// ── ② 两头删 ────────────────────────
d.pop_front(); // ★ 删头(vector 没有这个!)
d.pop_back(); // 删尾
show("两头各删一个后", d);
// ── ③ 随机访问 ──────────────────────
cout << "d[0] = " << d[0] << ",d.at(1) = " << d.at(1) << endl;
cout << "front() = " << d.front() << ",back() = " << d.back() << endl;
// ── ④ 中间操作:和 vector 一样要搬家 ──
d.insert(d.begin() + 1, "插进来");
show("中间插入后", d);
d.erase(d.begin()); // 参数是迭代器
show("erase(begin()) 后", d);
// ── ⑤ 遍历:三种写法同 vector ────────
cout << "范围 for:";
for (const string &s : d) cout << s << " ";
cout << endl;
cout << "size=" << d.size() << ",empty()=" << (d.empty() ? "空" : "非空") << endl;
return 0;
}
看到什么算成功:
两头加之后 [size=4]:最前面 A B C
两头各删一个后 [size=2]:A B
d[0] = A,d.at(1) = B
front() = A,back() = B
中间插入后 [size=3]:A 插进来 B
erase(begin()) 后 [size=2]:插进来 B
范围 for:插进来 B
size=2,empty()=非空
和 vector 的差别只是一张表:
| 操作 | vector |
deque |
|---|---|---|
push_back / pop_back |
✅ O(1) | ✅ O(1) |
push_front / pop_front |
❌ 根本没有这两个函数 | ✅ O(1) |
[] / at / 迭代器 |
✅ | ✅(略慢一点) |
insert / erase 中间 |
O(n) | O(n) |
| 迭代器失效(重要) | 扩容/中间增删后大批失效 | 中间增删后全体失效;两端增删只失效被删的那个 |
有没有 capacity() / reserve() |
有 | 没有(它不需要"一个大块",所以这两个概念没意义) |
| 和裸指针的关系 | &v[0] 能当数组用(连续) |
不能当一整块数组用 |
注意最后两行:这是
deque与vector的本质分界—— ①deque没有capacity()和reserve():它不靠"一整块",所以"预留容量"这个动作它不需要。 ②deque的数据不是一整块:所以别指望deque能和 C 风格接口对接(那些老函数要的是"一个连续数组")。要跟老代码/系统调用对接时,vector才对。
四、动手 Demo:一个"双向排队"的售票系统
任务:模拟两个入口的排队。前门排的人先服务(FIFO 队列),后门来的人可以插到队头(VIP)。
#include <iostream>
#include <deque>
#include <string>
using namespace std;
int main() {
deque<string> line;
// 普通顾客从后门来(排在队尾)
line.push_back("顾客甲");
line.push_back("顾客乙");
line.push_back("顾客丙");
// VIP 从特别通道来(插到队头)
line.push_front("VIP·张总");
cout << "当前排队(从队头到队尾):";
for (const string &s : line) cout << s << " ";
cout << endl;
// 开始服务:总是服务队头
cout << "-- 开始服务 --" << endl;
while (!line.empty()) {
cout << "正在服务:" << line.front() << "(剩下 " << line.size() - 1 << " 人)" << endl;
line.pop_front();
}
cout << "全部服务完毕,empty()=" << (line.empty() ? "true" : "false") << endl;
cout << "回头看:queue 的完整形态就是 deque 的两端操作 + 只许从一头进出。" << endl;
return 0;
}
看到什么算成功:
当前排队(从队头到队尾):VIP·张总 顾客甲 顾客乙 顾客丙
-- 开始服务 --
正在服务:VIP·张总(剩下 3 人)
正在服务:顾客甲(剩下 2 人)
正在服务:顾客乙(剩下 1 人)
正在服务:顾客丙(剩下 0 人)
全部服务完毕,empty()=true
回头看:queue 的完整形态就是 deque 的两端操作 + 只许从一头进出。
"只许从一头进、另一头出"就是数据结构里的队列(queue)——用 deque 做它是最自然的(标准库的 std::queue 默认就是拿 deque 当底层的)。"只许一头进出"就是栈(stack)——那个默认用 vector 或在 deque 上限制也行。
五、什么时候用 deque:一张决策表
| 你的需求 | 选谁 | 原因 |
|---|---|---|
| 只在尾部加、经常按下标读 | vector |
连续内存,缓存友好,最快 |
要用 C 风格接口(传 T*) |
vector |
&v[0] 是连续数组;deque 不是 |
| 要预知容量、性能敏感 | vector |
它才有 reserve |
| 头和尾都频繁进出 | deque |
两端都是 O(1) |
| 用它做队列/双端队列 | deque |
天生的 |
| 要在中间频繁插入删除 | 都不是,选 list |
见 13.1.3 |
几个现实里的 deque 用例:
- 滑动窗口(比如"最近 10 条消息")—— 加新的在尾、淘汰旧的在头;
- 任务队列(一边收任务、一边发任务);
- BFS 广度优先搜索(图算法里要的"队列",就是它);
- 撤销/重做(undo/redo) —— 两头的操作栈。
标准库的一个冷知识:
std::stack(栈)和std::queue(队列)默认的底层容器都是deque(栈也可以指定用vector)。"容器适配器"就是"拿一个容器,只暴露一部分操作"——就像上面 Demo 里我们手写的那样。
30 秒实验
- 把 13.1.1 的实验反过来做:把本节开头的对比程序里
deque改成vector的push_back(尾插)→ 两者就差不多了(尾插对两个容器都是 O(1))。说明deque的优势只在"头"。 - 验证"中间还是慢":把
d.push_front(i)改成d.insert(d.begin() + d.size() / 2, i)(往正中间插)→ 时间回到秒级。中间插入对任何容器都一样慢(除了list)。 - 试
capacity:写一句d.capacity()→ 编译不过('class std::deque<int>' has no member named 'capacity')。这是deque与vector最直接的语法差异。 - 试
sort:sort(d.begin(), d.end());→ 能编译能运行(deque的迭代器是随机访问的,满足sort的要求)。对比 13.1.3 的list,那里这就编译不过。
常见误区
- "
deque就是加强版vector,那就都用deque"——不是。vector有一整块连续内存,所以遍历时的缓存命中率高(CPU 预取友好),实战里vector遍历通常比deque快。默认还是vector;确实两头都动才换deque。 - "
deque也不能和 C 数组对接?可是我写过&d[0]啊"——&d[0]语法上能编译,但语义是错的:它只指向第一块的开头,&d[0] + 100很可能跑到别的块去了。要连续内存,请用vector。 - "
deque也有reserve"——没有。deque没有capacity/reserve/shrink_to_fit。 - "
push_front比push_back慢"——基本一样(都是 O(1)),只是头部要挂新块的概率稍高,实际几乎无感。 - "
deque的内存一定比vector省"——相反,通常略多:它要额外维护那张块目录,而且每块末尾常常有没用上的空隙。空间换时间,老规矩。 - "迭代器失效的规则和
vector一样"——不一样,要分开记: vector:扩容时全部失效;push_back不安全时也可能全部失效。deque:中间insert/erase→ 全部失效;两端push/pop→ 只失效被删的那个指针(其他迭代器仍然有效,但end()可能变)。 记这两条的目的很实际:删元素时别继续用旧的迭代器。稳妥做法:it = v.erase(it)(用返回值)。
自测三问(点开对答案)
1. **`deque` 比 `vector` 强在哪?弱在哪?**——强:**两端插入删除都是 O(1)**(`vector` 头插是 O(n),差三个数量级)。弱:① **数据不是一整块**,不能当连续数组用(`&d[0]+n` 是错的)、不能对接 C 接口;② **按下标访问要多查一次目录**,遍历时缓存不如 `vector`;③ **没有 `capacity` / `reserve`**。 2. **`deque` 中间插入还是慢,为什么?**——因为**块内部的元素仍然排在一起**,中间插一个,那块里的后半截还是要往后挪。**要中间也快,只有链表 `list`(13.1.3)。** 3. **标准库里 `stack` 和 `queue` 的底层默认是什么?为什么?**——**都是 `deque`。** 因为栈要"一头进出"、队列要"一头进一头出"——**正好都是 `deque` 的两端操作**。这类"拿个容器、只暴露部分操作"的东西叫**容器适配器**。- ☐ 下一节(13.1.3 链表类模板)