我用Python刷完了LeetCode HOT 100:这是我最常用的10个解题模板和5个易错点
去年夏天,我给自己定下了一个小目标:用Python完整刷完LeetCode HOT 100题目。三个月后,当我终于完成最后一道hard题时,代码提交记录已经积累了超过200次尝试。这段经历让我深刻体会到,算法刷题不是简单的记忆题解,而是需要建立自己的解题框架库。今天,我想分享那些真正高频出现、能解决80%问题的核心代码模板,以及那些看似简单却让我反复栽跟头的"坑"。
1. 高频解题模板:从双指针到动态规划
1.1 双指针的三种经典范式
双指针是我在数组和字符串类题目中使用最频繁的技巧,根据场景不同可以分为三种典型模式:
同向快慢指针(常用于去重、删除元素):
def removeDuplicates(nums): slow = 0 for fast in range(len(nums)): if nums[fast] != nums[slow]: slow += 1 nums[slow] = nums[fast] return slow + 1相向指针(适用于有序数组的两数之和等问题):
def twoSum(numbers, target): left, right = 0, len(numbers)-1 while left < right: sum_ = numbers[left] + numbers[right] if sum_ == target: return [left+1, right+1] elif sum_ < target: left += 1 else: right -= 1滑动窗口(解决子串/子数组问题)的通用模板:
def slidingWindow(s, t): window = defaultdict(int) need = defaultdict(int) for c in t: need[c] += 1 left = right = valid = 0 while right < len(s): c = s[right] right += 1 # 窗口内数据更新 while (window needs shrink): d = s[left] left += 1 # 窗口内数据更新 return result1.2 回溯算法的标准化改造
回溯题最容易写出冗长的代码,我总结出这个通用框架后,解组合、排列类题目效率大幅提升:
def backtrack(路径, 选择列表): if 满足结束条件: 结果.append(路径.copy()) return for 选择 in 选择列表: if 选择不合法: continue 做选择 backtrack(路径, 选择列表) 撤销选择实际应用时,根据题目特点填充以下关键点:
- 选择合法性判断(如去重)
- 路径记录方式(通常用list)
- 终止条件(如达到目标长度)
1.3 动态规划的状态转移方程库
经过大量练习,我发现DP问题可以归纳为几类核心状态转移模式:
线性DP(如打家劫舍、股票问题):
dp[i] = max(dp[i-1], dp[i-2] + nums[i])二维DP(矩阵路径类问题):
dp[i][j] = dp[i-1][j] + dp[i][j-1]背包问题的通用解法框架:
dp = [0] * (target + 1) dp[0] = 1 # 初始化 for num in nums: for i in range(target, num-1, -1): # 01背包倒序 dp[i] += dp[i - num]2. Python特有的五个"深坑"
2.1 可变对象作为默认参数
这个坑让我在回溯题上浪费了整整两小时:
# 错误写法 def dfs(path=[]): pass # 正确写法 def dfs(path=None): path = path or []Python的函数默认参数在定义时就被创建,所有调用共享同一个列表对象。
2.2 浅拷贝导致的意外修改
当需要保存中间状态时,直接赋值会导致引用传递:
result = [] path = [1,2,3] result.append(path) # 错误!后续修改path会影响result中的值 result.append(path.copy()) # 正确2.3 整数除法与浮点精度
Python 3中/是真除法,//是地板除。在二分查找时:
mid = (left + right) // 2 # 避免溢出更安全的写法:left + (right - left) // 22.4 字典键的存在性检查
判断key是否存在的三种方式对比:
if key in dict: # 最Pythonic if dict.get(key): # 可能误判None/False if dict[key]: # KeyError风险2.5 循环中修改迭代对象
在遍历list时删除元素会导致意外跳过:
# 错误方式 for i in range(len(nums)): if nums[i] == target: del nums[i] # 后续元素前移导致漏检 # 正确方式 i = 0 while i < len(nums): if nums[i] == target: del nums[i] else: i += 13. 效率优化:从暴力解到AC的进阶路径
3.1 时间复杂度优化对照表
| 问题类型 | 暴力解法 | 优化方案 | 典型例题 |
|---|---|---|---|
| 两数之和 | O(n²) 双重循环 | O(n) 哈希表 | #1 Two Sum |
| 连续子数组最大和 | O(n²) 枚举 | O(n) Kadane算法 | #53 Maximum Subarray |
| 字符串匹配 | O(mn) 逐个比较 | O(n) KMP算法 | #28 Implement strStr() |
3.2 空间换时间的典型场景
- 前缀和:频繁查询区间和(#560)
- 记忆化:递归中存在重复计算(#70 Climbing Stairs)
- 哈希缓存:快速查找元素(#3 Longest Substring)
# 前缀和应用示例 def subarraySum(nums, k): prefix = {0: 1} res = s = 0 for num in nums: s += num res += prefix.get(s - k, 0) prefix[s] = prefix.get(s, 0) + 1 return res4. 调试技巧:如何快速定位边界条件错误
4.1 必须测试的边界case
- 空输入([]、""、None)
- 单元素/双元素情况
- 极值(最大/最小整数)
- 完全升序/降序排列
- 所有元素相同的情况
4.2 防御性编程检查清单
- 循环终止条件是否包含等号?
- 数组访问前是否检查了索引有效性?
- 除法操作是否有除零保护?
- 递归是否有终止条件?
- 变量初始化是否覆盖所有分支?
经验:当你的代码通过示例但提交失败时,优先检查循环的边界索引和递归的终止条件
5. 我的刷题工具箱:实用代码片段
5.1 数据结构速建
# 带虚拟头的链表 dummy = ListNode(-1) dummy.next = head # 二叉树节点定义 class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right5.2 常用工具函数
# 快速反转字符串 s[::-1] # 统计元素频率 from collections import Counter count = Counter(nums) # 堆操作 import heapq heapq.heapify(arr) val = heapq.heappop(arr)刷完HOT 100的最大收获不是记住了多少题解,而是建立了遇到新问题时快速拆解的能力。现在回头看最初花三小时都解不出来的medium题,往往能在二十分钟内找到优化方向。这大概就是所谓的"题感"吧——知道该用什么工具,也清楚哪里可能有坑。