第 07 站 · 线性结构
栈、单调队列与堆
单调栈:下一个更大元素 时间 O(n) · 空间 O(n)
先做这道
LC 20有效的括号卡住再看
用栈匹配括号类型,处理提前闭合和残留开括号。
要点
- 栈里存下标而不是值:既能取到值,也能算距离(比如「还要等几天才升温」)。
- 每个元素只进栈、出栈各一次,所以虽然套了 while,总复杂度仍是 O(n)。
- 求前 k 大用大小为 k 的小根堆;滑动窗口最值用单调双端队列,队头过期就弹出。
易错点
- 有重复值时 < 和 <= 结果不同,先想清楚要「严格更大」还是「大于等于」。
- Python 的 heapq 只有小根堆,要大根堆就存相反数。
- Java 的 Stack 类又慢又是同步的,栈和队列都用 ArrayDeque;比较器不要写成 a - b。
看动画
一步一步播放这个模式在小例子上的执行过程。可以暂停、单步前进或后退,拖动进度条跳到任意一步。
单调栈 · 下一个更大元素
数组 3, 1, 4, 1, 5, 9, 2, 6
换个输入
- 当前下标
- —
- 栈大小
- 0
上一行是数组,下一行是答案:每个数右边第一个更大的数。栈里存还没找到答案的下标。
始终成立从栈底到栈顶,值不增;每个下标只进栈、出栈各一次,总共 O(n)。
堆 · 数据流中的第 k 大
数据流 5, 2, 9, 1, 7, 8, 3,求第 3 大
换个输入
- k
- 3
- 堆大小
- 0
- 第 k 大
- —
维护一个最多 3 个元素的小顶堆:堆顶是堆里最小的数。
始终成立堆里始终是目前读到的最大的 k 个数;每次进出堆都是 O(log k)。
模板
选一种语言展开。四种写法实现的是同一个模板,每一份都编译运行过,并用测试数据核对了输出。看完用它重写热身题 LC 20。
Python
def next_greater(nums: list[int]) -> list[int]:
"""For each i, the first value to the right that is strictly greater, or -1."""
ans = [-1] * len(nums)
stack = [] # indexes still waiting; their values decrease from bottom to top
for i, x in enumerate(nums):
while stack and nums[stack[-1]] < x:
ans[stack.pop()] = x
stack.append(i)
return ansC++
// For each i, the first value to the right that is strictly greater, or -1.
vector<int> nextGreater(const vector<int>& nums) {
vector<int> ans(nums.size(), -1);
vector<int> st; // indexes still waiting; their values decrease from bottom to top
for (int i = 0; i < (int)nums.size(); i++) {
while (!st.empty() && nums[st.back()] < nums[i]) {
ans[st.back()] = nums[i];
st.pop_back();
}
st.push_back(i);
}
return ans;
}Java
class Solution {
// For each i, the first value to the right that is strictly greater, or -1.
int[] nextGreater(int[] nums) {
int[] ans = new int[nums.length];
Arrays.fill(ans, -1);
Deque<Integer> st = new ArrayDeque<>(); // indexes still waiting; values decrease to the top
for (int i = 0; i < nums.length; i++) {
while (!st.isEmpty() && nums[st.peek()] < nums[i]) ans[st.pop()] = nums[i];
st.push(i);
}
return ans;
}
}Go
// nextGreater returns, for each i, the first value to the right that is strictly greater, or -1.
func nextGreater(nums []int) []int {
ans := make([]int, len(nums))
var st []int // indexes still waiting; their values decrease from bottom to top
for i, x := range nums {
ans[i] = -1
for len(st) > 0 && nums[st[len(st)-1]] < x {
ans[st[len(st)-1]] = x
st = st[:len(st)-1]
}
st = append(st, i)
}
return ans
}常见变体
这一站的题还会用到下面 3 种写法。展开看什么时候换用它,代码同样四种语言都跑过测试。
单调队列:滑动窗口最大值时间 O(n)(每个下标进出队列各一次)· 空间 O(k)
窗口在滑、要随时拿到窗口里的最大(或最小)值:双端队列里存下标、对应值从头到尾递减;新元素从尾部挤掉比它小的,队头滑出窗口就弹掉,队头永远是最大值。LC 239 滑动窗口最大值;LC 862 在前缀和上用同一个队列。
Python
from collections import deque
def max_sliding_window(nums: list[int], k: int) -> list[int]:
"""Maximum of every length-k window (LC 239)."""
dq: deque[int] = deque() # indexes in the window, values decreasing front to back
res = []
for i, x in enumerate(nums):
while dq and nums[dq[-1]] <= x:
dq.pop() # smaller values before x can never be a maximum again
dq.append(i)
if dq[0] <= i - k:
dq.popleft() # the front slid out of the window
if i >= k - 1:
res.append(nums[dq[0]])
return resC++
// Maximum of every length-k window (LC 239).
vector<int> maxSlidingWindow(const vector<int>& nums, int k) {
deque<int> dq; // indexes in the window, values decreasing front to back
vector<int> res;
for (int i = 0; i < (int)nums.size(); i++) {
while (!dq.empty() && nums[dq.back()] <= nums[i]) dq.pop_back(); // they can never be a maximum again
dq.push_back(i);
if (dq.front() <= i - k) dq.pop_front(); // the front slid out of the window
if (i >= k - 1) res.push_back(nums[dq.front()]);
}
return res;
}Java
class Solution {
// Maximum of every length-k window (LC 239).
int[] maxSlidingWindow(int[] nums, int k) {
Deque<Integer> dq = new ArrayDeque<>(); // indexes in the window, values decreasing front to back
int[] res = new int[nums.length - k + 1];
for (int i = 0; i < nums.length; i++) {
while (!dq.isEmpty() && nums[dq.peekLast()] <= nums[i]) dq.pollLast(); // they can never be a maximum again
dq.offerLast(i);
if (dq.peekFirst() <= i - k) dq.pollFirst(); // the front slid out of the window
if (i >= k - 1) res[i - k + 1] = nums[dq.peekFirst()];
}
return res;
}
}Go
// maxSlidingWindow returns the maximum of every length-k window (LC 239).
func maxSlidingWindow(nums []int, k int) []int {
var dq []int // indexes in the window, values decreasing front to back
var res []int
for i, x := range nums {
for len(dq) > 0 && nums[dq[len(dq)-1]] <= x {
dq = dq[:len(dq)-1] // they can never be a maximum again
}
dq = append(dq, i)
if dq[0] <= i-k {
dq = dq[1:] // the front slid out of the window
}
if i >= k-1 {
res = append(res, nums[dq[0]])
}
}
return res
}大小为 k 的小顶堆求第 k 大时间 O(n log k) · 空间 O(k)
只关心最大的 k 个(第 k 大、前 k 个高频、数据流里的第 k 大):维护一个大小为 k 的小顶堆,堆顶是这 k 个里最弱的;新值比堆顶大才替换。比整体排序省,O(n log k)。LC 215 数组中的第 K 个最大元素、LC 703、LC 373。
Python
import heapq
def kth_largest(nums: list[int], k: int) -> int:
"""The k-th largest value (LC 215), with a size-k min-heap."""
heap: list[int] = [] # the k largest so far; heap[0] is the smallest of them
for x in nums:
if len(heap) < k:
heapq.heappush(heap, x)
elif x > heap[0]:
heapq.heapreplace(heap, x) # x beats the weakest of the top k
return heap[0]C++
// The k-th largest value (LC 215), with a size-k min-heap.
int kthLargest(const vector<int>& nums, int k) {
priority_queue<int, vector<int>, greater<int>> heap; // the k largest so far; top is the smallest
for (int x : nums) {
if ((int)heap.size() < k) heap.push(x);
else if (x > heap.top()) {
heap.pop(); // x beats the weakest of the top k
heap.push(x);
}
}
return heap.top();
}Java
class Solution {
// The k-th largest value (LC 215), with a size-k min-heap.
int kthLargest(int[] nums, int k) {
PriorityQueue<Integer> heap = new PriorityQueue<>(); // the k largest so far; peek() is the smallest
for (int x : nums) {
if (heap.size() < k) heap.offer(x);
else if (x > heap.peek()) {
heap.poll(); // x beats the weakest of the top k
heap.offer(x);
}
}
return heap.peek();
}
}Go
import "container/heap"
// minHeap is a min-heap of ints for container/heap.
type minHeap []int
func (h minHeap) Len() int { return len(h) }
func (h minHeap) Less(i, j int) bool { return h[i] < h[j] }
func (h minHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *minHeap) Push(x any) { *h = append(*h, x.(int)) }
func (h *minHeap) Pop() any {
old := *h
x := old[len(old)-1]
*h = old[:len(old)-1]
return x
}
// kthLargest returns the k-th largest value (LC 215), with a size-k min-heap.
func kthLargest(nums []int, k int) int {
h := &minHeap{} // the k largest so far; (*h)[0] is the smallest
for _, x := range nums {
if h.Len() < k {
heap.Push(h, x)
} else if x > (*h)[0] {
(*h)[0] = x // x beats the weakest of the top k
heap.Fix(h, 0)
}
}
return (*h)[0]
}表达式求值:乘除立刻结算,加减入栈时间 O(n) · 空间 O(n)
计算只有 + - * / 的表达式(LC 227 基本计算器 II):读完一个数时看它前面的运算符,加减把带符号的数压栈,乘除直接和栈顶算掉;最后把栈里的数加起来。有括号的 LC 224、LC 772 是遇到括号时递归或再压一层。
Python
def calculate(s: str) -> int:
"""+ - * / over non-negative integers, no parentheses (LC 227); / truncates toward zero."""
stack: list[int] = [] # signed terms to add up; * and / fold into the last one
num, op = 0, "+" # op: the operator before num
for i, c in enumerate(s):
if c.isdigit():
num = num * 10 + int(c)
if (not c.isdigit() and c != " ") or i == len(s) - 1: # an operator or the end closes num
if op == "+":
stack.append(num)
elif op == "-":
stack.append(-num)
elif op == "*":
stack.append(stack.pop() * num)
else:
top = stack.pop()
stack.append(abs(top) // num * (1 if top >= 0 else -1)) # truncate toward zero
op, num = c, 0
return sum(stack)C++
// + - * / over non-negative integers, no parentheses (LC 227); / truncates toward zero.
long long calculate(const string& s) {
vector<long long> stack; // signed terms to add up; * and / fold into the last one
long long num = 0;
char op = '+'; // the operator before num
for (size_t i = 0; i < s.size(); i++) {
char c = s[i];
if (isdigit(c)) num = num * 10 + (c - '0');
if ((!isdigit(c) && c != ' ') || i == s.size() - 1) { // an operator or the end closes num
if (op == '+') stack.push_back(num);
else if (op == '-') stack.push_back(-num);
else if (op == '*') stack.back() *= num;
else stack.back() /= num; // C++ division truncates toward zero
op = c;
num = 0;
}
}
return accumulate(stack.begin(), stack.end(), 0LL);
}Java
class Solution {
// + - * / over non-negative integers, no parentheses (LC 227); / truncates toward zero.
long calculate(String s) {
Deque<Long> stack = new ArrayDeque<>(); // signed terms to add up; * and / fold into the last one
long num = 0;
char op = '+'; // the operator before num
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (Character.isDigit(c)) num = num * 10 + (c - '0');
if ((!Character.isDigit(c) && c != ' ') || i == s.length() - 1) { // an operator or the end closes num
if (op == '+') stack.push(num);
else if (op == '-') stack.push(-num);
else if (op == '*') stack.push(stack.pop() * num);
else stack.push(stack.pop() / num); // Java division truncates toward zero
op = c;
num = 0;
}
}
long total = 0;
for (long term : stack) total += term;
return total;
}
}Go
// calculate evaluates + - * / over non-negative integers, no parentheses (LC 227).
func calculate(s string) int {
var stack []int // signed terms to add up; * and / fold into the last one
num, op := 0, byte('+') // op: the operator before num
for i := 0; i < len(s); i++ {
c := s[i]
digit := c >= '0' && c <= '9'
if digit {
num = num*10 + int(c-'0')
}
if (!digit && c != ' ') || i == len(s)-1 { // an operator or the end closes num
switch op {
case '+':
stack = append(stack, num)
case '-':
stack = append(stack, -num)
case '*':
stack[len(stack)-1] *= num
default:
stack[len(stack)-1] /= num // Go division truncates toward zero
}
op, num = c, 0
}
}
total := 0
for _, term := range stack {
total += term
}
return total
}为什么对
- x 到来时下标 i 还在栈里,说明 i 和当前位置之间没有比 nums[i] 更大的数(否则 i 早被弹出),所以弹出 i 的 x 就是它右边第一个更大的数。
- 均摊 O(n):每个下标恰好入栈一次、最多出栈一次,所有 while 循环加起来最多弹出 n 次。
- 大小为 k 的最小堆:堆顶是保留的 k 个里最小的,新元素只有比堆顶大才可能进前 k 大,替换堆顶不会丢掉真正的前 k 大。
30 秒口述稿
面试官让你讲思路时,按做法、为什么对、复杂度的顺序说。中英文各一版,可以对着读几遍。
找右边第一个更大的数,我用单调栈,栈里存下标,对应的值从底到顶递减。新元素到来时,把比它小的都弹出,并把它们的答案记为这个新元素。一个下标还留在栈里,说明中间没有更大的数,所以弹出时记下的一定是第一个更大的。每个下标只进出栈各一次,所以虽然有嵌套 while,总复杂度还是 O(n)。For the next greater element I keep a monotonic stack of indexes whose values decrease from bottom to top. When a new value arrives, I pop every smaller one and record the new value as its answer. An index still on the stack means nothing in between was larger, so the value that pops it really is the first greater one. Each index is pushed and popped at most once, so despite the nested loop the total is O(n).自评一下
对照上面的答案给自己打个分。没掌握的题会按 1、3、7、21 天的间隔回来,到期的题在「我的进度 · 到期复习」里。 只存在这台设备上;登录后会合并到你的账号。
小测
5 道概念题和复杂度题。答过的题会和 OA 选择题练习一起按间隔回来复习。 去 OA 选择题练习
作答记录只保存在这台设备的浏览器里;登录后打开OA 选择题练习页,会存进账号。
模板练习
用上面的模板和变体出题:把打乱的行排回原序,把挖掉的边界条件填回去,或者在空白编辑器里整段默写,跑本站的测试并逐行对比。每次「换一题」都会重新出题。
下面几行被打乱了。点一行选中,再点另一行和它交换,排回模板原来的顺序;缩进是提示。
分档练习
本站在 500 路里的 34 道题,按基础、核心、进阶分三档。每题附一句话思路,先自己想,想不出再看。
基础 6
| # | 题目 | 一句话思路 | 难度分 |
|---|---|---|---|
| 20 | 有效的括号 | 用栈匹配括号类型,处理提前闭合和残留开括号。 | |
| 150 | 逆波兰表达式求值 | 用操作数栈求后缀表达式,注意非交换运算的出栈次序。 | |
| 1047 | 删除字符串中的所有相邻重复项 | 把相邻抵消过程实现为栈,使新相邻字符继续参与删除。 | |
| 496 | 下一个更大元素 I | 从右向左维护单调栈,查询每个值后方的首个更大元素。 | |
| 921 | 使括号有效的最少添加 | 统计无法匹配的左右括号,而非仅检查总数量差。 | |
| 1046 | 最后一块石头的重量 | 反复取最大两个元素并回插差值,练习优先队列更新。 |
核心 16
| # | 题目 | 一句话思路 | 难度分 |
|---|---|---|---|
| 71 | 简化路径 | 将路径片段解释为当前目录、父目录与普通名称,维护规范路径栈。 | |
| 1209 | 删除字符串中的所有相邻重复项 II | 给栈元素附带重复次数,实现达到k次后的级联删除。 | |
| 394 | 字符串解码 | 保存括号前字符串与重复次数,恢复嵌套编码的上下文。 | |
| 224 | 基本计算器 | 用符号与括号上下文处理加减表达式,正确支持一元负号。 | |
| 227 | 基本计算器 II | 区分乘除与加减优先级,并明确整数除法的截断规则。 | |
| 739 | 每日温度 | 栈中保留下标,弹出时结算首次更高温度的等待距离。 | |
| 503 | 下一个更大元素 II | 通过两轮遍历模拟循环数组,避免为不存在的第二圈位置写答案。 | |
| 962 | 最大宽度坡 | 建立潜在左端点的递减栈,再逆序匹配最远右端点。 | |
| 402 | 移掉 K 位数字 | 在删除预算内维护单调数字栈,并处理尾删与前导零。 | |
| 735 | 小行星碰撞 | 只处理相向运动的碰撞,区分小行星消失、存活和同时抵消。 | |
| 946 | 验证栈序列 | 模拟入栈并尽可能匹配出栈序列,验证全局次序约束。 | |
| 1249 | 移除无效的括号 | 记录未匹配括号的位置,再保留其余字符得到合法结果。 | |
| 1190 | 反转每对括号间的子串 | 先配对括号,再改变遍历方向,实现嵌套反转而不反复复制子串。 | |
| 215 | 数组中的第K个最大元素 | 用大小为k的堆维护候选,并与快速选择的期望线性方案比较。 | |
| 1834 | 单线程 CPU | 按到达时间推进模拟,并按处理时间和下标选择下一个任务。 | |
| 239 | 滑动窗口最大值 | 用单调双端队列同时移除过期元素和被支配元素。 |
进阶 12
| # | 题目 | 一句话思路 | 难度分 |
|---|---|---|---|
| 772 | 基本计算器 III 🔒 | 结合嵌套括号与运算优先级实现完整表达式解析。 | |
| 726 | 原子的数量 | 为每层括号维护原子计数,正确应用多位数倍数并规范输出顺序。 | |
| 84 | 柱状图中最大的矩形 | 为每根柱子确定能延伸的左右边界,处理相等高度和哨兵。 | |
| 85 | 最大矩形 | 逐行把二进制矩阵转为柱状图,复用最大矩形算法,遇到零时清空连续高度。 | |
| 907 | 子数组的最小值之和 | 用单调栈统计每个元素作为最小值的贡献,并统一相等值归属。 | |
| 2104 | 子数组范围和 | 分别计算子数组最大值与最小值贡献,处理重复元素的边界规则。 | |
| 456 | 132 模式 | 逆序维护候选第三个元素与单调栈,识别非相邻132模式。 | |
| 316 | 去除重复字母 | 结合剩余字符次数与已入栈集合,构造不重复的最小字典序子序列。 | |
| 2402 | 会议室 III | 分别管理空闲会议室与占用结束时间,正确延迟会议并处理同刻事件。 | |
| 373 | 查找和最小的 K 对数字 | 把有序配对和视为多条有序序列,用堆逐步扩展候选。 | |
| 632 | 最小区间 | 维护每个列表的当前候选及全局最大值,寻找覆盖所有列表的最短区间。 | |
| 862 | 和至少为 K 的最短子数组 | 在含负数时对前缀和维护单调队列,替代失效的普通滑动窗口。 |
本站 OA 真题 102
- Nested List Weight Sum (for E4)Meta
- System VulnerabilitiesAmazon
- Consistency ScoreAmazon
- Find Max Value (Matrix Compression)Amazon
- Get Minimal Cost (Distinct Sizes)Amazon
- Can All Packages Be DeliveredAmazon
- Get Maximum Cost (Subscription Plan)Amazon
- Maximize Rental Revenue (VM Stock)Amazon
- Assign Tasks to ServersAmazon
- Calculate Warehouse Efficiency (Probably for Supply Chain related NG positions --Fungible-- :)Amazon