第 32 站 · 设计与位运算
容器与迭代接口设计
惰性迭代器:hasNext 负责前进 每次调用均摊 O(1)
- 学完去
- 没有后续站依赖它。
- 面试频次
- 4 家公司考过 · 面经出现 4 次Amazon 1、Applied Intuition 1、Chime 1、Oracle 1
先做这道
LC 225用队列实现栈卡住再看
在仅允许队列操作的限制下实现栈,比较入栈与出栈承担重排成本。
要点
- 把「跳过空行、走到下一个有效元素」放进 hasNext,next 只负责取值;这样连续调用多次 hasNext 也没有副作用。
- 只保存位置(行号、列号),不要在构造函数里把所有元素复制成一个大数组。
- 任意深度的嵌套列表用一个栈保存各层的位置;Python 里用生成器 yield from 最自然。
易错点
- 中间或末尾有空行时,hasNext 只看下一行就会返回错误的 true。
- next 里不先调用 hasNext,直接取值会越界。
- 迭代过程中修改了原容器:Java 会抛 ConcurrentModificationException,其他语言则是悄悄出错。
看动画
一步一步播放这个模式在小例子上的执行过程。可以暂停、单步前进或后退,拖动进度条跳到任意一步。
二叉树遍历 · 栈与队列
前序、中序、后序、层序遍历这棵树
换个输入
- 遍历
- 前序
- 输出
- —
同一棵树,四种遍历顺序。节点下的 #k 是它在当前遍历里第几个输出。
始终成立深度优先的三种顺序只差在「输出根」排在左右子树的前、中、后;层序用队列,先进先出就是一层一层。
模板
选一种语言展开。四种写法实现的是同一个模板,每一份都编译运行过,并用测试数据核对了输出。看完用它重写热身题 LC 225。
Python
class Flatten2D:
"""Iterates a list of lists in order, skipping empty rows."""
def __init__(self, rows: list[list[int]]):
self.rows, self.r, self.c = rows, 0, 0
def has_next(self) -> bool:
while self.r < len(self.rows) and self.c == len(self.rows[self.r]):
self.r, self.c = self.r + 1, 0 # skip finished and empty rows
return self.r < len(self.rows)
def next(self) -> int:
if not self.has_next():
raise StopIteration
self.c += 1
return self.rows[self.r][self.c - 1]C++
// Iterates a vector of vectors in order, skipping empty rows.
class Flatten2D {
vector<vector<int>> rows; // own a copy: a temporary argument would dangle
size_t r = 0, c = 0;
public:
Flatten2D(vector<vector<int>> rows) : rows(std::move(rows)) {}
bool hasNext() {
while (r < rows.size() && c == rows[r].size()) r++, c = 0; // skip finished and empty rows
return r < rows.size();
}
int next() {
if (!hasNext()) throw out_of_range("no more elements");
return rows[r][c++];
}
};Java
// Iterates a list of lists in order, skipping empty rows.
class Flatten2D implements Iterator<Integer> {
private final List<List<Integer>> rows;
private int r = 0, c = 0;
Flatten2D(List<List<Integer>> rows) {
this.rows = rows;
}
@Override
public boolean hasNext() {
while (r < rows.size() && c == rows.get(r).size()) { // skip finished and empty rows
r++;
c = 0;
}
return r < rows.size();
}
@Override
public Integer next() {
if (!hasNext()) throw new NoSuchElementException();
return rows.get(r).get(c++);
}
}Go
// Flatten2D iterates a slice of slices in order, skipping empty rows.
type Flatten2D struct {
rows [][]int
r, c int
}
func (it *Flatten2D) HasNext() bool {
for it.r < len(it.rows) && it.c == len(it.rows[it.r]) {
it.r, it.c = it.r+1, 0 // skip finished and empty rows
}
return it.r < len(it.rows)
}
func (it *Flatten2D) Next() int {
if !it.HasNext() {
panic("no more elements")
}
it.c++
return it.rows[it.r][it.c-1]
}常见变体
这一站的题还会用到下面 2 种写法。展开看什么时候换用它,代码同样四种语言都跑过测试。
两个栈拼一个队列:出栈空了才倒每次操作均摊 O(1) · 空间 O(n)
只给你栈(或要用栈模拟别的容器):新元素压进 inbox,取元素从 outbox 拿,outbox 空了才把 inbox 整个倒过去,顺序正好反过来。每个元素最多被搬一次,均摊 O(1)。LC 232 用栈实现队列;反过来用队列实现栈见 LC 225。
Python
class MyQueue:
"""A FIFO queue built from two stacks (LC 232)."""
def __init__(self):
self.inbox: list[int] = [] # new items go on top here
self.outbox: list[int] = [] # items leave from here, oldest on top
def push(self, x: int) -> None:
self.inbox.append(x)
def _refill(self) -> None:
if not self.outbox: # only when empty, or the order breaks
while self.inbox:
self.outbox.append(self.inbox.pop())
def pop(self) -> int:
self._refill()
return self.outbox.pop()
def peek(self) -> int:
self._refill()
return self.outbox[-1]
def empty(self) -> bool:
return not self.inbox and not self.outboxC++
// A FIFO queue built from two stacks (LC 232).
class MyQueue {
stack<int> inbox, outbox; // new items go on inbox; items leave from outbox, oldest on top
void refill() {
if (outbox.empty()) // only when empty, or the order breaks
for (; !inbox.empty(); inbox.pop()) outbox.push(inbox.top());
}
public:
void push(int x) { inbox.push(x); }
int pop() {
refill();
int x = outbox.top();
outbox.pop();
return x;
}
int peek() {
refill();
return outbox.top();
}
bool empty() const { return inbox.empty() && outbox.empty(); }
};Java
// A FIFO queue built from two stacks (LC 232).
class MyQueue {
private final Deque<Integer> inbox = new ArrayDeque<>(); // new items go on top here
private final Deque<Integer> outbox = new ArrayDeque<>(); // items leave from here, oldest on top
void push(int x) {
inbox.push(x);
}
int pop() {
refill();
return outbox.pop();
}
int peek() {
refill();
return outbox.peek();
}
boolean empty() {
return inbox.isEmpty() && outbox.isEmpty();
}
private void refill() {
if (outbox.isEmpty()) // only when empty, or the order breaks
while (!inbox.isEmpty()) outbox.push(inbox.pop());
}
}Go
// MyQueue is a FIFO queue built from two stacks (LC 232).
type MyQueue struct {
inbox, outbox []int // new items go on inbox; items leave from outbox, oldest on top
}
func (q *MyQueue) Push(x int) { q.inbox = append(q.inbox, x) }
func (q *MyQueue) refill() {
if len(q.outbox) == 0 { // only when empty, or the order breaks
for len(q.inbox) > 0 {
q.outbox = append(q.outbox, q.inbox[len(q.inbox)-1])
q.inbox = q.inbox[:len(q.inbox)-1]
}
}
}
func (q *MyQueue) Pop() int {
q.refill()
x := q.outbox[len(q.outbox)-1]
q.outbox = q.outbox[:len(q.outbox)-1]
return x
}
func (q *MyQueue) Peek() int {
q.refill()
return q.outbox[len(q.outbox)-1]
}
func (q *MyQueue) Empty() bool { return len(q.inbox) == 0 && len(q.outbox) == 0 }每个元素记住「到它为止的最小值」每次操作 O(1) · 空间 O(n)
要在 O(1) 里随时拿到栈里的最小(或最大)值:入栈时把「这个元素和它下面所有元素的最小值」一起存进去,出栈时它跟着走,不用重新算。LC 155 最小栈;同样的想法也能给队列(两个这样的栈)加上 O(1) 最值。
Python
class MinStack:
"""A stack with O(1) get_min (LC 155)."""
def __init__(self):
self.items: list[tuple[int, int]] = [] # (value, minimum of it and everything below)
def push(self, x: int) -> None:
low = min(x, self.items[-1][1]) if self.items else x
self.items.append((x, low))
def pop(self) -> None:
self.items.pop()
def top(self) -> int:
return self.items[-1][0]
def get_min(self) -> int:
return self.items[-1][1]C++
// A stack with O(1) getMin (LC 155).
class MinStack {
vector<pair<int, int>> items; // {value, minimum of it and everything below}
public:
void push(int x) {
int low = items.empty() ? x : min(x, items.back().second);
items.push_back({x, low});
}
void pop() { items.pop_back(); }
int top() const {
return items.back().first;
}
int getMin() const {
return items.back().second;
}
};Java
// A stack with O(1) getMin (LC 155).
class MinStack {
private final Deque<int[]> items = new ArrayDeque<>(); // {value, minimum of it and everything below}
void push(int x) {
items.push(new int[] {x, items.isEmpty() ? x : Math.min(x, items.peek()[1])});
}
void pop() {
items.pop();
}
int top() {
return items.peek()[0];
}
int getMin() {
return items.peek()[1];
}
}Go
// MinStack is a stack with O(1) GetMin (LC 155).
type MinStack struct {
items [][2]int // {value, minimum of it and everything below}
}
func (s *MinStack) Push(x int) {
low := x
if n := len(s.items); n > 0 {
low = min(x, s.items[n-1][1])
}
s.items = append(s.items, [2]int{x, low})
}
func (s *MinStack) Pop() { s.items = s.items[:len(s.items)-1] }
func (s *MinStack) Top() int { return s.items[len(s.items)-1][0] }
func (s *MinStack) GetMin() int { return s.items[len(s.items)-1][1] }为什么对
- 不变式:hasNext 返回后,(r, c) 要么指向下一个有效元素,要么 r 已经越过最后一行;next 只读取并前进一格,下一次 hasNext 再恢复不变式。
- 幂等:hasNext 只跳过已读完或为空的行,已经指向有效元素时什么都不改,所以连续调用多次结果一样。
- 均摊 O(1):每一行最多被跳过一次,每个元素只被读一次,总工作量是行数加元素数。
30 秒口述稿
面试官让你讲思路时,按做法、为什么对、复杂度的顺序说。中英文各一版,可以对着读几遍。
迭代器设计我把跳过空行、走到下一个有效元素放在 hasNext 里,next 只负责读取当前元素并前进一格,开头先调用一次 hasNext 防止越界。这样 hasNext 连续调用多次也没有副作用。我只保存行号和列号,不在构造函数里把所有元素拷贝成一个大数组。每行最多被跳过一次,所以每次调用均摊 O(1)。For iterator design I put skipping empty rows and reaching the next valid element in hasNext, while next only reads the current element and steps forward, calling hasNext first to stay in bounds. That way calling hasNext repeatedly has no side effects. I keep only a row and column index instead of copying everything into one big array in the constructor. Each row is skipped at most once, so every call is amortised O(1).自评一下
对照上面的答案给自己打个分。没掌握的题会按 1、3、7、21 天的间隔回来,到期的题在「我的进度 · 到期复习」里。 只存在这台设备上;登录后会合并到你的账号。
小测
3 道概念题和复杂度题。答过的题会和 OA 选择题练习一起按间隔回来复习。 去 OA 选择题练习
作答记录只保存在这台设备的浏览器里;登录后打开OA 选择题练习页,会存进账号。
模板练习
用上面的模板和变体出题:把打乱的行排回原序,把挖掉的边界条件填回去,或者在空白编辑器里整段默写,跑本站的测试并逐行对比。每次「换一题」都会重新出题。
下面几行被打乱了。点一行选中,再点另一行和它交换,排回模板原来的顺序;缩进是提示。
class Flatten2D:分档练习
本站在 500 路里的 8 道题,按基础、核心、进阶分三档。每题附一句话思路,先自己想,想不出再看。
核心 6
本站 OA 真题 22
- Build Blocks and Obstacles on a Number LineMeta
- Perform QueriesAmazon
- Implement Queue using Stacks (Round 2, LC 232Microsoft
- Command Undo Data StructureNetflix
- Chat Event Counts in Recent WindowOpenAI
- In-Memory Database with Backup and RestoreAnthropic
- Obstacles and Blocks on a Number LineUber
- Count Ways to Reach a SumUber
- First Unique IP Hitting the ServerUber
- For All Intents And Purposes Part 1 - Initializing the SystemStripe