ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

C++ std::deque 双端队列详解:内部原理、迭代器机制与性能边界

C++ std::deque 双端队列详解:内部原理、迭代器机制与性能边界

先说个我自己的真实经历。前阵子在一个生产者-消费者模型的数据管道里,我要频繁往队列尾部追加日志、同时从头部取出待处理的任务,一开始偷懒直接用std::vector,头部弹出用erase(begin()),结果压测一上来吞吐直接崩了,O(n) 的搬移把性能吃了个干净。改成std::deque之后,头尾操作都是常数时间,整个链路一下子就顺了。那次之后我对这个容器的态度就从“哦,就是双端都能插的 vector”变成了“它是有着独立内存策略和迭代器结构的重要工具,值得单独花时间研究”。

std::deque,全称 double-ended queue,中文一般叫双端队列,是 C++ 标准库容器家族里最容易被低估的一个。它做得好的地方很明确:头尾两端都能在均摊常数时间内插入和删除,同时又保留了按下标随机访问的能力。这个组合,在vector和list身上是找不到的。这篇文章我想从设计思路、内部原理、迭代器机制、性能边界和常见误用这五个维度把它讲透,适合下面这样的读者:知道deque基本用法、但说不清它为什么快的人;准备在项目里挑选容器、想做对选型的人;以及想深入理解 STL 实现细节、将来面对面试题或性能排查时心里有底的人。

1. 先搞清楚 deque 到底适合干什么

1.1 双端操作 + 随机访问,这个组合很特别

很多人对deque的第一印象就是“能在头部push_front的vector”。这句话一半对一半错。vector在头部插入是 O(n) 的灾难,deque在头部插入是 O(1) 的得意技,这一点确实不同。但deque不仅仅是把vector的头部插口补上了,它的整体存储模型、迭代器结构、扩容策略都与vector有本质区别。最直观的差异在内存布局上:vector保证所有元素在一整块连续内存里,而deque并不保证连续,它只是把若干段连续内存通过一张“容器内部的地图”组织起来。这个特点带来一个直接后果:deque虽然没有list那样完全的离散化,但它也不能像vector那样把超大数据一块儿映射进 CPU 缓存里连续扫荡。所以你在使用deque前,最好先想清楚:你是冲着它双端操作去的,还是冲着随机访问去的?如果只是想要随机访问,vector永远更快;如果只是想要头尾插入,deque会让代码变得干净;如果你是需要回溯式访问的队列场景,deque几乎是标准答案。

举例来说,撤销/重做历史记录、浏览器前进后退、任务调度队列、最近消息列表,这类“一端进一端出或者两端都可能进出的数据流”就是deque的主场。你会频繁追加、频繁消费、偶尔需要回头看一眼第几个元素,但不会在中间位置反复增删。这种模式下,deque的均摊 O(1) 头尾操作能跑得极其舒服,而vector的头部操作让人头疼,list虽然头尾也快但随机访问和缓存局部性又不行。选容器的本质就是选“你最频繁的那个操作”的复杂度,deque不是万能钥匙,但它把双端操作和随机访问两个需求同时装进了一个容器里。

1.2 与 vector、list 的定位对比

我把这个对比做成了一个表格,你在选型时可以直接参照:

操作/特性vectordequelist
尾部插入/删除O(1) 均摊O(1)O(1)
头部插入/删除O(n)(需搬移)O(1)O(1)
中间插入/删除O(n)O(n)O(1)(已知位置)
随机访问O(1),常数极小O(1),常数为 vector 的 2-4 倍O(n)
内存布局连续单块分段连续每个节点独立分配
扩容开销搬运所有元素只搬“块指针”,不搬元素无
缓存友好度极高中等偏高低
迭代器失效规则插入/删除均可能使迭代器失效头尾插入只失效迭代器不失引用,中间操作全失效插入不失,删除被删节点的迭代器失效

从这个表格能看出,deque处在一种“既不是最快的随机访问容器,也不是最灵活的插入容器”的中间位置。但它强在“综合能力强”:你最常用的操作如果集中在两端,那么deque的组合优势就非常明显。比如你要维护一个窗口,窗口右端不断进入新元素、左端不断淘汰旧元素,这时vector做不到高效,list做不到随机访问,deque两边都能搭上。这也是我在滑动窗口类算法题里几乎默认使用deque的原因。

2. 内部实现:一个中控器握着多段连续内存

2.1 分段连续存储与缓冲区(block)

std::deque在标准里只规定接口和复杂度,不强制规定实现方式,但在主流编译器的标准库里,它普遍采用“分段连续存储 + 中控器”的架构。简单说,deque里没有一个长度为 N 的大数组,而是分配了若干个固定大小的“块”(通常称为 block 或 buffer),每个块内部是连续存储的一片区域,块与块之间不需要相邻。元素从哪个块哪个位置放起,由内部的映射结构来记录。

这带来一个有意思的结论:deque的“连续性”是局部的。你在遍历一个deque时,前几个元素可能在一块内存里连续排列,走到块边界后就“跳”到另一块内存去了。如果用指针遍历,不能简单地把begin()指向的指针不断++就完事,必须由迭代器自己感知当前块边界、主动切换到下一块。这也是deque迭代器设计比vector复杂得多的根源。

每个 block 到底装多少个元素,标准库实现的常见做法与元素类型大小有关。以某主流实现为例,block 的默认字节数是 512B,单个 block 的元素数量大致是512 / sizeof(T),如果sizeof(T)大于 512,那么一个 block 里只放一个元素。这个设计是为了平衡内存利用率和分配次数:元素小就每个块多装一些,减少分配开销;元素大就给每个元素足够独立的存储块,避免浪费。你在实际项目中如果观察到deque的内存占用比vector高一些,请不要惊讶,这是块边界、中控器和迭代器尺寸共同造成的开销。

2.2 中控器 map 与扩容逻辑

把各个 block 串起来的核心结构是一张指针表,标准库内部一般称它为 map(中控器映射表)。它本质上是一个动态数组,里面每个元素是一个T*,指向某个 block 的首地址。当deque需要新增一个 block 时,会在中控器末尾或开头放入一个新的指针。这么做的巧思在于:扩容时完全不需要动已有的元素数据,只动中控器这张“指针表”。

更具体的步骤是这样:头插元素时,如果当前头部所在 block 的前端已经满了,就新申请一个 block,把它的指针放到中控器的某个位置;尾插逻辑对称。如果中控器本身的容量不够了,比如头尾两个方向一直往两端扩展,导致中控器数组放不下新的指针了,这时候才需要给中控器“翻新”:分配一块更大的指针数组,把原来所有 block 的指针搬过去。注意,这里搬的只是几个字节的指针,不是上万个元素。所以deque的扩容代价远小于vector,后者扩容要移动全部元素。

我可以用一个生活中的场景来类比:假设你开了一家仓库,vector的方式是先把所有货物整齐摆在一整条货架上,货架不够长了,就得在旁边再租一整条更长的货架,然后把货全部搬过去;deque的方式是你买了若干个小仓库(block),每个仓库放一批货,然后在办公室挂一张“仓库索引表”(map),仓库不够用了就再租一个小仓库,只要更新索引表就行,仓库里的货完全不用动。这就是deque头尾插入均摊 O(1) 的根本来源——绝大多数情况下,你只是往两端补充新仓库,旧货物永远原地不动。

2.3 为什么 deque 做不到“绝对的连续随机访问”

既然deque有随机访问能力,为什么它的operator[]总是比vector慢?原因就在这里:vector的随机访问可以简化为一次寻址,base + index * sizeof(T)直接算出地址;而deque的随机访问必须经过两步:先通过下标定位到哪个 block,再定位到 block 内的哪个位置。如果用中控器来实现,通常的做法是先做除法找到块号,取块首地址,再做取模找到块内偏移。除法指令本身就比加法贵,再加上一次指针跳转,速度自然就下来了。

实际测试时,deque的随机访问耗时通常是vector的 2 到 3 倍左右,数据量越大差距越明显。如果你在一个热点循环里疯狂通过下标访问deque元素,那它就是性能瓶颈。这里面有个很常见的误用:有人把deque当vector用,全程dq[i]遍历,结果发现怎么比vector慢那么多。正确的做法是:如果遍历频率极高且不需要两端操作,直接用vector;如果必须用deque,遍历时尽量用迭代器而不是下标,因为迭代器可以维持“当前块地址 + 当前块剩余元素数”的局部状态,跨块时才做一次块切换,减少取模运算的重复开销。

2.4 容量增长策略与内存碎片

deque的块大小设计还有一个隐藏价值:降低内存碎片和分配频率。想象一下,如果你用list,每插入一个元素就要做一次动态分配,每个节点还额外携带两个指针,内存碎片率和分配次数都会随元素数量线性增长。而deque一般来说,每填满一个 block 才做一次分配,block 里能放很多个元素,所以分配次数显著减少。这也是很多实际系统在处理大量短小元素时更喜欢deque而不是list的原因之一。

但也要注意,deque的内存占用与vector相比没有优势,某些极端情况下甚至更大。第一,block 边界可能因为元素大小和块大小不整除而产生少量空隙;第二,中控器本身要额外存储指针;第三,每个迭代器包含多个指针(下面会细说),如果你存放了很多deque的迭代器,内存开销不可忽视。如果你在内存受限的嵌入式环境里跑,并且元素总数是固定的、不需要两端扩展,那么vector或原生数组会更合适。

3. deque 的迭代器:那个著名的“四指针”结构

3.1 一次迭代如何跨块

要真正理解deque的性能和失效行为,绕不开它的迭代器实现。不同标准库的细节略有差异,但核心思想大同小异:deque迭代器一般由四个指针组成,分别记为cur、first、last、node。

  • cur指向迭代器当前位置的元素
  • first和last是迭代器当前所在 block 的首尾边界
  • node指向中控器(map)中指向当前 block 的那个指针

当迭代器执行++操作时,先把cur往后挪一格,然后检查cur是否已经超过了last。如果还没超,说明当前 block 内部还有空间,直接返回;如果cur等于last,说明当前 block 已经走到头了,这时要通过node拿到中控器里的下一个 block 指针,让first、last、node全部更新到新 block 上,再把cur指向新 block 的第一个元素。这个动作就是所谓的“跨界跳转”。因为这个跳转依赖分支判断,deque的遍历效率天然低于vector——不过在现代 CPU 分支预测的加持下,只要边界检查模式规律,实际开销不会特别夸张。

这个设计还解释了另一个细节:为什么deque的迭代器不是简单的T*,而是一个比较胖的自定义类型。sizeof(std::deque<int>::iterator)在常见的 64 位实现里通常是 4 个指针的大小,也就是 32 字节,比vector迭代器的 8 字节大得多。如果你在代码里大量存储迭代器,比如一个vector<deque<int>::iterator>,内存开销就要心里有数。

3.2 迭代器失效规则:和 vector 的微妙区别

deque的迭代器失效规则和vector有很大不同,这是我见过最多人踩坑的地方。

vector在push_back触发扩容时,所有迭代器、指针、引用全部失效,因为元素整体搬到了新地址。deque呢?头插和尾插时,元素本身不搬移,所以你持有的指向已有元素的指针和引用依然有效。但是,如果这个插入恰好导致中控器扩容,那中控器数组的地址就变了,可迭代器内部保存的node指针还是指向旧中控器的某个位置,此时所有迭代器都会失效。注意标准库文档里,push_back/push_front对deque的描述是:所有迭代器失效,但引用和指向元素的指针不受影响。这是一个非常微妙、容易写错语境的规则。记住一句话:deque的头尾插入保留“指向元素的引用/指针”,但不保证“定位元素的迭代器”继续可用。

删除操作就更严格了。中间位置的erase会使所有迭代器和引用失效,因为删除中间元素往往伴随着同一 block 内元素的搬移以填补空洞。pop_back和pop_front则只使被删除元素的迭代器和引用失效,其他位置的迭代器不受影响。这些规则拼起来可以看到一个清晰的逻辑:deque牺牲了“插入时迭代器稳定”,换来了“头尾插入时引用稳定”。如果你的代码里某个模块长时间保存着一个deque元素的地址,在这个容器上执行头尾插入是安全的;但如果你保存的是迭代器,那就要打上大大的问号。

为了把这个问题说清楚,我用一个具体例子展示错误用法:

#include <deque> #include <iostream> int main() { std::deque<int> dq = {1, 2, 3, 4, 5}; auto it = dq.begin() + 2; // 指向元素 3 dq.push_back(6); // 可能触发中控器扩容 // 这里再使用 it 就是未定义行为 // std::cout << *it << std::endl; // 不要这么做 std::cout << dq[2] << std::endl; // 用下标就没问题 }

你如果需要一个长期稳定的定位方式,又想保留双端操作的性能,可以用下标或std::distance换算成位置值,而不是保存迭代器。下标本质上是隐式地每次重新计算地址,绕开了迭代器缓存失效的问题。

4. 性能实测边界:什么时候 deque 是真正的赢家

4.1 一个简单的对比实验

我这里给一个可以直接复现的经验型测试思路。准备三组操作:第一组做 100 万次push_front,分别用vector和deque来跑;第二组做 100 万次随机访问;第三组做 100 万次“队列式”的交替 push 和 pop(类似先进先出的流水线)。

用vector做 100 万次push_front,即使不严谨也能直观感受到卡顿:因为每次都要把现有元素整体往后挪一个位置,总移动次数接近 50 万亿次元素搬运量级。deque做同样操作,只是偶尔需要申请新 block,整体耗时几乎可以忽略。这个差异不用精确计时,光是感官就已经非常明确。随机访问的对比则反过来:vector完胜,deque在大量operator[]场景下会出现肉眼可见的时间损耗。第三组队列模拟,如果vector用erase(begin())来做头部淘汰,那结果依然是灾难;如果换用环形缓冲区或者deque,性能会好很多。

我把常见测试结果的经验值总结一下(不同平台、不同元素类型会有浮动):deque头插速度是vector的几十倍甚至上百倍,deque随机访问速度是vector的一半甚至更低,deque尾部追加速度与vector的均摊尾部插入相比则各有胜负——deque不需要搬运已有元素,但每次触发新 block 分配时有额外开销;vector尾部插入通常更快,但在边缘场景(比如大量 push_back 触发多次扩容)会突然卡顿。

4.2 当心“看似能用的中间插入”

很多人看到deque有insert接口就觉得它也能胜任中间插入,这是一个误区。deque的insert在中间位置的时间复杂度是 O(n) 的,因为它必须搬移元素来腾出位置。虽然它可能比vector的中间插入略有优势(只搬移部分 block 的元素,而不必搬全部),但它绝对不是list那种“断开重连”的 O(1) 操作。如果程序核心逻辑是在有序序列的中间频繁插入,应该用list或其他树形结构;如果插入规模不大、频率很低,deque的中间插入也不会造成灾难性的问题,毕竟它的 n 被拆分到了多个 block 内。

我遇到过一个案例:某同学写了一个排行榜功能,分数更新的频率极高,每次更新都需要把玩家从排行榜中间位置移到新的位置。他用了deque+ 二分查找,看起来挺合理,但线上压测就是慢。后来定位下来,瓶颈就是那个“从中间 erase + insert”的操作,数据量上万之后每次移动的代价叠加让耗时爆炸。换成了list之后,定位靠索引辅助、更新靠节点指针操作,才发现容器选型对架构的影响能大到这种程度。所以,deque适合的是“中间几乎不动、两端持续进出”的场景,这个边界一定要记清楚。

4.3 缓存友好度与实际遍历速度

deque的块大小设计对它访问速度的影响也值得聊一下。因为deque的块内是连续存储的,遍历时在一段时间内访问的是一段连续内存,所以它的缓存命中率虽然不如vector,但远高于list。在块大小约为 512B 的前提下,一个块可以装几十个int或十几个自定义结构体,一次缓存行加载可以覆盖多个元素。所以,对“中等规模、双端操作频繁”的数据,deque往往比list快得多,又比vector来得灵活。

如果你想把deque的性能再往上顶一顶,可以考虑自定义block映射与元素访问方式:遍历时优先用for (auto &x : dq)这种基于迭代器的写法;实在要用下标,也可以用dq.data()?注意,标准库没有为deque提供data()方法,因为它不存在一块连续内存保证。vector的data()可以拿到底层数组,deque不行,这本身就是它跟vector最重要的“契约差异”。

5. 实操中的坑与排查技巧实录

5.1 坑一:把 deque 当 vector 遍历

最常见的误用就是大规模下标遍历。写循环for (int i = 0; i < dq.size(); ++i)在里面做dq[i] += x,数据量小时感觉不出来,数据量一大性能就暴露了。每次dq[i]都要经历“除法 + 取模 + 两次跳转”,和vector的“一次加法”差距明显。排查这个问题的方式很简单:用perf或std::chrono把循环耗时打出来对比一下,就知道掉了多少坑。实在要在deque里频繁按下标访问,可以先把数据拷贝到vector里面处理完再写回来,如果双端操作不多的话,整体代价反而低。

也有人会想:“我用std::deque是因为要两端扩展,但遍历也很多,有没有两全其美的办法?”有,分治:用deque承接动态两端的写入,定期把deque里快照导出到vector中做重操作。这种思路在很多日志系统中都能见到——写入端用deque,当积累到一定量后批量转成连续数组做索引。

5.2 坑二:长时间持有迭代器

我记得之前有一段画面渲染代码,每帧都要往缓冲队列里塞新数据,另外有个线程会持有一个“当前处理到的位置迭代器”。初期测试没出问题,是因为中控器容量刚好够,没触发 map 扩容;用户数据一上去,push_back 导致中控器扩容,持有迭代器的线程再访问时就崩了。这类崩溃在 Debug 下可能表现为断言失败,在 Release 下则可能直接读到垃圾数据甚至段错误。

遇到这种问题时,建议做两件事。第一,在关键代码路径上尽量不要跨插入操作保存迭代器,改用下标记录位置;第二,如果必须持有迭代器,用std::deque的“[ ]”访问把迭代器重新算出来,保住和容器形态的同步。记住,deque给出的“安全承诺”是:插入不会失效你的元素引用和指针,但不是迭代器。

5.3 坑三:分不清 queue、deque 和 priority_queue

这个问题看起来基础,但在实际沟通中经常出乱子。std::queue是一个容器适配器,默认底层就是deque,但它只提供push/pop/front/back,不支持随机访问。std::priority_queue默认底层是vector,实现的是堆结构,弹出的是最大或最小元素。deque则是一个完整的双端队列容器,具备随机访问和迭代器。你写代码前如果没想清楚自己到底要哪种语义,很容易把 queue 的活交给 deque 干(可以,但接口暴露太多无意义的自由度),或者把 deque 当 queue 用(可以,push_back+pop_front就是队列语义,但代码不够自解释)。

顺带提一个写接口时的小技巧:如果你封装一个消息队列,内部用std::deque<T>作为底层,对外只暴露Push和Pop方法,这是很成熟的做法;内层用 deque 的灵活,外层约束接口的语义,代码既快又干净。

5.4 坑四:把 deque 和“环形缓冲”混为一谈

有人觉得deque内部既然分段,那就天然是环形缓冲区,这也是误解。环形缓冲区是一整块连续内存上用头尾指针“绕圈”,而deque是多个 block 离散拼接。deque的块之间没有物理上的环,只是逻辑上可以通过中控器来回跳转。如果你需要一个极高性能的固定容量 FIFO,自研一个基于环形数组的队列可能会比deque有更好的缓存命中率;如果容量不固定、经常动态伸缩,那deque更省心。性能调优时要有意识地分清这两者。

5.5 一些调试技巧

我平时定位deque相关问题时,有几个顺手的方法可以分享。第一,打印sizeof(std::deque<T>)、sizeof(std::deque<T>::iterator)可以快速估算这个容器在你平台上的基础开销。第二,打印相邻元素的地址,能直观看出 block 切换点:比如某次打印结果连续三个元素地址递增,第四个元素突然跳到另一块地址,这说明你刚好跨越了 block 边界。第三,如果你用的标准库实现支持内部结构可视化(比如调试器窗口能展开 map 映射表),可以直接观察中控器的指针数组膨胀过程,对理解它何时扩容非常有帮助。

还有一个小技巧:在写测试代码时,可以用std::deque<T>::size()和std::deque<T>::capacity()吗?注意,标准库的deque并没有capacity()方法,这跟vector不一样。因为deque不存在单一连续的容量概念,它有很多个 block,每个 block 有自己的容量状态。这个 API 上的缺失也从侧面提醒你,deque的容量模型天然就不是“一个整体”。如果你试图用“预留空间”来优化deque的插入性能,很可能会发现缺少对应的接口,这其实不是缺点,它是容器设计哲学上的自洽。

6. 一个真实场景:用 deque 实现滑动窗口最大值

6.1 场景与思路

滑动窗口最大值是一个很经典的算法题,也是deque最能体现“双端操作 + 随机访问价值”的场景。题目大致是这样:给定一组整数序列和一个固定大小的窗口 k,窗口从最左端滑到最右端,每次滑动一格,要求输出每个窗口内的最大值。

暴力的做法是每移动一次窗口,就扫一遍窗口内的 k 个元素,复杂度 O(nk)。如果数据规模很大,这个开销会很可观。用deque的经典优化思路是:在deque里存放元素的下标,而不是元素值,并且始终保持deque中的下标对应元素是“从队头到队尾单调递减”的。这样窗口每次移动时,队头元素就是当前窗口的最大值。为什么用deque?因为这个过程需要从尾部弹出较小元素(pop_back)、从头部弹出过期元素(pop_front)、从尾部追加新元素(push_back),还要站队头拿到结果(front)。这五种操作恰好是deque的看家本领。

6.2 代码实现与逐行解读

#include <deque> #include <vector> std::vector<int> maxSlidingWindow(const std::vector<int>& nums, int k) { std::vector<int> result; std::deque<int> dq; // 存储下标,保证下标对应的值递减 for (int i = 0; i < (int)nums.size(); ++i) { // 1. 移除已经滑出窗口的下标 if (!dq.empty() && dq.front() == i - k) { dq.pop_front(); } // 2. 从尾部弹出所有不大于当前元素的下标 while (!dq.empty() && nums[dq.back()] <= nums[i]) { dq.pop_back(); } // 3. 当前下标入队 dq.push_back(i); // 4. 窗口完整时,记录窗口最大值 if (i >= k - 1) { result.push_back(nums[dq.front()]); } } return result; }

第一步,dq.front()存储的是窗口左边缘结束后的第一个过期元素下标。如果它等于i - k,说明已经滑出窗口,直接弹出。第二步是单调队列的核心:当前元素nums[i]如果比队尾元素大,说明队尾元素在窗口剩余生命周期内不可能是最大值了(新元素更大且更晚过期),直接弹掉。这个“淘汰”过程保证了队头的元素永远是当前窗口最大值的候选。第三步把新下标放进去。第四步,只有当下标从0走到k-1及之后,窗口才真正形成,此时队头下标对应的值就是窗口最大值。整体复杂度 O(n),每个元素最多入队一次、出队一次,非常干净。

这个例子里的deque完全不可被vector替代(头尾频繁 pop 的开销太大),也不建议用list(需要随机访问nums[dq.front()]且迭代器遍历更慢)。它是我在面试和工程中反复遇到的模板式用法,值得你存下来。

7. 选择 deque 的决策清单

聊了这么多,我把“什么时候该用 deque、什么时候不该用”浓缩成一张决策清单,方便你在架构评审时快速对照。

优先级最高的场景:队列/栈式数据流。比如消息队列、任务队列、日志缓冲、撤消历史,这些数据天然从一端进入、另一端消费,偶尔需要窥探内部元素,选deque是最稳的。它比vector灵活,比list高效,实现成本最低。

可以容忍的场景:需要偶尔的随机访问 + 两端动态扩展。比如一个固定窗口的统计、分页缓存、游戏里的渲染批次缓冲。deque能让你不做“环形缓冲自研”就能获得不错的性能,代码可读性也好。

不适合的场景:中间频繁插入删除。这类操作是deque的软肋,优先list或更适合的其他结构。纯粹的随机访问密集遍历,也是vector的舒适区。如果你发现自己的代码里到处是dq.begin() + position的跨度跳转,就要停下来想想,是不是容器选错了。

另外还有一个工程上的额外考量:ABI 和跨平台行为。不同编译器标准库中deque的 block 大小、中控器实现细节不完全一致,但对外接口语义是一致的。如果你的代码运行在异构环境中,尽量只依赖标准规定的复杂度,不要依赖某个实现内部的 block 大小,否则将来换编译器时行为可能发生变化。这也是一种“面向接口编程”的实践。

在实际项目里,我现在的态度是:默认首选vector,因为它的连续内存和遍历速度对大多数场景都最友好;但只要数据结构天然呈现“一端进、一端出”或者“双端进、双端出”的形态,我就会直接切到deque,而不是拿vector去硬扛。容器选型的功夫不在背复杂度表,而在对数据访问模式有清晰的认识。deque是一个综合能力很强的“慢热型选手”,你用对它,它的价值会非常稳定;用错它,它也会诚实地把性能开销摆在你面前。希望这篇文章能帮你少走一点弯路,至少在下次遇到“双端队列”时,你能比过去的自己多想一层。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进