第一章:LeetCode Top 100 概述与学习路径
LeetCode Top 100 是全球技术公司面试中高频出现的经典算法题集合,被广泛认为是准备数据结构与算法面试的黄金标准。这些题目覆盖了数组、链表、树、动态规划、图论等核心主题,难度分布合理,适合从初级到高级开发者系统性提升编码与问题拆解能力。
学习价值与适用人群
该题库不仅帮助巩固基础算法思想,还能训练在限定时间内写出高效、可读代码的能力。适用于准备技术面试的应届生、转行者,以及希望提升系统设计与逻辑思维的在职工程师。
高效学习路径建议
遵循“分类刷题 + 复盘总结”的模式效果最佳:
- 第一阶段:按类型集中突破
将题目分为数组/字符串、链表、二叉树、回溯、动态规划等类别,逐个攻克。 - 第二阶段:限时模拟
使用 LeetCode 的计时功能,每题控制在25分钟内完成,模拟真实面试压力。 - 第三阶段:反复回顾错题
建立个人题解笔记,记录易错点与优化思路。
常见题型分布示例:
| 类型 | 题目数量 | 典型代表 |
|---|---|---|
| 数组/双指针 | 20 | Two Sum, Container With Most Water |
| 树 | 18 | Invert Binary Tree, Maximum Depth of Binary Tree |
| 动态规划 | 15 | Climbing Stairs, House Robber |
工具与实践建议
使用以下命令初始化本地刷题环境,便于保存与测试代码:
# 创建刷题目录
mkdir leetcode-top100 && cd leetcode-top100
# 为每道题创建独立文件(以Two Sum为例)
touch 001_two_sum.py
# 在文件中编写带注释的解法
坚持每日一题,结合提交后的运行结果与最优解对比,逐步形成自己的解题直觉与模板库,是掌握 Top 100 的关键。
第二章:数组与字符串高频题解析
2.1 数组遍历与双指针技巧理论基础
数组遍历是算法设计中最基础的操作之一。传统的单指针遍历通过线性扫描访问每个元素,时间复杂度为 O(n)。但在某些场景下,使用双指针技巧可以显著提升效率或简化逻辑。
双指针的基本模式
双指针法通常分为两类:
- 同向指针:两个指针从同一端出发,常用于滑动窗口问题;
- 相向指针:指针从两端向中间移动,适用于有序数组的两数之和等问题。
# 相向双指针示例:在有序数组中查找两数之和等于目标值
def two_sum_sorted(arr, target):
left, right = 0, len(arr) - 1
while left < right:
current_sum = arr[left] + arr[right]
if current_sum == target:
return [left, right]
elif current_sum < target:
left += 1 # 左指针右移增大和
else:
right -= 1 # 右指针左移减小和
该代码利用数组有序特性,每次比较后都能安全地移动一个指针,避免暴力枚举,将时间复杂度优化至 O(n)。
空间与时间权衡
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 暴力遍历 | O(n²) | O(1) | 无序数据,小规模输入 |
| 哈希表辅助 | O(n) | O(n) | 允许额外空间 |
| 双指针 | O(n) | O(1) | 数组有序 |
mermaid 流程图可表示相向双指针的决策过程:
graph TD
A[初始化 left=0, right=n-1] --> B{left < right?}
B -->|否| C[结束]
B -->|是| D[计算 arr[left] + arr[right]]
D --> E{等于目标?}
E -->|是| F[返回索引]
E -->|小于| G[left++]
E -->|大于| H[right--]
G --> B
H --> B
2.2 实战:两数之和与三数之和优化方案
两数之和:哈希表加速查找
使用哈希表将时间复杂度从 $O(n^2)$ 优化至 $O(n)$。遍历数组时,检查目标差值是否已在表中。
def two_sum(nums, target):
seen = {}
for i, num in enumerate(nums):
diff = target - num
if diff in seen:
return [seen[diff], i]
seen[num] = i
seen存储已遍历的数值与索引;- 每次计算
target - num,若存在即返回索引对。
三数之和:排序 + 双指针
先排序,固定一个数,用双指针在剩余区间找互补对,避免重复组合。
| 方法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 暴力枚举 | O(n³) | O(1) |
| 排序+双指针 | O(n²) | O(1) |
优化路径对比
graph TD
A[两数之和暴力解] --> B[引入哈希表]
C[三数之和暴力解] --> D[排序+双指针]
B --> E[线性时间求解]
D --> F[减少一层循环]
2.3 字符串操作核心方法与性能分析
字符串是编程中最常用的数据类型之一,其操作效率直接影响程序整体性能。掌握核心方法并理解其底层机制,是优化代码的关键。
常见字符串操作方法对比
Python 提供了丰富的字符串方法,其中 join()、format()、f-string 和 + 拼接在使用频率和性能上差异显著。
| 方法 | 示例 | 时间复杂度 | 适用场景 |
|---|---|---|---|
+ 拼接 |
s = a + b + c |
O(n²) | 少量拼接 |
join() |
''.join([a,b,c]) |
O(n) | 多字符串拼接 |
| f-string | f"{a}{b}{c}" |
O(1) per var | 格式化输出 |
性能关键:避免重复创建对象
由于字符串不可变,每次 + 操作都会创建新对象。大量循环中应避免使用:
# 低效:O(n²)
result = ""
for item in data:
result += str(item)
# 高效:O(n)
result = ''.join(str(item) for item in data)
join() 将所有元素一次性合并,减少内存分配次数,显著提升性能。
2.4 实战:最长无重复子串与回文串判断
滑动窗口解决最长无重复子串
使用滑动窗口算法可高效求解最长无重复字符子串。维护一个哈希表记录字符最近出现的位置,动态调整窗口左边界。
def lengthOfLongestSubstring(s):
char_index = {}
left = 0
max_len = 0
for right in range(len(s)):
if s[right] in char_index and char_index[s[right]] >= left:
left = char_index[s[right]] + 1 # 缩小窗口
char_index[s[right]] = right
max_len = max(max_len, right - left + 1)
return max_len
char_index 存储字符最新索引,left 标记窗口起始位置。当字符重复且在窗口内时,移动 left 至上次出现位置的下一位。
中心扩展法判断回文串
回文串可通过中心扩展法验证,枚举每个可能的中心点并向外扩展。
| 中心类型 | 示例(”aba”) | 扩展方向 |
|---|---|---|
| 单字符中心 | ‘b’为中心 | 向左右对称扩展 |
| 双字符中心 | ‘ab’间缝隙 | 处理偶数长度情况 |
该方法时间复杂度为 O(n²),适合短字符串场景。
2.5 边界处理与算法鲁棒性设计实践
在实际系统开发中,边界条件往往是引发异常的根源。良好的鲁棒性设计需从输入校验、异常兜底和容错机制三方面入手。
输入校验与防御式编程
对所有外部输入进行类型、范围和格式验证,是防止非法数据破坏逻辑的第一道防线:
def divide(a: float, b: float) -> float:
if abs(b) < 1e-10:
raise ValueError("除数不能为零")
return a / b
该函数通过阈值判断避免浮点数精度导致的“近零”误判,
1e-10作为容差边界,提升数值稳定性。
异常传播与降级策略
采用分层异常处理机制,关键操作应具备自动降级能力。例如缓存失效时回退至数据库查询。
| 场景 | 处理方式 | 降级方案 |
|---|---|---|
| 网络超时 | 重试3次 | 返回本地缓存 |
| 数据格式错误 | 捕获并记录日志 | 使用默认配置 |
容错流程设计
使用流程图明确异常流转路径:
graph TD
A[接收请求] --> B{参数合法?}
B -->|是| C[执行核心逻辑]
B -->|否| D[返回400错误]
C --> E{成功?}
E -->|是| F[返回结果]
E -->|否| G[触发降级]
G --> H[返回默认值]
第三章:链表与树的经典题目剖析
3.1 链表反转与环检测算法原理
链表反转是基础但关键的操作,常用于优化遍历路径。通过迭代方式,逐个调整节点的指针方向:
def reverse_list(head):
prev = None
curr = head
while curr:
next_temp = curr.next # 临时保存下一个节点
curr.next = prev # 反转当前节点指针
prev = curr # 移动prev和curr
curr = next_temp
return prev # 新的头节点
该算法时间复杂度为 O(n),空间复杂度 O(1),核心在于避免指针丢失。
环检测:Floyd判圈算法
使用快慢双指针检测链表中是否存在环:
def has_cycle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
return True
return False
slow 每步走一格,fast 走两格,若两者相遇则存在环。该方法高效且无需额外存储。
| 方法 | 时间复杂度 | 空间复杂度 | 是否可定位环入口 |
|---|---|---|---|
| 哈希表法 | O(n) | O(n) | 是 |
| Floyd算法 | O(n) | O(1) | 是(扩展实现) |
mermaid 流程图如下:
graph TD
A[开始] --> B{head为空?}
B -- 是 --> C[返回None]
B -- 否 --> D[prev = None, curr = head]
D --> E{curr不为空}
E -- 是 --> F[next_temp = curr.next]
F --> G[curr.next = prev]
G --> H[prev = curr]
H --> I[curr = next_temp]
I --> E
E -- 否 --> J[返回prev]
3.2 实战:合并两个有序链表与LRU缓存实现
合并两个有序链表
在链表操作中,合并两个升序链表是典型双指针应用场景。通过维护一个虚拟头节点,可简化边界处理:
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def mergeTwoLists(l1: ListNode, l2: ListNode) -> ListNode:
dummy = ListNode()
current = dummy
while l1 and l2:
if l1.val <= l2.val:
current.next = l1
l1 = l1.next
else:
current.next = l2
l2 = l2.next
current = current.next
current.next = l1 or l2
return dummy.next
该算法时间复杂度为 O(m+n),空间复杂度 O(1)。dummy 节点避免了对首节点的特殊判断,current 指针驱动归并过程。
LRU缓存机制设计
LRU(Least Recently Used)需满足:
- 快速查找 → 哈希表
- 维护访问顺序 → 双向链表
| 操作 | 时间复杂度 | 数据结构支持 |
|---|---|---|
| get | O(1) | 哈希表 |
| put | O(1) | 哈希表+双向链表 |
graph TD
A[Put Operation] --> B{Key Exists?}
B -->|Yes| C[Update Value & Move to Head]
B -->|No| D{Reach Capacity?}
D -->|Yes| E[Remove Tail Node]
D -->|No| F[Create New Node]
F --> G[Add to Head]
3.3 二叉树递归与迭代遍历统一框架
统一思想:栈与状态标记
无论是先序、中序还是后序遍历,递归的本质是函数调用栈的自动管理。而迭代实现的关键在于手动模拟栈行为,并通过状态标记控制节点访问顺序。
核心策略:节点与状态绑定
使用栈存储元组 (node, status),其中 status 表示该节点是否已被“处理过”。例如:
status = False:未处理,需将其子节点入栈;status = True:已展开,可输出值。
def inorderTraversal(root):
stack = [(root, False)]
result = []
while stack:
node, visited = stack.pop()
if not node:
continue
if visited:
result.append(node.val)
else:
# 按逆序入栈:右 → 自身(标记为True) → 左
stack.append((node.right, False))
stack.append((node, True))
stack.append((node.left, False))
return result
逻辑分析:通过显式控制访问顺序,将三种遍历方式统一为“延迟处理”模式。改变入栈顺序即可切换遍历类型,具备高度可扩展性。
| 遍历类型 | 入栈顺序(右→根→左) |
|---|---|
| 先序 | 根 → 左 → 右 |
| 中序 | 右 → 根 → 左 |
| 后序 | 根 → 右 → 左 |
流程图示意
graph TD
A[开始] --> B{栈非空?}
B -->|否| C[结束]
B -->|是| D[弹出节点与状态]
D --> E{是否已访问?}
E -->|是| F[加入结果]
E -->|否| G[右子入栈(未访问)]
G --> H[自身入栈(已访问)]
H --> I[左子入栈(未访问)]
F --> B
I --> B
第四章:动态规划与回溯算法精讲
4.1 动态规划状态定义与转移方程构建
动态规划的核心在于状态的合理定义与转移方程的准确构建。恰当的状态设计能将复杂问题拆解为可递推的子问题。
状态定义的关键原则
- 无后效性:当前状态仅依赖于之前状态,不受未来决策影响。
- 完备性:状态需涵盖所有可能情形,确保覆盖全部解空间。
转移方程构建步骤
- 分析问题的最优子结构
- 明确状态变量的物理意义
- 推导状态间的依赖关系
以经典的0-1背包问题为例:
# dp[i][w] 表示前i个物品在容量w下的最大价值
dp = [[0] * (W + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
for w in range(W + 1):
if weights[i-1] <= w:
dp[i][w] = max(dp[i-1][w], dp[i-1][w-weights[i-1]] + values[i-1])
else:
dp[i][w] = dp[i-1][w]
该代码中,dp[i][w] 的状态转移体现了“选或不选”第 i 个物品的决策过程。若物品重量超过当前容量,则继承前一状态;否则取两种选择的最大值,确保每一步都保留最优解路径。
4.2 实战:最大子数组和与背包问题变种
动态规划在实际问题中展现出极强的建模能力,本节通过两个经典问题的变种揭示其灵活应用。
最大子数组和的扩展
当要求子数组和最大且长度至少为k时,需调整状态定义。维护前缀和并限制窗口大小:
def maxSubArraySumWithLengthK(nums, k):
n = len(nums)
pre_sum = [0] * (n + 1)
for i in range(n):
pre_sum[i+1] = pre_sum[i] + nums[i]
# 初始化前k个元素的和
max_sum = pre_sum[k] - pre_sum[0]
min_prefix = pre_sum[0] # 可用于更新的最小前缀和
for i in range(k, n):
# 更新可选的最小前缀(至少距离当前k)
min_prefix = min(min_prefix, pre_sum[i - k + 1])
current_sum = pre_sum[i + 1] - min_prefix
max_sum = max(max_sum, current_sum)
return max_sum
该算法通过维护滑动窗口内的最小前缀和,确保子数组长度合规,时间复杂度O(n)。
有容量限制的背包变形
考虑物品可重复使用但总重量不超过W,且价值最大化:
| 物品 | 重量 | 价值 |
|---|---|---|
| A | 2 | 3 |
| B | 3 | 5 |
| C | 4 | 6 |
使用一维DP数组,dp[w]表示重量w下的最大价值,转移方程:
dp[w] = max(dp[w], dp[w-weight[i]] + value[i])
4.3 回溯算法框架与剪枝优化策略
回溯算法是一种系统性搜索解空间的递归技术,常用于组合、排列、子集等问题。其核心思想是“尝试所有可能路径,遇到不满足条件的情况立即回退”。
基本框架
def backtrack(path, choices, result):
if 满足结束条件:
result.append(path[:]) # 保存副本
return
for choice in choices:
if 剪枝条件: continue # 提前终止无效分支
path.append(choice) # 做选择
backtrack(path, choices, result)
path.pop() # 撤销选择
上述模板中,path 记录当前路径,choices 表示可选列表,result 收集合法解。关键在于“做选择”与“撤销选择”之间的对称操作,确保状态正确回滚。
剪枝优化策略
有效的剪枝能显著降低时间复杂度:
- 约束剪枝:在进入递归前检查是否满足约束条件;
- 限界剪枝:预判后续路径无法产生更优解时提前终止;
- 去重剪枝:通过排序避免重复枚举相同组合。
| 剪枝类型 | 触发时机 | 效果 |
|---|---|---|
| 约束剪枝 | 递归前判断 | 减少无效调用 |
| 限界剪枝 | 搜索过程中 | 提升最优解效率 |
| 去重剪枝 | 遍历选择时 | 避免重复解 |
执行流程可视化
graph TD
A[开始] --> B{满足结束条件?}
B -->|是| C[保存结果]
B -->|否| D[遍历可选列表]
D --> E{需剪枝?}
E -->|是| F[跳过该分支]
E -->|否| G[做选择]
G --> H[递归下一层]
H --> I[撤销选择]
F --> J[继续下一选项]
I --> J
J --> B
4.4 实战:全排列与N皇后问题Go实现
全排列的回溯实现
在算法实践中,全排列是回溯法的经典应用。通过递归交换元素位置,可生成所有可能的排列组合。
func permute(nums []int) [][]int {
var result [][]int
backtrack(nums, 0, &result)
return result
}
func backtrack(nums []int, start int, result *[][]int) {
if start == len(nums) {
temp := make([]int, len(nums))
copy(temp, nums)
*result = append(*result, temp)
return
}
for i := start; i < len(nums); i++ {
nums[start], nums[i] = nums[i], nums[start] // 交换
backtrack(nums, start+1, result) // 递归
nums[start], nums[i] = nums[i], nums[start] // 回溯
}
}
上述代码通过 backtrack 函数在每个位置尝试所有未使用的元素。参数 start 表示当前决策层,当其等于数组长度时记录结果。每次交换后递归进入下一层,返回后恢复原状以保证状态正确。
N皇后问题建模
N皇后问题要求在 N×N 棋盘上放置 N 个皇后,使其互不攻击。使用列、主对角线和副对角线集合剪枝,提升搜索效率。
func solveNQueens(n int) [][]string {
var res [][]string
board := make([][]byte, n)
for i := range board {
board[i] = make([]byte, n)
for j := range board[i] {
board[i][j] = '.'
}
}
cols, diag1, diag2 := map[int]bool{}, map[int]bool{}, map[int]bool{}
dfs(&res, board, cols, diag1, diag2, 0, n)
return res
}
func dfs(res *[][]string, board [][]byte, cols, diag1, diag2 map[int]bool, row, n int) {
if row == n {
solution := make([]string, n)
for i, row := range board {
solution[i] = string(row)
}
*res = append(*res, solution)
return
}
for col := 0; col < n; col++ {
d1, d2 := row-col, row+col
if cols[col] || diag1[d1] || diag2[d2] {
continue
}
board[row][col] = 'Q'
cols[col], diag1[d1], diag2[d2] = true, true, true
dfs(res, board, cols, diag1, diag2, row+1, n)
cols[col], diag1[d1], diag2[d2] = false, false, false
board[row][col] = '.'
}
}
该实现中,cols[col] 标记列占用,diag1[row−col] 和 diag2[row+col] 分别标记两条对角线。每行尝试每一列,若位置安全则放置皇后并递归处理下一行,回溯时清除状态。
算法对比分析
| 问题 | 状态空间 | 剪枝策略 | 时间复杂度 |
|---|---|---|---|
| 全排列 | 排列树 | 无重复选择 | O(n!) |
| N皇后 | 子集树 | 列与对角线约束 | O(N^N)(最坏) |
两者均采用回溯框架,但约束条件不同导致搜索结构差异。N皇后因剪枝更高效,实际运行远快于理论上限。
第五章:高效刷题策略与代码模板总结
在高强度的算法训练中,单纯刷题数量并不能保证能力提升。真正高效的刷题策略需要系统性方法与可复用的代码模板相结合。以下是一些经过验证的实战路径。
刷题阶段划分与目标设定
将刷题过程划分为三个阶段:基础巩固、专项突破、模拟冲刺。
- 基础巩固阶段主攻数组、链表、栈、队列等数据结构的经典题目,每类完成15~20题;
- 专项突破聚焦动态规划、回溯、图论等难点,按子类(如背包问题、区间DP)逐个击破;
- 模拟冲刺则采用限时套题训练,模拟真实面试或竞赛环境。
建议使用如下表格追踪进度:
| 类别 | 目标题数 | 已完成 | 正确率 |
|---|---|---|---|
| 二分查找 | 20 | 18 | 83% |
| DFS/BFS | 25 | 20 | 76% |
| 动态规划 | 40 | 28 | 65% |
高频题型代码模板库建设
建立个人专属的代码模板库能显著提升编码速度与准确性。例如,二分查找的统一模板可封装为:
def binary_search(nums, target):
left, right = 0, len(nums) - 1
while left <= right:
mid = (left + right) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
对于回溯算法,通用结构如下:
def backtrack(path, options):
if base_condition:
result.append(path[:])
return
for opt in options:
path.append(opt)
backtrack(path, remaining_options)
path.pop()
错题归因分析流程
每次提交失败后,应执行标准化归因流程。通过mermaid绘制决策流图辅助定位:
graph TD
A[提交失败] --> B{是语法错误?}
B -- 是 --> C[检查缩进/括号]
B -- 否 --> D{是逻辑错误?}
D -- 是 --> E[打印中间变量调试]
D -- 否 --> F[检查边界条件]
E --> G[修正递归终止条件]
时间优化技巧实战
在LeetCode 146. LRU Cache中,结合哈希表与双向链表实现O(1)操作。关键点在于维护key -> node映射,并抽象出move_to_head和remove_node两个函数。实际编码时优先实现核心逻辑,再封装细节。
高频考点如滑动窗口,可套用如下结构:
left = 0
for right in range(n):
update_window(state, right)
while invalid(window):
update_window(state, left, remove=True)
left += 1
update_result()
