从刷题到通关 —— 算法面试实战指南
学了那么多数据结构和算法,面试的时候到底怎么用?刷题到底该怎么刷?这一章不讲新知识,只讲一件事:如何把前面学的所有东西变成你的实战能力。
📋 开篇自测:你准备好了吗?
- 面试官给你一道从未见过的算法题,你的前3分钟会做什么?
- 你能在2分钟内说出数组、链表、栈、队列、哈希表、树、堆、图各自最适合解决什么类型的问题吗?
- 当你写完代码后,面试官问”还能优化吗?“——你知道从哪些角度去思考吗?
一、算法面试的本质——考的不是背题
很多人以为算法面试就是”背题”——把LeetCode的题刷几百道,面试时遇到原题就稳了。这个思路有一个致命问题:题目的变体是无穷的,你不可能背完。
算法面试真正考察的是三个能力:
- 问题分析能力: 把模糊的问题转化为精确的数据结构和算法模型
- 编码实现能力: 把思路正确地翻译成可运行的代码
- 沟通优化能力: 清晰地表达你的思路,并在面试官的引导下改进方案
掌握这三个能力,即使遇到没见过的题,你也能从容应对。
二、数据结构选择速查表
面试中最关键的第一步是:选对数据结构。 数据结构选对了,算法往往就自然浮现了。
| 问题特征 | 优先考虑的数据结构 | 典型例题 |
|---|---|---|
| 需要O(1)查找/判重 | 哈希表(dict/set) | 两数之和、判断重复元素 |
| 有序数据中查找 | 二分查找 | 搜索插入位置、旋转数组查找 |
| 需要维护顺序+快速增删 | 链表 | 合并有序链表、反转链表 |
| ”最近”相关的操作 | 栈 | 括号匹配、每日温度、最小栈 |
| ”先来先到”的处理顺序 | 队列 | BFS、任务调度 |
| 需要频繁取最大/最小值 | 堆(优先队列) | 前K大元素、合并K个有序链表 |
| 层级关系、递归结构 | 树 | 路径总和、最近公共祖先 |
| 多对多关系、连通性 | 图 | 课程表(拓扑排序)、岛屿数量 |
| 求最优解 + 有重叠子问题 | 动态规划 | 背包问题、最长子序列 |
| 求所有方案/排列组合 | 回溯 | 全排列、N皇后、子集 |
三、面试解题四步法
拿到一道算法题,不要急着写代码。按这四步走:
第一步:理解问题(1-2分钟)
- 用自己的话复述一遍题意,确保理解正确
- 确认边界条件:空输入怎么办?只有一个元素呢?有负数吗?
- 和面试官确认输入输出格式
# 例题:给定整数数组nums和目标值target,找出和为target的两个数的下标。
# 先问清楚:
# - 同一个元素能用两次吗?(不能)
# - 一定有解吗?(假设一定有)
# - 有多组解时返回哪一组?(任意一组)
第二步:想思路(3-5分钟)
- 先想暴力解法——它永远是你的保底方案
- 分析暴力解法的瓶颈在哪里,能不能用更好的数据结构消除
- 说出你的时间复杂度和空间复杂度
# 两数之和——思路演进
# 暴力法:两层循环,O(n^2)
def two_sum_brute(nums, target):
for i in range(len(nums)):
for j in range(i + 1, len(nums)):
if nums[i] + nums[j] == target:
return [i, j]
# 思考:内层循环在做什么?在找 target - nums[i] 是否存在。
# 这不就是一个"查找"操作吗?用哈希表可以把查找从O(n)降到O(1)!
# 哈希表法:一次遍历,O(n)
def two_sum_hash(nums, target):
seen = {} # 值 -> 下标
for i, num in enumerate(nums):
complement = target - num
if complement in seen:
return [seen[complement], i]
seen[num] = i
print(two_sum_hash([2, 7, 11, 15], 9)) # [0, 1]
第三步:写代码(10-15分钟)
- 先写出代码骨架(函数签名、主要逻辑框架)
- 再填充细节(边界判断、索引处理)
- 变量命名要有意义(别用
a,b,c,用left,right,count)
第四步:验证和优化(3-5分钟)
- 用一两个简单例子手动走一遍代码逻辑
- 检查边界情况(空数组、单元素、全相同元素等)
- 如果面试官问”还能优化吗?“,从时间和空间两个角度思考
四、高频题型与解题模板
题型1:滑动窗口
适用场景: 连续子数组/子串问题,要求满足某种条件的最长或最短窗口。
def sliding_window_template(s):
"""滑动窗口模板(伪代码,需根据具体问题替换收缩条件)"""
left = 0
window = {} # 窗口内的状态
result = 0
for right in range(len(s)):
# 1. 扩大窗口:加入右边元素
char = s[right]
window[char] = window.get(char, 0) + 1
# 2. 收缩窗口:当窗口不满足条件时
# 将下面的条件替换为具体问题的收缩判断,例如:
# while len(window) > k: (最多k个不同字符)
# while window_sum > target: (子数组和超过目标)
while should_shrink(window): # 替换为具体条件
left_char = s[left]
window[left_char] -= 1
left += 1
# 3. 更新结果
result = max(result, right - left + 1)
return result
# 实战:无重复字符的最长子串
def length_of_longest_substring(s):
"""找出不含重复字符的最长子串的长度"""
left = 0
char_index = {} # 字符 -> 最近出现的位置
max_length = 0
for right in range(len(s)):
char = s[right]
if char in char_index and char_index[char] >= left:
left = char_index[char] + 1 # 跳过重复字符
char_index[char] = right
max_length = max(max_length, right - left + 1)
return max_length
print(length_of_longest_substring("abcabcbb")) # 3 ("abc")
print(length_of_longest_substring("bbbbb")) # 1 ("b")
print(length_of_longest_substring("pwwkew")) # 3 ("wke")
题型2:双指针
适用场景: 有序数组/链表上的搜索和匹配问题。
# 实战:盛最多水的容器
def max_area(height):
"""双指针从两端向中间逼近"""
left, right = 0, len(height) - 1
max_water = 0
while left < right:
# 计算当前面积
width = right - left
h = min(height[left], height[right])
max_water = max(max_water, width * h)
# 移动较矮的那一端(因为移动较高的一端不可能让面积更大)
if height[left] < height[right]:
left += 1
else:
right -= 1
return max_water
print(max_area([1, 8, 6, 2, 5, 4, 8, 3, 7])) # 49
题型3:BFS/DFS
适用场景: 树和图的遍历、连通性问题、层级相关问题。
from collections import deque
# 实战:岛屿数量(网格中的连通分量)
def num_islands(grid):
"""计算网格中'1'组成的连通岛屿数量"""
if not grid:
return 0
rows, cols = len(grid), len(grid[0])
count = 0
def bfs(r, c):
queue = deque([(r, c)])
grid[r][c] = '0' # 标记为已访问
while queue:
row, col = queue.popleft()
for dr, dc in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
nr, nc = row + dr, col + dc
if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == '1':
grid[nr][nc] = '0'
queue.append((nr, nc))
for r in range(rows):
for c in range(cols):
if grid[r][c] == '1':
bfs(r, c)
count += 1
return count
grid = [
['1', '1', '0', '0', '0'],
['1', '1', '0', '0', '0'],
['0', '0', '1', '0', '0'],
['0', '0', '0', '1', '1']
]
print(num_islands(grid)) # 3
题型4:二分查找的变体
适用场景: 在有序或半有序数据中查找目标值,或查找满足条件的边界。
# 实战:在旋转排序数组中搜索
def search_rotated(nums, target):
"""旋转数组中的二分查找"""
left, right = 0, len(nums) - 1
while left <= right:
mid = (left + right) // 2
if nums[mid] == target:
return mid
# 判断哪一半是有序的
if nums[left] <= nums[mid]:
# 左半边有序
if nums[left] <= target < nums[mid]:
right = mid - 1
else:
left = mid + 1
else:
# 右半边有序
if nums[mid] < target <= nums[right]:
left = mid + 1
else:
right = mid - 1
return -1
print(search_rotated([4, 5, 6, 7, 0, 1, 2], 0)) # 4
print(search_rotated([4, 5, 6, 7, 0, 1, 2], 3)) # -1
五、复杂度速查表
面试中经常需要快速说出各种操作的复杂度。这张表值得记住:
数据结构操作复杂度
| 数据结构 | 查找 | 插入 | 删除 | 空间 |
|---|---|---|---|---|
| 数组 | O(n) / O(1)按索引 | O(n) | O(n) | O(n) |
| 链表 | O(n) | O(1)已知位置 | O(1)已知位置 | O(n) |
| 哈希表 | O(1)平均 | O(1)平均 | O(1)平均 | O(n) |
| 二叉搜索树 | O(log n)平均 / O(n)最坏 | O(log n)平均 / O(n)最坏 | O(log n)平均 / O(n)最坏 | O(n) |
| AVL树/红黑树 | O(log n) | O(log n) | O(log n) | O(n) |
| 堆 | O(n) / O(1)取极值 | O(log n) | O(log n) | O(n) |
| 栈/队列 | O(n) | O(1) | O(1) | O(n) |
排序算法复杂度
| 算法 | 最优 | 平均 | 最差 | 空间 | 稳定性 |
|---|---|---|---|---|---|
| 冒泡排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 |
| 选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
| 插入排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 |
| 归并排序 | O(n log n) | O(n log n) | O(n log n) | O(n) | 稳定 |
| 快速排序 | O(n log n) | O(n log n) | O(n^2) | O(log n) | 不稳定 |
| 堆排序 | O(n log n) | O(n log n) | O(n log n) | O(1) | 不稳定 |
图与字符串算法复杂度
| 算法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| BFS/DFS | O(V + E) | O(V) | V为顶点数,E为边数 |
| Dijkstra(堆优化) | O((V + E) log V) | O(V) | 不支持负权边 |
| Kruskal | O(E log E) | O(V) | 需要并查集 |
| KMP | O(n + m) | O(m) | n为文本长度,m为模式长度 |
| BM(完整版) | O(n/m) 平均,O(n*m) 最坏 | O(m) | 实际中通常最快;加入Galil规则后最坏可优化为O(n+m) |
六、刷题策略——质量远比数量重要
按专题刷,不要随机刷
随机刷题就像把10种药混在一起吃——效果不好还容易晕。正确的做法是按专题集中突破:
- 第一周: 数组 + 哈希表(两数之和、三数之和、移动零、有效的字母异位词)
- 第二周: 链表 + 栈 + 队列(反转链表、合并链表、有效括号、每日温度)
- 第三周: 二叉树 + BFS/DFS(最大深度、翻转二叉树、层序遍历、验证BST)
- 第四周: 二分查找 + 排序(搜索插入位置、旋转数组、前K大元素)
- 第五周: 动态规划(爬楼梯、最长子序列、背包问题、编辑距离)
- 第六周: 回溯 + 贪心(全排列、子集、组合总和、活动选择)
每道题至少做三遍
- 第一遍: 花30分钟尝试,实在想不出就看题解。重点是理解思路
- 第二遍: 隔一天,不看任何提示,独立写出完整代码。重点是记住模式
- 第三遍: 隔一周,再写一次。重点是检验内化程度
建立自己的题型笔记
每做完一类题,用自己的话总结:
题型:滑动窗口
核心思路:维护一个窗口[left, right],right不断右移扩大窗口,
当窗口不满足条件时left右移收缩窗口。
关键细节:窗口内用哈希表维护状态,注意更新结果的时机。
典型题目:无重复最长子串、最小覆盖子串、字母异位词。
我曾经犯的错:忘记在收缩窗口时更新哈希表的计数。
七、面试中的沟通技巧
算法面试不是闭卷考试。你要像和同事讨论技术方案一样,和面试官交流。
技巧1:先说暴力解法
即使你一眼看出了最优解,也建议先说暴力解法。这展示了你的分析过程,也给自己留了退路。
“最直接的方法是两层循环暴力搜索,时间复杂度O(n^2)。但如果我们用哈希表来存已经遍历过的元素,就可以把内层循环的查找降到O(1),整体变成O(n)。”
技巧2:写代码前先说清楚思路
面试官更看重思路而非代码。在写代码之前,用1-2句话概括你的方案。
技巧3:卡住时主动沟通
卡住了不丢人,闷头想5分钟才丢人。你可以说:
“我现在的思路是…,但在处理…这个部分遇到了困难。我在想是不是可以用…来解决?”
面试官通常会给出有用的提示。
技巧4:分析完复杂度后主动提优化
写完代码后,主动说出时间和空间复杂度,再想想有没有优化空间。
“当前方案时间O(n log n),空间O(n)。如果要优化空间到O(1)的话,可以考虑原地操作…”
📝 掌握度自测
- 分析题: 给你一个无序数组,要求找出第K大的元素。你能想出几种方法?每种方法的时间复杂度是多少?哪种最优?
- 编码题: 实现一个LRU缓存——支持O(1)的get和put操作,容量满时淘汰最久没使用的元素。(提示:哈希表 + 双向链表)
- 设计题: 如果面试官让你设计一个自动补全系统(像搜索引擎的搜索框),你会用什么数据结构?如何处理按热度排序?
- 综合题: 拿到这道题:“给定一个二维网格和一个单词,判断单词是否存在于网格中(相邻字符可以上下左右连接,同一格不能重复使用)“。按四步法,写出你的完整解题过程。
- 反思题: 回顾本课程前10章,选出你最薄弱的3个知识点,为每个知识点制定一个具体的强化计划。
💡 自我评估
- 全部答对:你已经具备了扎实的面试实战能力,继续保持这个手感。
- 答对3-4题:基础扎实,建议重点练习编码速度和沟通表达。
- 答对1-2题:回到前面的章节,把每个数据结构的增删查改操作再实现一遍,然后回来做这些题。
- 全部答错:不要气馁。算法能力需要时间积累。建议从”刷题策略”中的第一周开始,每天只做1-2题,坚持6周。
面试技巧讲完了,但我们的数据结构之旅还没有结束。前面的章节中,有两个重要的数据结构被刻意留到了后面——因为它们的理解需要以前面的知识为基础。下一章,我们来认识”堆与优先队列”——一种能在 O(log n) 时间内找到最大值或最小值的数据结构,它在 Top-K 问题、任务调度、合并有序序列等场景中大显身手。
购买课程解锁全部内容
刷题到通关:数据结构与算法面试实战
¥29.90