1. 为什么刷题要从遍历开始?
作为一名从大厂面试官转型的算法教练,我见过太多新手一上来就死磕动态规划,结果连最基本的遍历都写不利索。遍历是算法世界的"扎马步",看似简单却蕴含了递归、迭代、边界处理等核心编程思维。
去年我带的一个转行学员,花了两个月专攻各类遍历题目,后来在面试中遇到一道变形的层序遍历题,15分钟就写出了bug-free的代码。面试官当场感叹:"这是我今年见过最稳健的遍历实现。"
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 遍历题型的四大金刚
2.1 线性结构的遍历艺术
数组和链表这类线性结构的遍历,新手常犯三个错误:
- 边界条件处理不当(特别是空数组和单元素情况)
- 循环变量更新位置错误
- 忘记处理遍历中断条件
以经典的二分查找为例,正确的遍历框架应该是:
python复制left, right = 0, len(nums) - 1
while left <= right: # 注意等号
mid = left + (right - left) // 2 # 防溢出写法
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1 # 必须+1
else:
right = mid - 1 # 必须-1
return -1
关键点:循环条件中的等号决定是否包含边界,mid计算方式影响大数处理,左右指针更新必须跳过mid避免死循环
2.2 二叉树遍历的六种姿势
前序、中序、后序的递归写法是基础中的基础,但实际面试中更常考迭代实现。以中序遍历为例,栈实现的精妙之处在于:
python复制def inorderTraversal(root):
stack = []
res = []
curr = root
while curr or stack:
while curr: # 深入左子树
stack.append(curr)
curr = curr.left
curr = stack.pop() # 回溯父节点
res.append(curr.val)
curr = curr.right # 转向右子树
return res
层序遍历的BFS写法也有讲究,我建议使用队列长度控制遍历层次:
python复制from collections import deque
def levelOrder(root):
if not root: return []
queue = deque([root])
res = []
while queue:
level_size = len(queue) # 关键点!
level = []
for _ in range(level_size):
node = queue.popleft()
level.append(node.val)
if node.left: queue.append(node.left)
if node.right: queue.append(node.right)
res.append(level)
return res
2.3 图的遍历陷阱
DFS和BFS在图中的应用比树更复杂,主要难点在于:
- 环的检测(需要visited集合)
- 邻接表的构建方式
- 多源点情况处理
以岛屿数量问题为例,标准的DFS遍历模板:
python复制def numIslands(grid):
def dfs(i, j):
if not (0 <= i < m and 0 <= j < n):
return
if grid[i][j] != '1':
return
grid[i][j] = '0' # 标记已访问
for di, dj in [(-1,0),(1,0),(0,-1),(0,1)]:
dfs(i+di, j+dj)
count = 0
m, n = len(grid), len(grid[0])
for i in range(m):
for j in range(n):
if grid[i][j] == '1':
count += 1
dfs(i, j)
return count
踩坑提醒:不要直接修改原数组(除非允许),面试时应该先深拷贝一份。Python中用copy.deepcopy(),但要注意二维数组的特殊处理。
2.4 特殊结构的遍历技巧
嵌套结构(如JSON)、跳表、多级链表等,需要灵活运用递归或双指针。以扁平化嵌套列表为例:
python复制def flatten(nestedList):
stack = nestedList[::-1]
res = []
while stack:
curr = stack.pop()
if curr.isInteger():
res.append(curr.getInteger())
else:
stack.extend(curr.getList()[::-1]) # 关键倒序入栈
return res
3. 遍历算法的五个段位
3.1 青铜:会写标准模板
能正确实现各类基础遍历,处理简单变种(如二叉树的反向层序遍历)
3.2 白银:掌握时空复杂度分析
清楚知道:
- 递归遍历的隐式栈空间消耗
- 迭代实现中容器的最坏情况
- 缓存友好性对性能的影响
3.3 黄金:灵活处理边界条件
包括但不限于:
- 空输入处理
- 极值测试(如超长链表)
- 特殊结构(环形链表、带环图)
3.4 铂金:设计遍历终止条件
典型场景:
- 提前终止的DFS(如找到路径就返回)
- 带剪枝的BFS(如单词接龙中的已访问记录)
- 多条件终止(如满足某阈值立即返回)
3.5 钻石:改造遍历过程
高阶技巧包括:
- 在遍历时同步完成其他操作(如链表反转)
- 利用遍历特性解决问题(如判断BST的中序单调性)
- 自定义遍历顺序(如之字形打印二叉树)
4. 刷题路线图建议
根据300+学员的实战数据,我总结出遍历题的最佳攻克顺序:
- 线性结构(数组/链表)→ 2. 二叉树 → 3. 多叉树/N叉树 → 4. 图 → 5. 特殊结构
必刷题清单:
- 数组:27移除元素、54螺旋矩阵、59螺旋矩阵II
- 链表:206反转链表、141环形链表、234回文链表
- 二叉树:102层序遍历、94中序遍历、145后序遍历
- 图:200岛屿数量、207课程表、133克隆图
- 综合:341扁平化嵌套列表迭代器、173二叉搜索树迭代器
5. 调试遍历代码的秘籍
当遍历代码出现问题时,我的三板斧:
- 打印法:在关键节点输出变量状态(如递归的每层参数)
- 小黄鸭法:对空气逐行解释代码逻辑
- 边界测试法:构造最小出错用例(如空树、单节点树)
以二叉树遍历为例,常见的debug检查点:
- 递归:终止条件是否覆盖所有基本情况
- 迭代:栈/队列的push/pop操作是否成对出现
- 层序遍历:是否正确处理了空子节点
6. 遍历算法的工程实践
在实际项目中,遍历算法最常见的三种应用场景:
- 数据预处理:如DOM树的深度优先扫描
- 状态检测:如游戏地图的可达性分析
- 批量操作:如文件系统的递归删除
一个真实的性能优化案例:某电商平台的商品分类树渲染,将递归实现的DFS改为迭代+记忆化后,页面加载时间从1200ms降至400ms。关键改动点:
javascript复制// 优化前
function renderTree(node) {
let html = `<li>${node.name}`;
if (node.children) {
html += '<ul>';
node.children.forEach(child => {
html += renderTree(child);
});
html += '</ul>';
}
html += '</li>';
return html;
}
// 优化后
function renderTree(root) {
const stack = [{node: root, html: ''}];
let result = '';
while (stack.length) {
const {node, html} = stack.pop();
let currentHtml = html + `<li>${node.name}`;
if (node.children) {
currentHtml += '<ul>';
// 倒序压栈保证渲染顺序
for (let i = node.children.length - 1; i >= 0; i--) {
stack.push({node: node.children[i], html: ''});
}
stack.push({node: null, html: currentHtml + '</ul></li>'});
} else {
result += currentHtml + '</li>';
}
}
return result;
}
7. 高频面试考点精析
根据近半年一线大厂的面试记录,遍历相关的高频考点包括:
- 迭代实现二叉树遍历(占67%)
- 图的遍历应用(如最短路径问题,占52%)
- 遍历过程中的状态维护(占45%)
- 多线程环境下的遍历安全(占28%)
一个典型的进阶问题:"如何用常量空间实现二叉树的中序遍历?" 答案就是Morris遍历:
python复制def morrisTraversal(root):
curr = root
res = []
while curr:
if not curr.left:
res.append(curr.val)
curr = curr.right
else:
# 找前驱节点
pre = curr.left
while pre.right and pre.right != curr:
pre = pre.right
if not pre.right:
pre.right = curr # 建立线索
curr = curr.left
else:
pre.right = None # 拆除线索
res.append(curr.val)
curr = curr.right
return res
这个算法的精妙之处在于利用叶子节点的空指针存储回溯信息,将空间复杂度从O(n)降到O(1)。我在面试中看到能完整写出这个实现的候选人,都会额外加分。
