Theory · Data Structure · Stack & Queue

栈与队列

受限的线性表:LIFO 与 FIFO 两条纪律 · 环形队列 · 单调栈与单调队列 —— 从括号匹配到滑动窗口最大值

栈 LIFO

后进先出:函数调用、括号匹配、DFS、撤销——"最近的先处理"

队列 FIFO

先进先出:BFS、任务调度、消息队列——"先来的先处理"

单调化

栈/队列装上"单调性"约束,从 O(n²) 暴力降到 O(n)——面试中级题钥匙

定位:栈和队列是"加了纪律的线性表",重点不在结构本身而在两条纪律适配的场景;后半场的单调栈/单调队列是把暴力降维的技巧,中级题必考。

Why It Matters

先看两件小事:撤销键与打印机,为什么不能用同一个清单

事件一:你按了 5 次编辑,然后按 Ctrl+Z

你期待撤销的是刚才那一次。如果把编辑记录存进一个"先来先取"的清单,撤销键会去撤第 1 次编辑——功能直接是错的。撤销必须后进先出

事件二:三个同事同时点了打印

如果打印机也"后进先出",最先提交的人会一直被后来者插队、永远打不出来。打印必须先进先出

关键观察:顺序不是装饰,是正确性

两件事用的都是"一个待处理清单",差别只在从哪一端取。取错端不是慢一点,而是功能错——把 BFS 的队列换成栈,最短路的答案就不对了。所以这两条纪律要单独命名、单独学:栈(LIFO)队列(FIFO)

为什么它们值得一整篇

① 它们是出现频率最高的两个结构:函数调用、递归、DFS、括号匹配全是栈;BFS、线程池、消息队列全是队列;② 它们还能被改造成算法武器——加一条"内部保持单调"的规矩就能把暴力降一个数量级,见右侧。

一个把人叫醒的数字:给 10 万个温度,问每个位置"下一个更高的温度在几天后"。
· 朴素做法:对每个位置往后找 → 最坏 105×105 = 约 100 亿次比较,跑几十秒;
· 单调栈:每个下标最多进栈一次、出栈一次 → 约 20 万次操作,毫秒级。
差距不来自"代码写得更快",而来自一个结构约束:让栈里的元素保持单调,就能把"还没找到答案的候选"全都攒在一起批量结算
本 deck 路线:① 两条纪律与它们的物理实现(数组尾部 / 环形取模);② 各自的应用清单(凡"最近的先处理"是栈,凡"先来的先服务"是队列);③ 单调栈与单调队列——把容器变成筛选器;④ 落回工程:Go channel 其实就是"加了锁和等待队列的环形队列"。
读完你应该能回答:某个需求该用栈还是队列(看"最近"还是"最先")、环形队列怎么判空判满、以及看到"下一个更大/窗口最值"时为什么条件反射想到单调结构。
动机页两层:第一层用撤销键/打印机说明"取哪端"是正确性问题(不是性能问题);第二层用 100 亿次 vs 20 万次的数字给出单调结构的存在理由,为第 9-11 页铺路。全页不出现任何结构定义。

Prerequisites & Glossary

先把词认全:下面每一页都会用到它们

术语一句话理解(细节后面展开)
LIFO 后进先出Last In First Out:最后放进去的最先拿出来——一叠盘子
FIFO 先进先出First In First Out:最先放进去的最先拿出来——排队买饭
栈顶 top栈唯一的出入口;push 压入、pop 弹出、peek 只看不取
队头 / 队尾队列的出口 head / 入口 tail;enqueue 从队尾进、dequeue 从队头出
取模 %除法取余数;(i+1)%n 让下标走到末尾自动回到 0,是"成环"的全部魔法
假溢出数组队列出队只挪头指针,前面空着却报"满了"——环形队列就是来治它的
背压 backpressure队列满了以后怎么办:阻塞生产者 / 丢弃 / 拒绝,属于业务决定的语义
均摊 amortized把偶发的大开销摊到多次操作上算平均值,得到"每次成本"
单调一列数只增不减(或只减不增);本 deck 指"容器内部始终保持有序"
双端队列 deque两端都能进出的容器;只用一端即栈,两端分工即队列

前置知识:不确定就先看这两篇

数组与链表 → 栈和队列都要"住"在其中之一里,本 deck 默认你知道两者代价
复杂度分析 → O(1)/O(n) 怎么读,"均摊"到底摊的是什么

最小心智模型(记住这一句)

栈和队列都是受限的线性表——限制只落在一处:允许从哪一端进、从哪一端出
· 同一端进出 = ("最近的先处理");
· 一端进、另一端出 = 队列("先来的先服务");
· 两端都能进出 = 双端队列(前两者的超集)。

再加一条规矩,它就变成算法武器

在上面的容器里额外要求"内部元素始终单调",进出时顺手把"不可能再当答案的候选"清掉——这就是单调栈 / 单调队列。存储结构因此变成了筛选器,这是本 deck 后半场的全部内容。

阅读提示:术语忘了回来查这一页。真正要背的只有两处:环形队列的判空判满,以及倒数第三页的速查表。
前置页:十个术语先定义再使用,"取模成环""假溢出""背压"三个是零背景读者最容易卡住的点。最小心智模型把栈/队列/deque 统一成"限制在哪端进出"一个动作,并预告"加单调约束"这条升级路线。

Two Disciplines

同一个容器,两种出入纪律

把开场的撤销键和打印机画出来就是下面两张图:左边同端进出(栈),右边两端分工(队列)。

栈的后进先出与队列的先进先出操作示意 左图栈只允许在栈顶压入和弹出,后放进去的元素先出来;右图队列从队尾入队、从队头出队,先放进去的元素先出来。 STACK · LIFO 后进先出 cba top push pop 只在栈顶进出 · a 最后进却最先出 函数调用 / 括号 / 撤销 / DFS QUEUE · FIFO 先进先出 xyz dequeue enqueue front(出队位) rear(入队位) 两端各司其职 · 先来的先服务 BFS / 任务队列 / 消息队列 / 缓冲
一图定调:栈单端进出(蓝框是 top),队列两端分工(蓝框是队头)。记住右侧的场景清单——栈是"最近的先处理",队列是"先来的先服务",所有应用都是这两句话的推论。

Stack · Implementation

栈的实现:动态数组为主,链表为辅

操作复杂度说明
push / pop / peekO(1)全部在栈顶一端进行
搜索(任意元素)O(n)栈不提供遍历语义
空间O(n)数组版有扩容预留
Go 惯用法:标准库没有 Stack,社区惯例直接用 slice——s = append(s, v) 入栈,v, s = s[len(s)-1], s[:len(s)-1] 出栈。扩容是均摊 O(1)(复杂度 deck),缓存又友好,链栈只在没有容量预期且频繁头插时才用。

顺序栈(slice / vector)

缓存局部性好、内存紧凑;代价是偶尔扩容搬迁、预留空间浪费。现代工程默认选择。

链栈

天然无"满栈"、峰值内存按需;每节点 8B 指针开销 + 分配开销 + 缓存不友好。只在内存紧张或元素巨大时考虑。

语言对照

Java Deque<Integer> s = new ArrayDeque<>()(官方已不建议用继承 Vector 的 Stack 类);C++ std::stack(默认基于 deque);Python 直接用 list。

一个设计直觉

栈操作全在一端,数组"尾部操作 O(1)"正好对上;队列两端操作,普通数组就不合适了——这正是下一页环形队列要解决的问题。

核心只有一句:栈=数组尾部操作,天然 O(1)。Go 面试要能写出 slice 版 push/pop 惯用法;Java 的 Stack 类是遗留设计(继承 Vector 带锁),官方推荐 ArrayDeque——这是加分细节。

Circular Queue · Ring Buffer

环形队列:取模让数组首尾相接

容量为 8 的环形队列及判空判满规则 8 个槽位围成圆环,head 指向出队位槽 2,tail 指向下一个入队位槽 5,槽 2 到槽 4 存有数据;判空条件 head 等于 tail,判满条件 tail 加一取模等于 head,即牺牲一个槽位。 abc 01234567 head = 2 出队位 tail = 5 · 入队位 INVARIANT · 容量 n=8 入队:buf[tail] = v; tail = (tail+1) % n 出队:v = buf[head]; head = (head+1) % n 判空:head == tail 判满:(tail+1) % n == head(牺牲一格) 替代方案:另记 size 计数,或 tag 区分空/满 入队/出队都是 O(1),无搬移
先说动机:普通数组出队要整体前移 O(n),或者用头指针只进不退造成"假溢出";取模成环后入队出队都 O(1)。判满"牺牲一格"是最经典约定,Go channel 的环形缓冲就是这个结构(chan.go 的 buf + sendx/recvx)。

Deque · Double-Ended Queue

双端队列:两端都能进出的"合体"结构

定位:栈 + 队列的超集

两端都支持 O(1) 插入删除(pushFront/pushBack/popFront/popBack)。只在一端用就是栈,两端分工用就是队列。

两种主流实现

双链表:实现最直接,指针开销大;② 块状数组:多个固定块(如每块 64 槽)用中间索引表串起来——Python collections.deque 就是它,兼顾两端 O(1) 与缓存局部性;Java ArrayDeque 用环形数组

Go 里的对应物

标准库没有 deque;container/list(双链表)或手写环形数组。GMP 调度器的本地运行队列是固定 256 的环形队列 + 偷一半语义(GMP deck)。

高光应用:单调队列

装上"值单调"约束的 deque 就是滑动窗口最大值的 O(n) 解法——第 10 页展开。

语言实现底层
Pythoncollections.deque块状链表(64 槽/块)
JavaArrayDeque / LinkedList环形数组 / 双链表
C++std::deque块数组 + 中央映射表
Gocontainer/list / 手写双向循环链表 / 环形数组
面试常问:"deque 和两个栈拼的队列有什么区别?"——两栈版(in/out 栈)pop 均摊 O(1) 但最坏单次 O(n);真 deque 每次 O(1)。对延迟敏感的调度/IO 场景必须真 deque。
记住三种实现路线:双链表、环形数组(Java ArrayDeque)、块状数组(Python deque / C++ deque)。C++ deque 的"中央映射表"和 GMP 本地队列是两个可以展开的细节,按面试方向选用。

Stack Applications

栈的应用:凡是"最近的先处理",都是栈

① 括号匹配(LC 20)

左括号入栈;右括号必须与栈顶的左括号配对——"最近的未匹配左括号"天然是 LIFO 语义。计数器数不出 [(]) 这种交错非法。

② 函数调用栈

每层调用一个栈帧(参数、返回地址、局部变量);递归就是隐式栈。爆栈(深递归)的标准出路:显式栈改迭代。

③ 表达式求值(LC 150 / 224)

后缀(逆波兰)直接一个栈:操作数入栈,遇运算符弹两个算完压回。中缀转后缀:运算符栈按优先级进出,括号是优先级屏障。

④ 浏览器前进后退 / 编辑器撤销

后退栈 + 前进栈成对工作:跳转时把当前页压入"后退栈"并清空"前进栈"——两个栈就是完整的历史状态机。

⑤ DFS / 回溯

递归DFS靠调用栈;显式栈版用 stack 存待访问节点。回溯的"撤销选择"正是出栈语义(回溯 deck)。

⑥ 最小栈(LC 155)

辅助栈与主栈同步:push 时若 ≤ 辅助栈顶则同步压入(或每步都压"当前最小"),pop 时同步弹——getMin 恒 O(1)。

面试金句:"栈的本质是嵌套/成对结构的匹配器——括号成对、函数调用成对(调用/返回)、撤销重做成对。看到'最近相关'四个字,条件反射上栈。"
递归深度红线:Go 的 goroutine 栈从 2KB 起按需增长(连续栈复制扩容),64 位默认上限 1GB(debug.SetMaxStack);但深递归的帧复制仍有成本,热路径上显式栈迭代化更稳。
六个应用按出现频率排:括号和调用栈必考;表达式求值是中等频率硬骨头;前进后退双栈是好的白板题。底部 Go 栈增长机制是 Go 岗差异化素材——与 os/goroutine 话题衔接。

Queue Applications

队列的应用:凡是"先来的先服务",都是队列

① BFS(逐层遍历 / 无权最短路)

FIFO 保证"先入队的先扩展"——第 k 层全部处理完才轮到 k+1 层,层次性就是正确性。换成栈就退化成 DFS。

② 生产者—消费者 / 任务队列

线程池的待办队列、日志异步落盘、削峰填谷。内存版最典型实现就是 Go channel:环形缓冲 + 两个等待队列 + 互斥锁(channel deck)。

③ 消息队列(跨进程/跨机器)

Kafka/RocketMQ 把"队列"升级成分布式服务:持久化、分区并行、消费组、重试与回溯——解耦、削峰、异步三大价值(Kafka deck)。

④ 缓冲与限流

网络收发包缓冲、令牌桶的令牌供给、打印队列;OS 的就绪队列/IO 队列同理——调度器的等待语义全是队列。

场景为什么是队列关键属性
BFS保序 → 层次正确FIFO + visited 标记
线程池任务公平排队阻塞 + 唤醒
channel协程间生产消费环形缓冲 + 等待队列
Kafka跨服务解耦削峰持久化 + 分区 + 消费组
限流缓冲瞬时洪峰排队有界 + 满时拒绝/阻塞
面试金句:"队列的本质是排队公平性——只要需求是'按到达顺序处理',不管是协程、线程、请求还是消息,中间那个缓冲区一定是队列。区别只在规模:内存队列(channel)到分布式队列(Kafka),FIFO 语义没变,加的是持久化与水平扩展。"
队列这页重点是"从小到大"的尺度感:BFS 是算法层,channel 是进程内工程层,Kafka 是分布式层——三层都是同一 FIFO 语义。这个递进式回答在面试里很出彩,也把两个相关 deck 串了起来。

Monotonic Stack

单调栈:给栈装上单调性,暴力变线性

适用信号

对每个元素问"下一个/前一个 更大/更小的元素是谁"。暴力是 O(n²) 双重扫描;单调栈一遍 O(n)。

不变式(以"下一个更大"为例)

栈里存还没找到答案的下标,其对应值自底向顶单调不增。新元素 x 到来:弹出所有严格小于 x 的栈顶——x 就是它们的"下一个更大元素"(相等的留下:x 并不比它们大);然后 x 入栈等待自己的答案。

为什么是 O(n)

每个下标最多入栈一次、出栈一次,总操作 ≤ 2n——典型的摊还分析(复杂度 deck)。

方向速查

下一个更大 → 递减栈(栈顶最小,比 x 小的才弹,相等别弹);下一个更小 → 递增栈;前一个更小(84 题)→ 从左扫递增栈;循环数组(503)→ 遍历 2n 取模。

// 每日温度 (LC 739):下一个更高温度隔几天
func dailyTemperatures(t []int) []int {
    ans := make([]int, len(t))
    stack := []int{} // 存下标,温度递减
    for i, v := range t {
        for len(stack) > 0 &&
            t[stack[len(stack)-1]] < v {
            j := stack[len(stack)-1]
            stack = stack[:len(stack)-1]
            ans[j] = i - j // v 是 j 的答案
        }
        stack = append(stack, i)
    }
    return ans // 栈里剩下的没有更大值,ans=0
}
模板骨架:外层遍历 + 内层 while 弹栈 + 弹出时记答案 + 当前元素入栈。四行结构适用于全部单调栈题,变的只有"弹栈时算什么"。
单调栈的三个记忆锚:信号词(下一个更大/更小)、不变式(栈内单调)、O(n) 的摊还证明。代码模板必须能默写——LC 739 是母题,496/503/84 全是它的变奏。

Largest Rectangle · LC 84

图解:柱状图最大矩形里的单调栈

柱状图中最大矩形问题的单调栈求解过程 高度为 2、1、5、6、2、3 的六根柱子,维护高度递增的栈;处理到高度 2 时把高度 6 和 5 弹出,分别得到宽 1 面积 6 和宽 2 面积 10,其中 10 是答案;末尾用哨兵 0 统一清栈。 215623 i=0i=1i=2i=3i=4i=5 答案:h5 × w2 = 10 STACK(下标 · 高度递增) i=0 h2 · 入栈 [0] i=1 h1 · 弹0→2×1=2 · 入1 [1] i=2 h5 · 入栈 [1,2] i=3 h6 · 入栈 [1,2,3] i=4 h2 · 弹3→6×1=6 弹2→5×2=10 ★ 哨兵 h0 清栈:统一出口 [1,4,5] 宽 = i − 新栈顶 − 1(左右边界)
这图配 84 题的标准推导:递增栈;遇到更矮柱子弹栈顶,"以弹出柱子高度为高"的矩形宽度由左右边界决定(i − 新栈顶 − 1)。i=4 弹出 5、6 两根得面积 10 是答案。哨兵 0 在首尾把"清栈"统一进主循环——工程上最优雅的写法。

Monotonic Deque · LC 239

单调队列:滑动窗口最大值的 O(n) 解法

结构约定

deque 里存下标,对应值从头到尾单调递减——队头永远是当前窗口的最大值。

两条维护规则

(窗口右侧进 x):从队尾把所有 ≤ x 的下标弹出——它们比 x 小又先过期,永远不可能再当最大(窗口左界滑过队头下标):从队头弹出。

为什么 O(n)

每个下标至多入队一次、出队一次 → 总操作 ≤ 2n。与单调栈同一套摊还论证。

vs 堆做法

堆(惰性删除过期下标)是 O(n log n),好写但慢一档;单调队列 O(n) 且每步取最大 O(1)。面试先说堆再优化到队列,展示递进。

// 滑动窗口最大值 (LC 239)
func maxSlidingWindow(a []int, k int) []int {
    dq := []int{} // 存下标,值递减
    ans := []int{}
    for i, v := range a {
        // 1. 尾部:弹掉不可能是最大的
        for len(dq) > 0 && a[dq[len(dq)-1]] <= v {
            dq = dq[:len(dq)-1]
        }
        dq = append(dq, i)
        // 2. 头部:弹出过期下标
        if dq[0] <= i-k { dq = dq[1:] }
        // 3. 窗口成型后取队头
        if i >= k-1 {
            ans = append(ans, a[dq[0]])
        }
    }
    return ans
}
与滑动窗口 deck 的分工:那篇讲"窗口伸缩框架",本篇讲"窗口内最值的维护"——两招组合覆盖连续子数组家族(滑动窗口 deck)。
单调队列三步走代码:尾弹(保单调)、头弹(去过期)、取头(拿答案)。核心洞察一句话:"比新来的小、又比它先出窗的元素,已经没有未来"。LC 239 是 hard 但模板就这十行。

From Ring Buffer To Channel

从环形队列到 Go channel:多了一层并发

环形队列的工程化三件事

并发安全:多生产者/消费者时 head/tail 要互斥或原子操作;② 背压:满了是阻塞、丢弃还是拒绝——语义由业务定;③ 内存序:无锁实现要配 memory barrier。

Go channel 的答案

hchan = 环形缓冲 buf + sendx/recvx 下标 + mutex + sendq/recvq 两个等待队列。有缓冲 channel 就是"阻塞版环形队列":满则发送方挂进 sendq 睡眠,接收方唤醒它——背压语义内建。

对比表

手写环形队列:零依赖、自己管锁与背压;channel:拿到来锁、阻塞唤醒、select 多路复用,性能略低但安全得多。工程默认 channel,极致热路径才手写。

维度环形队列(手写)Go channel
结构数组 + head/tail 取模hchan:buf 环形 + 双等待队列
并发自己加锁/原子mutex 内建
满时行为自定义(阻塞/丢弃/拒绝)阻塞 → goroutine 挂 sendq
多路复用select
适用热路径、固定拓扑默认选择
面试句式:"channel 底层就是一个加了锁和协程等待队列的环形队列"——一句话同时展示数据结构功底和 Go runtime 理解,然后引到 channel deck 的 hchan 字段级拆解。
这页是数据结构课和 Go runtime 课的接口:环形队列 + 锁 + 等待队列 = channel。三件套(并发安全/背压/内存序)是通用工程化问题,channel 给出了教科书答案。

LeetCode Shortlist

必刷题单:栈、队列与单调结构

分组题目(编号 · 难度)要点
基础栈20 有效的括号 E · 150 逆波兰表达式 M · 155 最小栈 E20 是栈语义入门;155 用辅助栈
结构互转232 用栈实现队列 E · 225 用队列实现栈 E232 要会均摊 O(1) 分析
单调栈739 每日温度 M · 496 下一个更大元素 I E · 503 循环 M · 84 柱状图最大矩形 H739 是母题;84 加哨兵;503 遍历 2n
单调队列239 滑动窗口最大值 H尾弹头弹取头三步;对比堆解法
组合应用42 接雨水 H · 224 基本计算器 H · 394 字符串解码 M42 三解法都值得写一遍
刷法建议:先 20/232 练手熟 → 739 背单调栈模板 → 84/42 上强度 → 239 收尾(单调队列唯一 hard 但模板十行)。每个单调栈题都先写 O(n²) 暴力再优化,面试时能讲"优化动机"。
题单五组由易到难。42 接雨水值得特别强调:前缀 max、双指针、单调栈三种解法对应三个思维角度,是"一题多解"的模范题。

Cheat Sheet

一页带走:选型、实现、单调模板、复杂度

① 选型:看到这些词,就用这个结构

需求里的信号词结构
最近的 / 嵌套 / 成对 / 撤销 / 回溯(括号、表达式、DFS、历史)
先来先服务 / 逐层 / 缓冲 / 削峰队列(BFS、线程池、channel、MQ)
两端都要操作 / 窗口两侧双端队列
下一个更大 / 更小、左右第一个更矮单调栈
固定窗口内的最大 / 最小值单调队列
要按权重 / 优先级取,不按到达序堆(优先队列)

② 实现三个必背结论

环形队列buf[tail]=v; tail=(tail+1)%n;空 head==tail;满 (tail+1)%n==head(牺牲一格)或另记 size
两栈实现队列in 栈进、out 栈出,out 空才整体倒运;均摊 O(1),单次最坏 O(n)
最小栈辅助栈同步压 min(v, 辅助栈顶),getMin 恒 O(1),空间换时间

③ 单调结构:方向 + 四步模板

找"下一个更大"递减栈(栈内自底向顶不增):来了更大的就弹栈并给答案
找"下一个更小"递增栈;口诀:要找更大就先让小的等在栈里
栈的四步骨架遍历 → while 弹栈 → 弹出时结算答案 → 当前入栈(末尾可加哨兵统一清栈)
队列的三步骨架尾弹(保单调)→ 头弹(去过期)→ 取头(拿答案)
为什么 O(n)每个下标至多入一次、出一次,总操作 ≤ 2n(摊还)

④ 复杂度一览

栈 push/pop/peekO(1)(数组尾部或链表头部)
队列 enqueue/dequeueO(1)(环形取模,无搬移)
deque 两端操作O(1)(双链表 / 环形数组 / 块状数组)
单调栈 / 单调队列全程O(n) 时间,O(n) 空间
一句话背下来:栈和队列只限制"从哪端进出",选哪一个取决于业务要的是最近还是最先;再加一条"内部保持单调",容器就从存储升级成 O(n) 的筛选器。
速查页四块按使用顺序:先选型(信号词→结构)、再实现要点、再单调模板、最后复杂度。末尾一句把第 3 页的最小心智模型收回来,形成闭环。

Interview QA · Part 1

高频追问:实现与互转

先盖住答案自己答一遍,再展开对照——想不起来比看得顺眼记得牢;答不出的翻回上一页速查表。

1 · 栈/队列用数组还是链表实现?

数组优先

数组(动态):缓存友好、内存紧凑、操作均摊 O(1);链表:无满栈/无搬移但缓存差、指针开销。栈用数组尾部天然对上;队列两端操作需环形化。

2 · 环形队列怎么判空判满?

牺牲一格size 计数

head==tail 判空时,满也是 head==tail——两解:① 牺牲一格,判满 (tail+1)%n==head(容量 n-1);② 另记 size;③ tag 位区分。Go channel 用 size 语义(qcount)。

3 · 两个栈实现队列,为什么是均摊 O(1)?

in/out 栈

push 进 in 栈 O(1);pop 从 out 栈弹,out 空才把 in 全部倒入。每个元素一生最多"进 in 一次 + 倒运两次 + 出 out 一次"——总代价 O(n) 摊到 n 次操作 → 均摊 O(1),但单次最坏 O(n)。

4 · 最小栈(getMin O(1))怎么设计?

LC 155

辅助栈与主栈同步:push 时压入 min(当前, 辅助栈顶)(每步都压,弹栈自动回滚);或只在不大于栈顶时压(省空间但要小心重复值——用 ≤)。空间 O(n) 换时间。

5 · 括号匹配为什么必须栈,计数器不行吗?

交错非法

计数器只数数量,查不出交错[(]) 数量合法但非法。栈顶永远是"最近的未匹配左括号",配对必须与最近者配——LIFO 正是嵌套语义。

6 · 单调栈的 O(n) 怎么证明?

摊还分析

每个下标至多 push 一次、pop 一次,内层 while 的总弹栈次数 ≤ n,加上外层 n 次遍历 → 总操作 ≤ 2n。和动态数组扩容同一套"总账"论证。

7 · 递增栈还是递减栈怎么选?

看答案方向

找"下一个更大"→ 递减栈(新的大元素来时弹出小元素给答案);找"下一个更小"→ 递增栈。口诀:要找更大就先让小的等在栈里。84 题的"前一个更小"是递增栈从左扫。

8 · BFS 为什么用队列,用栈行不行?

FIFO 保层次

队列保证 k 层节点先于 k+1 层被扩展——层次序即最短性来源。用栈变成 DFS,失去层次语义;无权图最短路就错了(可对照 Dijkstra 用优先队列的加权版)。

前八题覆盖实现层:数组选型、判空判满、两栈互转的均摊、最小栈、括号计数器反例、单调栈摊还、方向选择、BFS 正确性。第 3 题务必带上"单次最坏 O(n) 但均摊 O(1)"的完整表述。

Interview QA · Part 2

高频追问:工程落地与 Go runtime

同样先自答再对照。这一页的题要"结论 + 一句代价/边界"才完整——只答结论会被继续追问。

9 · 42 接雨水的几种解法?

一题三解

① 前缀 max 两个数组:每个柱接 min(左max,右max)−h,O(n)/O(n);② 双指针夹逼:短边决定水位,O(n)/O(1);③ 单调栈:按"行"结算,横向接水。三种都 O(n),空间和思路不同。

10 · LC 239 用单调队列而不是堆,快在哪?

O(n) vs O(n log n)

堆要把过期元素惰性删除,每次 O(log n);单调队列每个下标进出各一次,均摊 O(1),且取最大直接读队头 O(1)。数据量大时差距明显。

11 · 递归太深爆栈怎么办?

显式栈迭代

把递归改显式栈:状态(节点+阶段)自己管理。Go 里 goroutine 栈 2KB 起自动增长、64 位默认上限 1GB,一般不爆但帧复制有成本;链表/树深度受输入控制时优先迭代。

12 · Go channel 和队列是什么关系?

hchan

channel = 环形缓冲(buf + sendx/recvx)+ mutex + sendq/recvq 两个等待队列。有缓冲即阻塞式生产消费队列:满则发送方挂起、接收方取走后唤醒——背压内建。字段级拆解见 channel deck

13 · 内存队列和 Kafka 这类消息队列的边界?

进程内 vs 分布式

channel/BlockingQueue 服务进程内协程/线程;Kafka 加上持久化日志、分区并行、消费组与重试回溯,服务跨机器解耦。FIFO 语义同源,规模与可靠性保证不同(Kafka deck)。

14 · 双端队列怎么做到两端 O(1)?

三种实现

双链表(直接但指针重)、环形数组(Java ArrayDeque,取模两端推进)、块状数组(Python deque/C++ deque:块内连续 + 块间索引表)。数组系对缓存更友好。

15 · 什么时候"队列"要升级成优先队列?

公平 → 优先

到达序公平不再是唯一诉求时:任务有权重/截止期(调度器、Dijkstra、合并 K 链表)。堆 O(log n) 出入队换"始终取最优"——队列管顺序,堆管优先级。

后七题偏工程与 Go:接雨水三解、239 对比、爆栈迭代化、channel 本质、Kafka 边界、deque 实现、优先队列升级。第 12/13 题是 Go 后端面试的串联题,答好可以主动把三个 deck 串成体系。

Related & References

相关知识点与参考

数据结构系列(本分类)

数组与链表 →(栈/队列的实现载体)
堆与优先队列 →("队列"的加权升级版)
二叉树 →(BFS 层序遍历即队列应用)

算法与 Go / Kafka 系列

滑动窗口 →(单调队列的主战场)
回溯 →(显式栈替代递归)
Go channel 底层 →(环形缓冲 + 等待队列)
Kafka 内核 →(分布式消息队列)

参考来源(本 deck 结论可溯源至下列一手材料)

CLRS ch10.1 · Stacks and Queues栈/队列/环形队列的形式化定义
LeetCode 20/150/155/232/225/739/84/42/239 官方题解单调栈/单调队列模板与复杂度论证
src/runtime/chan.go(本机 Go 版本)hchan:buf/sendx/recvx/qcount/lock/sendq/recvq
oi-wiki.org/basic/stack · /queue单调栈与单调队列的中文系统讲解
docs.python.org · collections.deque块状双端队列实现说明
收尾:两条链接线把本 deck 挂进知识网络——数据结构侧(数组链表/堆/二叉树)与工程侧(channel/Kafka/滑动窗口)。Go channel 的结论与仓库 channel deck 同源(runtime/chan.go),互链不打架。