C++ STL stack与queue底层原理与实战:从deque到单调栈和滑动窗口

发布时间:2026/9/30 4:20:23
C++ STL stack与queue底层原理与实战:从deque到单调栈和滑动窗口 1. 从“会用”到“吃透”stack和queue到底该怎么学先问一个很现实的问题你在刷力扣、刷牛客、写业务代码的时候有多少次只是把std::stack和std::queue当成“装数据的盒子”push进去、pop出来完事如果是这样那这篇拓展学习就是给你看的。stack和queue是C STL里最容易被低估的两个容器适配器。表面上看它们的接口就那五六个函数比vector、deque简单太多但真正想用对、用活、用出性能背后牵扯到的底层机制、算法模型、内存策略和工程权衡远比大多数人想象的深。尤其到了面试环节关于stack和queue的高频题——括号匹配、单调栈、层序遍历、滑动窗口最大值、Top K问题——几乎每一道背后都是这两个适配器的变形和演化。更不用说实际工程中函数调用栈、表达式求值、消息队列、任务调度、图论BFS到处都有它们的影子。这篇文章我不想按官方文档的口吻罗列接口而是想用一线写代码的人视角把stack和queue拆开揉碎底层为什么是deque而不是vectortop()和pop()为什么不合并成一个接口单调栈到底“单调”在哪个维度滑动窗口最大值背后为什么非要用双端队列优先级队列的堆结构和stack、queue又有什么血缘关系不管你是刚学完C基础语法准备进阶、在准备校招社招面试、还是在工程中需要对容器做选型的开发这篇文章都值得你从头到尾读一遍。我尽量用实际代码和踩坑经历说话不会给你那些“面试背诵版”一堆勉强的结论而是把“为什么”讲清楚。2. 底层机制拆解不是容器而是容器适配器2.1 为什么std::stack底层不是vector而是deque先看一个最常见的定义式#include stack std::stackint s;很多人以为stack内部就是动态数组vector毕竟数组天然适合栈的场景尾插尾删都是常数时间。这种理解不算全错但STL的实现者并没有把默认容器定为vector而是选了deque双端队列。这个选择背后的理由值得一提。deque是一个支持在头尾两端都能以常数时间插入和删除的序列容器。它内部事实上是由一块块连续缓冲区拼接而成的“分段连续”结构并用一个中央映射表map管理这些缓冲区。基于这种结构deque能在头部插入时不移动已有元素也不需要像vector那样在扩容时做整体搬移。而stack需要的恰恰只是在一端做push、popdeque的头部操作能力和尾部操作能力都能被stack完整复用。如果默认用vector会发生什么也不是不能用但有两个问题一是vector在扩容时会申请一块更大的内存、把旧元素逐个拷贝或移动过去这个开销虽然均摊下来还是O(1)但单次扩容瞬间会有明显毛刺二是vector只在尾部高效但stack的语义不关心“另一端”适配器照样适合只存在扩容毛刺这一层隐患。deque的分段结构让扩容时不需要移动已有元素只需要在map中挂新块从性能稳定性上更优。除此之外stack还允许你显式指定底层容器比如用std::vector或std::liststd::stackint, std::vectorint s1; std::stackint, std::listint s2;这在某些需要减少内存占用、或者有特殊遍历需求的场景下有用但你得意识到一旦指定为vector它就不再具备deque那种头部操作能力。不过stack只用尾部所以传vector是完全合法的。真正奇怪的写法是把deque换成listlist每个节点单独分配内存节点多了会产生大量小内存碎片性能明显劣于deque我在实际中不推荐。这里还有一层更值得品味的点std::stack作为容器适配器container adaptor它的设计哲学是“约束接口而不是新建底层结构”。stack本身并没有发明新的内存布局它只是把deque的接口阉割成只能从一端进出。同样queue把deque阉割成前端出、后端进priority_queue把vector作为底层存储再加上堆算法。所以学stack和queue本质上是学“如何在一个强大而复杂的容器上用最少的外部接口完成最明确的任务”。这也是STL里适配器模式的经典课。2.2std::queue底层为什么也是dequestd::queue的语义是FIFO先进先出需要头部出队、尾部入队两个操作都要求高效。deque的头尾双向O(1)插入删除能力让它成为queue默认底层容器的自然选择。这里可以做一个对比实验。如果queue底层用vector头部出队时pop_front()会触发所有后续元素向前搬移单次操作时间复杂度直接变成O(n)。用list呢头尾操作确实O(1)但list节点内存分散缓存命中率低。实际测试里在相同数量级数据下list作为底层通常比deque慢一个数量级原因是连续内存遍历时CPU缓存能一次加载一串元素链表则每访问一个节点都可能产生一次缓存未命中。所以STL标准委员会把deque作为queue的默认底层是一个基于性能平均值的权衡结果。如果你的实际场景不需要queue的“天然顺序”而是需要快速随机访问那说明你选错了容器该用vector或deque本身。2.3 存储开小差deque的内存模型和迭代器失效问题想要真正理解stack和queue的底层心智模型deque的内存结构必须清楚deque由若干固定大小的缓冲区buffer构成每块缓冲区内的元素连续存储。缓冲区之间通过一个中控映射表map本质上是一个指针数组串联deque的迭代器需要维护“当前缓冲区指针 当前元素指针 缓冲区首尾边界”三个信息。因为它的元素并不整体连续所以std::deque的迭代器比vector::iterator“重”很多随机访问也相比vector慢一点多了两级跳转。这个结构带来的实际后果主要有两个第一deque的迭代器在插入/删除两端元素时基本不会失效中间插入仍会失效这一点和vector的“任何插入都可能全失效”有巨大差别。所以当你用queue的底层deque实现一个需要同时从两头操作的任务队列时可以更放心地持有迭代器。第二deque的operator[]虽然也是O(1)但常数比vector大。如果有人为了“性能”把deque当vector用频繁按下标访问往往讨不到便宜。我在工程里见过一种典型误用为了要双端操作又能下标访问直接使用deque然后大量dq[i]遍历结果发现性能并没有优于vector加双指针的方案。如果只需要“两端操作 不关心中间随机访问”deque是正解如果需要“随机访问 尾部增删”vector永远是首选。提示stack和queue默认隐藏了底层容器的所有“非适配器接口”所以你在使用时访问不到push_back、push_front这些底层能力看到的只是一组受约束的公共接口。这是适配器模式的最大特点也是最大安全边界。3. 接口细节与易错点真正拉开代码质量的地方stack和queue的接口看起来少得可怜但细节里全是坑。我在面试候选人和code review里经常发现能完整说清下面几个点的人才算真正掌握了这两个容器。3.1top()和pop()为什么不合并成一个接口这是很多初学者会问的经典问题为什么每次从stack中取值要分两步——先top()拿值再pop()删除为什么不像某些语言里的pop()直接返回弹出的元素先说结论合并之后返回值问题会让你付出拷贝代价或面临异常安全风险。如果pop()直接返回元素本身按值返回需要先把元素拷贝/移动构造出临时对象再返回对非平凡类型来说这就是一次多余的开销而且如果拷贝构造抛异常元素已经出栈了但调用方没拿到值数据就丢了。如果返回引用那pop()之后这个引用必然悬垂更加危险。C11之后虽然有了移动语义可以缓解“返回值拷贝”的代价但标准和STL实现仍然保持了top()和pop()分离的设计因为它把“读取”和“删除”两个操作解耦你先通过top()拿到引用可以安全地观察、甚至修改栈顶元素如果非const确定不再需要后再pop()。这给程序员提供了更大的控制力代价就是多写一行代码。顺带说一句queue的front()/back()对应读取头尾pop()只负责删除头部也是同样的分离逻辑。C这种“显式多于隐式”的设计哲学在这里体现得很彻底。3.2 size_t与int的坑循环里的!s.empty()才是正解另一个常见的低级错误是拿int和size()比较或者在循环条件里写for (int i 0; i s.size(); i)这时候编译器的-Wsign-compare警告就会跳出来。stack、queue类里的size_type本质上是size_t无符号类型。如果你写成for (int i 0; i s.size(); i) { ... }在s.size()大于INT_MAX时不溢出才会触发问题但更隐蔽的是当循环里同时push/popsize()实时变化那么“用初始size做循环上界”的逻辑本身就错了。标准做法永远是while (!s.empty()) { auto val s.top(); // 处理 val s.pop(); }我自己在写树的非递归遍历时这几个循环条件写法直接影响代码正确性。无符号整型的边界溢出不是“概率低就不管”的问题而是刷题和工程里一旦数据量上来极难排查的故障源。把empty()当成循环主条件是从源头避坑的好习惯。3.3emplace系列少一次拷贝的现代C写法C11给所有STL容器都增加了emplace系列方法stack有emplacequeue有emplace和emplace_back语义的对等操作。它们的意义是直接在容器内部的内存上构造对象而不是先构造临时对象再拷贝/移动进容器。比如std::stackstd::pairint, std::string s; s.push(std::make_pair(1, hello)); // 需要构造pair临时对象 s.emplace(1, hello); // 直接在stack内部构造pair对pair、tuple、自定义类型的场景emplace不只是语法糖它意味着一次构造函数的实际节省。尤其在栈容器里反复push大量对象时减少一次临时对象构造的意义会被放大。不过要注意emplace不会做隐式转换的某些检查参数不匹配时的报错信息往往很长很晦涩调试时要有心理准备。3.4stack内存增长的另一种思路提前reserve底层容器因为stack默认底层是deque你没有reserve()接口。但如果你明确知道会有大量元素入栈可以换用vector作为底层并提前reservestd::stackint, std::vectorint s; s.reserve(100000); // 这里编译不过stack适配器没有暴露reserve等等stack适配器确实没有暴露reserve因为它只暴露受限接口。想要提前预留容量就得直接操作底层容器std::vectorint v; v.reserve(100000); std::stackint, std::vectorint s(v);这样栈实际使用的是v的底层内存已经预留过空间后续入栈基本不会触发扩容。这个用法在已知数据规模上线的场景很实用。代价是你得“穿透”适配器对底层做一次初始化代码看起来不够“纯面向对象”但在性能敏感环境里是合理的取舍。3.5 queue还要分清front()和back()别从pop()取数据queue的接口语义经常让人混淆入队用push()也就是从尾部进来出队用pop()从头部出去观察队首用front()观察队尾用back()。关键点在于pop()是不返回值的你必须在pop()之前用front()取值。很多从Java或Python转过来的朋友一开始都不习惯Java的Queue.poll()返回队首并移除元素Python的collections.deque.popleft()也返回元素只有C的queue::pop()只负责移除。这不是C故意抬杠而是和top()/pop()分离同样的设计逻辑避免返回值带来的拷贝和异常安全问题。用多了就会发现这种显式分离在链路复杂时反而让人思路清晰。4. stack实战进阶从括号匹配到单调栈4.1 栈的经典应用括号匹配与表达式求值学stack不刷几道经典题等于白学。括号匹配是数据结构课上必讲的题也是最直观展示“栈顶元素对应最近未匹配项”的案例。核心思路是遇到左括号入栈遇到右括号和栈顶匹配则出栈匹配失败或栈提前为空则判定非法。bool isValid(const std::string s) { std::stackchar st; for (char c : s) { if (c ( || c [ || c {) { st.push(c); } else { if (st.empty()) return false; char top st.top(); if ((c ) top ! () || (c ] top ! [) || (c } top ! {)) { return false; } st.pop(); } } return st.empty(); }这里的“为什么用栈而不是计数器”值得深入理解用计数器只能验证左右括号数量相等无法验证顺序和嵌套关系。[(])这种序列用计数器是“合法”的但用栈一扫就能识别出错误。栈天然保存了“历史嵌套顺序”top()永远指向最近的未匹配左括号这种后进先出的组织方式与括号嵌套结构是同一个抽象模型。表达式求值也是同一套思路。中缀表达式转后缀逆波兰表达式时栈用来保存运算符根据优先级决定何时弹出后缀表达式求值时栈用来保存操作数。一个典型的“两个栈”分别是运算符栈和操作数栈配合优先级表就能实现一个简易计算器。如果你自己写过一遍就会彻底明白为什么函数调用底层的调用栈能支持函数嵌套调用——本质上和括号嵌套是同构的。4.2 单调栈找“下一个更大元素”的模板化思维单调栈算是stack最有含金量的扩展应用也是热搜里“单调栈算法c”的高频点。我把它当成所有用栈解题的“升级版思维”。单调栈的含义很简单从栈底到栈顶元素按某种单调性排列递增或递减。它用一种“维护单调性时顺便记录弹出时机”的技巧在线性时间内解决一类“找左右第一个更大/更小元素”的问题。经典题就是“每日温度”或“下一个更大元素”。核心代码模板如下std::vectorint nextGreater(const std::vectorint nums) { int n nums.size(); std::vectorint res(n, -1); std::stackint st; // 存下标 for (int i 0; i n; i) { while (!st.empty() nums[st.top()] nums[i]) { res[st.top()] nums[i]; st.pop(); } st.push(i); } return res; }为什么存下标而不是值因为下标能访问原数组的值还能计算位置差如果只存值就不够信息量。关键在于while循环的弹出条件当前元素比栈顶对应元素大说明当前元素就是栈顶元素的“下一个更大”记录答案后弹出继续比较新栈顶。整个数组扫描一遍每个元素最多入栈一次、出栈一次总时间复杂度O(n)。单调栈还有一个非常经典的进阶应用是“柱状图中最大的矩形”。它需要维护一个递增栈当遇到比栈顶高度小的柱子时以栈顶为矩形高度、左右边界由栈内相邻元素界定计算面积并更新最大值。这道题把“单调性 边界计算”用到极致而且看懂了之后“接雨水”那道题的解法也会迎刃而解。可以说单调栈是stack从“容器”变成“算法思想”的标志性产物。4.3 函数调用栈的工程视角刷题之外栈在工程上最重要的角色是函数调用栈。每次函数调用系统会在调用栈上压入一个栈帧栈帧里保存着局部变量、函数参数、返回地址等内容函数返回时对应弹栈。C里局部变量析构也是逆序的这个“后进先出”的规律和栈本身的属性完全一致。所以当你在vscode里配置C调试环境或者用gdb查看调用栈bt命令时看到的那个函数调用链本质上就是一个活生生的stack实例。理解了栈帧分配、栈空间大小限制通常默认8MB左右Linux下可以用ulimit -s查看就能解释为什么递归层级过深会导致栈溢出Stack Overflow。在实际工程中递归危险、深递归改非递归这种问题都会回到栈这个数据结构。树的非递归中序遍历本质上就是“手动维护调用栈”来模拟系统栈的行为。这种从“使用容器”到“模拟运行时机制”的视角转换是把stack学透的标志。5. queue实战进阶BFS、层序遍历与滑动窗口5.1 BFS统一模板图论中queue的标准用法图的广度优先搜索BFS是queue最经典的应用场景没有之一。如果你只会“用queue把节点存进去拿出来”却不理解BFS为什么必须用queue那你还没吃透。BFS的核心要求是按距离源点的远近分层访问。距离为1的先访问完再访问距离为2的这个“先来先服务”的顺序正好就是FIFO。如果换成栈DFS就近访问新发现的节点就变成了深度优先顺序完全不同。表达“先到先处理”这个语义queue是最精确的数据结构。一个标准的BFS模板我建议牢牢记住void bfs(const std::vectorstd::vectorint graph, int start) { std::queueint q; std::vectorbool visited(graph.size(), false); q.push(start); visited[start] true; while (!q.empty()) { int size q.size(); // 当前层的节点数 for (int i 0; i size; i) { int cur q.front(); q.pop(); // 处理cur for (int next : graph[cur]) { if (!visited[next]) { visited[next] true; q.push(next); } } } // 一层处理完可在此记录层级 } }这里有一个很多人没注意的细节int size q.size()这个变量必须在循环前取因为循环体内pop和push都会改变q.size()如果不提前存下来你无法准确控制“只遍历当前层”。在所有BFS分层问题里比如二叉树自底向上层序遍历、每个节点的层号、最短路径步数这个size取法都是关键。另一个细节是visited标记的时机。应该在节点入队时标记而不是出队时标记。否则同一个节点可能被多个邻接节点同时发现并重复入队造成队列膨胀甚至死循环。这个坑我在初学者代码里见过太多次。5.2 二叉树的层序遍历queue的直接应用二叉树层序遍历几乎是BFS的模板题没有任何多余的花哨std::vectorstd::vectorint levelOrder(TreeNode* root) { std::vectorstd::vectorint res; if (!root) return res; std::queueTreeNode* q; q.push(root); while (!q.empty()) { int levelSize q.size(); std::vectorint level; for (int i 0; i levelSize; i) { TreeNode* node q.front(); q.pop(); level.push_back(node-val); if (node-left) q.push(node-left); if (node-right) q.push(node-right); } res.push_back(level); } return res; }这个代码的季度感在于queue天然保存了二叉树节点的访问顺序配合每层提前记录的levelSize就能把每层元素单独切出来。如果不用queue想保持层序并且分层会更加别扭。许多二叉树题目最大宽度、右视图、之字形遍历都是在这个基础模板上做小改动。5.3 双端队列与单调队列滑动窗口最大值的核心std::queue本身不支持队尾出队、队头入队这类操作。当题目需要“滑动窗口中的最大值”时只靠queue是不够的需要用到它的底层兄弟std::deque也就是双端队列。这也是queue拓展学习中值得单开一节的原因。滑动窗口最大值问题给定一个数组和窗口大小k输出每个窗口内的最大值。朴素做法是每个窗口扫描k个元素总复杂度O(nk)大数据量下直接超时。优化思路是维护一个“窗口内元素下标”的双端队列并让队列中元素对应的值保持严格递减队头是最大值候选std::vectorint maxSlidingWindow(const std::vectorint nums, int k) { std::vectorint res; std::dequeint dq; // 保存下标 for (int i 0; i nums.size(); i) { // 移除不在窗口内的下标 while (!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而不是queue因为滑动窗口在移动时过期的窗口元素需要从队头删除这个操作是queue天然支持的pop_front但为了维护单调性新元素从队尾进来时要把前面所有比它小的元素都从队尾删掉这个动作只有deque的双端操作能力才能做到。单调队列把“窗口移动”和“极值维护”两个动作合在了一起每个元素同样最多进出各一次总复杂度O(n)。如果只盯着std::queue的受限接口是绝对写不出这个解的。这也再次说明只看适配器暴露的接口是远远不够的必须理解它背后的底层容器能力才能实现算法的各种变形。5.4 工程里的queue消息队列与任务队列在业务代码里queue最常见的用途是解耦生产者和消费者。生产者把任务放进队列消费者从队列另一头取任务不用直接耦合调用。C标准库里std::queue本身不是线程安全的所以多线程下要加锁或者使用线程安全队列封装。我在实际工作中用std::queue写过一个简单的日志异步写入模块主线程把日志消息push进队列后台线程从队列里pop出来批量写入文件。这个场景对顺序有严格要求FIFO正好符合日志的时间序。踩过的坑有两个一是队列为空时消费者必须等待不能用忙循环空转后来接入了条件变量cv.wait二是队列里存的值对象需要用移动语义std::move传入避免拷贝大字符串。这种实践不需要高深算法但能验证你对queue语义和实用边界的理解。面试时如果能讲出“为什么日志顺序不能乱”“队列积压怎么办背压/限流”比背十道题的模板更能获得面试官认可。6. priority_queuequeue家族里的“异类”6.1 堆与优先队列的关系严格来说std::priority_queue也是一个容器适配器但它不在FIFO语义的queue家族里。它的弹出顺序不是“先入先出”而是“优先级高的先出”。底层容器默认是vector配合std::push_heap和std::pop_heap算法在逻辑上构建出一个最大堆大顶堆。如果你想选一个“每次都能高效取最大值”的数据结构优先队列是标准答案。它保证push和pop的时间复杂度均为O(log n)top()则是O(1)。这种特性让它成为Top K问题、堆排序、Dijkstra最短路径算法中的核心工具。为什么它属于queue因为语义上它还是“从尾部入、从头部出”的受限操作只不过头部永远是“优先级最高的元素”。这里体现的适配器思想非常透彻限制接口 换一种堆策略就把一个双端队列变成了完全不同的数据结构。6.2 自定义比较器的正确写法priority_queue最大的坑在于自定义比较器。默认是大顶堆top()返回最大值。如果你想要小顶堆两个办法// 方法一直接用 greater std::priority_queueint, std::vectorint, std::greaterint minHeap; // 方法二自定义仿函数 struct Cmp { bool operator()(int a, int b) const { return a b; } }; std::priority_queueint, std::vectorint, Cmp minHeap2;注意在priority_queue的比较器里返回true意味着“a的优先级低于b”也就是a应该下沉、b先出来。这和我们惯用的sort比较器返回a b表示a排在b前语义相反。多数初学者的困惑都来自这里想要小顶堆却在比较器里写return a b结果实现成了大顶堆。我的一个习惯是用lambda时把比较器类型传给模板比如C14以后可以直接写成auto cmp [](int a, int b) { return a b; }; std::priority_queueint, std::vectorint, decltype(cmp) pq(cmp);C20可以用std::priority_queue的推导指引进一步简化但原理不变。关键是理解“比较器返回true表示前者优先级更低”建立这个心智模型后自定义类型也可以直接写例如按pair的第二关键字排序struct PairCmp { bool operator()(const std::pairint,int a, const std::pairint,int b) const { return a.second b.second; // 大顶堆队列先出second最大的 } };6.3 Top K与堆选择面试里“求数组前K个最大元素”这类题优先队列的解法本质上就是维护一个大小为K的小顶堆堆顶是当前第K大的元素。数组遍历时如果元素比堆顶大就替换堆顶并调整遍历完堆里恰好就是前K个最大的。复杂度O(n log K)比全排序O(n log n)更优尤其在K远小于n时优势明显。这个“只有K个坑位的堆”的思路和直接对全量排序最大的区别在于空间占用。在数据量巨大甚至无法一次性读入内存的场景比如海量日志里找topN堆只需要K个元素的内存这是用空间换时间的完美例证。优先队列把堆算法封装得这么好以至于你几乎不需要自己实现堆但如果你不懂二叉堆的下滤、上滤过程面试问“为什么建堆复杂度是O(n)”你还是会卡住。所以延伸学习std::make_heap、push_heap、pop_heap这三个算法对理解priority_queue有釜底抽薪的效果。7. 面试高频题与避坑速查7.1 常考基础问题与回答逻辑关于stack和queue面试里最常出现的问题就那么几类我把它们整理成一张表方便你做最后冲刺问题考察点回答要点stack和queue底层默认是什么STL实现细节都是deque原因在于deque头尾O(1)和扩容不搬移deque和vector有什么区别内存布局deque分段连续中控表管理缓冲区块vector单块连续capacity耗尽时整体搬移为什么top()和pop()分离异常安全与效率返回值会造成多余拷贝返回引用则pop后悬垂分离可以让调用方精确控制生命周期为什么stack编译期不能直接访问底层适配器模式适配器暴露受限接口约束语义queue和deque该选哪个接口与数据结构的权衡只需要FIFO用queue需要双端操作或随机访问用dequepriority_queue默认是最大堆还是最小堆默认比较器大顶堆小顶堆需传greater或自定义比较器单调栈的单调性指什么算法进阶栈内元素按值递增/递减排列弹出时记录答案多用于下一个更大/更小元素滑动窗口最大值为什么用单调队列算法进阶每个元素只进出一次O(1)均摊维护窗口极值栈溢出是什么怎么避免工程意识递归过深占满调用栈可改循环、增大栈空间、削减单帧存储这表里的内容比网上那些“百题速成”实在得多因为我从面试官角度告诉你回答“是什么”只是60分能解释“为什么这样设计”才是80分以上。7.2 工程中常见问题排查一览实际开发中stack和queue相关的bug大多数集中在几个点我做成一个速查表症状可能原因解法编译报错C2678或模板实例化失败自定义类型没有正确的比较运算符或比较器写错检查operator或自定义比较器语义priority_queue弹出的是最小值而非最大值比较器方向写反确认按“返回true表示优先级更低”理解无限循环BFS里visited在出队时才标记导致重复入队改为入队时标记pop()后仍访问top()忘了调用pop()后栈为空直接top()先empty()判断再访问程序崩溃且栈很近递归层数过深导致栈溢出改用循环或用线程栈更大方式大量push时偶发抖动底层容器扩容用vector作为底层并预先reserve见3.4节queue中对象被拷来拷去开销大值拷贝而非移动push(std::move(obj))或用emplace直接构造这表里的好些坑我都亲自踩过。比如用priority_queue写Dijkstra时为了优化曾经把大根堆的答案取反当成小根堆结果在边界条件处偶尔出错后来改成greater后一劳永逸。这种经验不写在代码注释里写在这里希望能帮你少交点学费。8. 用“为何”代替“是什么”一点个人体会写到这里我不打算做那种提纲挈领的总结了。只想说一个我在实际学习和带人的过程中越来越强烈感受stack和queue真正难的地方不在于接口有多少、代码有多长而在于你能不能从“背模板”走向“懂原理”。一旦理解了适配器模式、理解了deque的分段连续内存、理解了为什么要分离读取和删除你就能在面试和工程里举一反三。最后分享一个小技巧在vscode里配置C开发环境的时候我建议你直接把STL源码加入到includePath里平时写代码跟踪进去看看stack、deque的源码实现。看一遍priority_queue的底层堆调用胜过背十篇博客总结。C是一个语言和标准库都值得较真的方向stack和queue只是你深入探索的一个起点把这个起点走扎实后面的map、set、unordered系列、allocator、coroutine这些硬骨头你也会更有底气。