1. 项目背景与核心价值
"day57(1.8)——leetcode面试经典150"这个标题背后隐藏着程序员群体中一个持续升温的需求——系统性攻克技术面试中的算法关卡。作为全球知名的编程题库平台,LeetCode的150道经典题目被公认为技术面试的"黄金标准",涵盖数据结构、算法优化、系统设计等核心领域。
我完整刷完这150题用了近两个月时间,期间经历了从暴力解到最优解的思维蜕变。这个系列特别标注"day57(1.8)"的独特之处在于:它采用每日精进+进度追踪的实践模式,通过日期标记既形成学习闭环,又构建可视化的成长轨迹。这种将大目标拆解为daily commit的做法,正是应对复杂算法训练最有效的方法论。
2. 题目分类与战略解析
2.1 题型分布全景图
LeetCode经典150题可划分为六大核心板块:
-
数组/字符串操作(占比32%)
- 典型例题:两数之和(#1)、无重复字符的最长子串(#3)
- 核心考点:双指针、滑动窗口、哈希映射
-
链表/树结构(占比28%)
- 典型例题:反转链表(#206)、二叉树层序遍历(#102)
- 核心考点:递归/迭代转换、指针操作、DFS/BFS
-
动态规划(占比18%)
- 典型例题:爬楼梯(#70)、最长递增子序列(#300)
- 关键特征:状态转移方程、备忘录优化
-
图论算法(占比12%)
- 典型例题:课程表(#207)、岛屿数量(#200)
- 必备技能:邻接表构建、拓扑排序、并查集
-
设计类问题(占比7%)
- 典型例题:LRU缓存(#146)、最小栈(#155)
- 实现要点:数据结构组合、API边界处理
-
数学/位运算(占比3%)
- 典型例题:汉明距离(#461)、多数元素(#169)
- 解题技巧:位掩码、摩尔投票法
2.2 刷题策略黄金法则
根据我的实战经验,推荐采用"三阶递进法":
-
模式识别阶段(1-30天)
- 重点:建立题型-解法映射表
- 技巧:对每道题标注时空复杂度标签
- 工具:使用Notion建立题目特征矩阵
-
深度优化阶段(31-50天)
- 核心:每种解法至少实现3种变体
- 案例:二叉树遍历就有递归/迭代/Morris三种写法
- 指标:将运行时间优化到前10%提交
-
模拟面试阶段(51-57天)
- 方法:使用LeetCode模拟面试功能
- 要求:45分钟内完成2道中等或1道困难题
- 记录:录制屏幕并回放分析卡壳点
避坑提示:避免陷入"AC即止"的陷阱。我曾用暴力法通过#76最小覆盖子串后没有继续优化,后来面试中被要求写滑动窗口解法时险些翻车。
3. 核心算法实现精要
3.1 动态规划四步诀窍
以#322零钱兑换为例,演示DP问题的标准处理流程:
python复制def coinChange(coins, amount):
# 1. 定义DP数组:dp[i]表示金额i的最小硬币数
dp = [float('inf')] * (amount + 1)
dp[0] = 0
# 2. 遍历所有状态
for coin in coins:
for i in range(coin, amount + 1):
# 3. 状态转移方程
dp[i] = min(dp[i], dp[i - coin] + 1)
# 4. 返回有效解
return dp[amount] if dp[amount] != float('inf') else -1
关键学习点:
- 初始化时用
float('inf')表示不可达状态 - 遍历顺序决定是完全背包还是01背包问题
- 空间复杂度可从O(n)优化到O(amount)
3.2 二叉树序列化工程实践
#297二叉树的序列化需要兼顾效率和可读性:
python复制def serialize(root):
""" 层级遍历序列化 """
if not root: return "[]"
queue = collections.deque([root])
res = []
while queue:
node = queue.popleft()
if node:
res.append(str(node.val))
queue.append(node.left)
queue.append(node.right)
else:
res.append("null")
return '[' + ','.join(res) + ']'
def deserialize(data):
""" 注意处理null节点 """
if data == '[]': return None
vals = data[1:-1].split(',')
root = TreeNode(int(vals[0]))
queue = collections.deque([root])
i = 1
while queue and i < len(vals):
node = queue.popleft()
if vals[i] != 'null':
node.left = TreeNode(int(vals[i]))
queue.append(node.left)
i += 1
if vals[i] != 'null':
node.right = TreeNode(int(vals[i]))
queue.append(node.right)
i += 1
return root
工程化要点:
- 使用
'null'明确表示空节点 - 队列操作要注意边界条件
- 字符串处理注意去除首尾括号
4. 高频考点深度剖析
4.1 滑动窗口的三种变体
以#76最小覆盖子串为例,总结滑动窗口的演化形态:
- 固定窗口(如#567字符串排列)
python复制window_size = len(pattern)
for i in range(len(s) - window_size + 1):
window = s[i:i+window_size]
- 可变窗口-最小子串(如#76)
python复制while right < len(s):
window.add(s[right])
while valid(window):
update_result()
window.remove(s[left])
left += 1
right += 1
- 可变窗口-最长子串(如#3)
python复制while right < len(s):
if s[right] in window:
left = max(left, window[s[right]] + 1)
window[s[right]] = right
update_result()
right += 1
4.2 链表操作的六个经典场景
| 问题类型 | 例题编号 | 关键技巧 | 易错点 |
|---|---|---|---|
| 虚拟头节点 | #203 | dummy.next = head | 忘记返回dummy.next |
| 快慢指针 | #141 | slow=fast=head | 循环条件判断顺序 |
| 链表反转 | #206 | prev,curr=None,head | 丢失next指针 |
| 节点删除 | #19 | 先走n步再同步 | 边界条件处理 |
| 链表排序 | #148 | 归并排序+找中点 | 切断链表时机 |
| 复杂链表复制 | #138 | 哈希表+两次遍历 | 随机指针处理顺序 |
5. 实战问题排查指南
5.1 栈溢出常见场景
递归解法在以下情况极易爆栈:
- 树退化为链表时(如#98验证BST)
- 深层递归无剪枝(如#494目标和)
优化方案:
python复制# 尾递归优化示例(Python实际不优化,但写法有参考价值)
def factorial(n, acc=1):
if n == 0: return acc
return factorial(n-1, n*acc)
# 改为显式栈
def dfs(root):
stack = [(root, False)]
while stack:
node, visited = stack.pop()
if visited:
process(node)
else:
stack.append((node, True))
for child in [node.right, node.left]:
if child: stack.append((child, False))
5.2 时空复杂度分析盲区
容易被低估复杂度的典型题目:
-
#131分割回文串
- 表面:O(n^2)
- 实际:O(n*2^n) 因需要检查所有子集
-
#212单词搜索II
- 表面:O(mn*len(word))
- 实际:O(mn*4^L) L是最长单词长度
-
#224基本计算器
- 表面:O(n)
- 实际:O(n^2) 因字符串拼接操作
调试技巧:在LeetCode提交详情里查看测试用例规模,当输入达到10^4量级时,O(n^2)解法必定超时。
6. 效率提升工具箱
6.1 可视化调试技巧
对于树/图类问题,推荐使用以下调试方法:
python复制# 二叉树可视化(需要安装graphviz)
def visualize_tree(root):
from graphviz import Digraph
dot = Digraph()
def traverse(node):
if not node: return
dot.node(str(id(node)), str(node.val))
if node.left:
dot.edge(str(id(node)), str(id(node.left)))
traverse(node.left)
if node.right:
dot.edge(str(id(node)), str(id(node.right)))
traverse(node.right)
traverse(root)
return dot
6.2 测试用例设计模板
覆盖以下六种边界情况能发现90%的代码缺陷:
- 空输入(空数组、空字符串等)
- 极值输入(最大/最小限制值)
- 重复元素(数组含重复数字等)
- 有序/逆序输入(测试排序敏感性)
- 单元素集合(链表只有一个节点等)
- 整数溢出(特别是Python容易忽略)
例如测试#4寻找两个正序数组的中位数:
python复制test_cases = [
([], [1], 1.0), # 空数组
([1,3], [2,2,2,2], 2.0), # 重复元素
([0,0], [-1,0,1], 0.0), # 含负数
([10000], [10001], 10000.5), # 大数
([1,2], [3,4,5,6,7,8], 4.5) # 长度悬殊
]
刷题过程中我最大的体会是:理解每道题背后的设计意图比单纯AC更重要。比如#239滑动窗口最大值,表面考察双端队列,实则训练我们识别问题本质的能力——后来我在实际业务中处理实时流量控制时,就成功复用了这种单调队列的思想。