GitHub_Trending/le/LeetCode-Book:递归与回溯算法实战指南
你还在为递归超时抓狂?3大剪枝策略+4类经典问题通解
读完你将获得
- 递归与回溯算法的通用解题框架(附流程图)
- 排列/组合/子集/搜索四大类问题的模板代码
- 3种高效剪枝策略(去重/可行性/最优性)的实战应用
- Python/Java/C++多语言实现对比分析
- 从LeetCode中等题到Hard题的进阶路径
一、算法框架:从暴力递归到智能回溯
1.1 递归算法的数学本质
递归(Recursion)是一种直接或间接调用自身函数的算法思想,其数学基础是数学归纳法:
- 基础情况(Base Case):无需递归即可解决的最小子问题
- 归纳步骤(Inductive Step):将原问题分解为规模更小的子问题
斐波那契数列的递归实现(存在严重重复计算):
def fib(n):
if n <= 1: return n
return fib(n-1) + fib(n-2) # 重复计算fib(n-2)达O(2ⁿ)次
1.2 回溯算法的通用框架
回溯(Backtracking)是递归的特殊形式,通过深度优先搜索(DFS) 遍历解空间,并在搜索过程中通过剪枝(Pruning) 剔除无效路径。
回溯算法模板代码:
def backtrack(state, choices, result):
if 终止条件:
result.add(state.copy())
return
for choice in choices:
if 剪枝条件:
continue/break
# 做出选择
state.add(choice)
# 递归探索
backtrack(state, choices, result)
# 撤销选择(回溯)
state.remove(choice)
二、排列问题:从全排列到去重剪枝
2.1 无重复元素的全排列
问题特征:给定不含重复数字的数组,返回所有可能的全排列。
核心思想:通过元素交换固定当前位置,递归处理剩余位置。
Python实现:
def permute(nums):
def dfs(x):
if x == len(nums)-1:
res.append(nums.copy())
return
for i in range(x, len(nums)):
nums[i], nums[x] = nums[x], nums[i] # 交换固定
dfs(x+1) # 递归下一位
nums[i], nums[x] = nums[x], nums[i] # 回溯还原
res = []
dfs(0)
return res
2.2 含重复元素的排列去重
问题特征:给定可能包含重复数字的数组,返回所有不重复的全排列。
关键剪枝:使用集合记录已使用元素,避免同一位置重复选择相同元素。
Java实现:
class Solution {
List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> permuteUnique(int[] nums) {
Arrays.sort(nums); // 排序便于去重
backtrack(nums, new boolean[nums.length], new ArrayList<>());
return res;
}
void backtrack(int[] nums, boolean[] used, List<Integer> path) {
if (path.size() == nums.length) {
res.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
// 剪枝1:已使用的元素跳过
if (used[i]) continue;
// 剪枝2:重复元素只允许第一个未使用的被选择
if (i > 0 && nums[i] == nums[i-1] && !used[i-1]) continue;
used[i] = true;
path.add(nums[i]);
backtrack(nums, used, path);
path.remove(path.size()-1);
used[i] = false;
}
}
}
复杂度对比: | 方法 | 时间复杂度 | 空间复杂度 | 去重效果 | |------|------------|------------|----------| | 暴力递归 | O(n!) | O(n) | 无去重 | | 集合去重 | O(n!n) | O(n) | 有去重,效率低 | | 排序剪枝 | O(n!logn) | O(n) | 最优去重 |
三、组合问题:从元素求和到剪枝优化
3.1 组合总和(无限选取)
问题特征:给定无重复元素数组和目标数,找出所有可以使数字和为目标的组合,元素可重复使用。
关键策略:排序后通过起始索引控制选择顺序,避免重复组合。
C++实现:
class Solution {
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
vector<vector<int>> res;
vector<int> path;
sort(candidates.begin(), candidates.end()); // 排序便于剪枝
backtrack(candidates, target, 0, path, res);
return res;
}
void backtrack(vector<int>& candidates, int target, int start,
vector<int>& path, vector<vector<int>>& res) {
if (target == 0) {
res.push_back(path);
return;
}
for (int i = start; i < candidates.size(); i++) {
// 可行性剪枝:当前元素大于剩余target,直接break(因数组已排序)
if (candidates[i] > target) break;
path.push_back(candidates[i]);
// 允许重复选取,下一轮start仍为i
backtrack(candidates, target - candidates[i], i, path, res);
path.pop_back();
}
}
};
3.2 组合总和去重(有限选取)
关键剪枝:在无限选取基础上,对重复元素增加"相同元素只允许第一个未使用的被选择"的限制。
def combinationSum2(candidates, target):
def backtrack(start, target, path):
if target == 0:
res.append(path.copy())
return
for i in range(start, len(candidates)):
# 剪枝1:超过目标值
if candidates[i] > target:
break
# 剪枝2:跳过重复元素(同一层相同元素只选第一个)
if i > start and candidates[i] == candidates[i-1]:
continue
path.append(candidates[i])
# 有限选取,下一轮start为i+1
backtrack(i+1, target - candidates[i], path)
path.pop()
res = []
candidates.sort()
backtrack(0, target, [])
return res
四、二维网格搜索:DFS+回溯的经典应用
4.1 单词搜索问题
问题特征:给定m x n网格和单词,判断单词是否存在于网格中。
核心思想:从每个单元格出发,上下左右四个方向DFS探索,走过的路径做标记避免重复。
多语言实现对比:
| 语言 | 核心实现 | 时间复杂度 | 空间复杂度 |
|---|---|---|---|
| Python | 列表推导式+递归 | O(MN·3^L) | O(L) |
| Java | 字符数组+回溯 | O(MN·3^L) | O(L) |
| C++ | 引用传参+回溯 | O(MN·3^L) | O(L) |
Python实现:
class Solution:
def exist(self, board: List[List[str]], word: str) -> bool:
def dfs(i, j, k):
# 终止条件:越界或不匹配
if not 0<=i<len(board) or not 0<=j<len(board[0]) or board[i][j]!=word[k]:
return False
# 终止条件:匹配完成
if k == len(word)-1:
return True
# 标记当前位置为已访问
board[i][j] = ''
# 四方向探索
res = dfs(i+1,j,k+1) or dfs(i-1,j,k+1) or dfs(i,j+1,k+1) or dfs(i,j-1,k+1)
# 回溯:恢复当前位置
board[i][j] = word[k]
return res
# 遍历所有起点
for i in range(len(board)):
for j in range(len(board[0])):
if dfs(i,j,0):
return True
return False
五、算法优化:三大剪枝策略深度解析
5.1 可行性剪枝(Feasibility Pruning)
适用场景:当当前路径无法达到目标状态时,立即停止探索。
典型实现:
# 组合总和中的可行性剪枝
if candidates[i] > target:
break # 因数组已排序,后续元素更大,直接终止循环
5.2 去重剪枝(Duplication Pruning)
适用场景:存在重复元素时,避免生成重复解。
两种实现方式对比:
# 方法1:排序后同一层相同元素只选第一个
if i > start and candidates[i] == candidates[i-1]:
continue
# 方法2:使用集合记录当前层已使用元素
used = set()
for i in range(start, len(candidates)):
if candidates[i] in used:
continue
used.add(candidates[i])
5.3 最优性剪枝(Optimality Pruning)
适用场景:求最优解问题中,当前路径已不可能优于已知最优解时停止探索。
案例:N皇后问题中记录最小冲突数,超过则剪枝。
六、实战进阶:从模板到复杂问题
6.1 回溯算法通用模板总结
def backtrack(参数列表):
if 终止条件:
记录结果
return
for 选择 in 选择列表:
# 剪枝操作
if 剪枝条件:
continue/break
# 做出选择
标记选择
# 递归探索
backtrack(新参数)
# 撤销选择(回溯)
撤销标记
6.2 复杂度分析框架
时间复杂度 = 状态数 × 每个状态的操作数
- 排列问题:O(n!) × O(n)
- 组合问题:O(2ⁿ) × O(n)
- 单词搜索:O(MN·3^L) × O(1) (L为单词长度)
空间复杂度 = 递归深度 + 辅助空间
- 递归深度通常为问题规模n
- 辅助空间主要为存储结果的列表
6.3 LeetCode进阶路径
入门级(Easy):
-
- 爬楼梯(简单递归)
-
- 二叉树的最大深度(DFS)
进阶级(Medium):
-
- 全排列
-
- 组合总和
-
- 单词搜索
挑战级(Hard):
-
- N皇后
-
- 解数独
-
- 连接词(字典树+回溯)
七、总结与资源推荐
7.1 回溯算法关键点
- 状态表示:如何记录当前探索的路径信息
- 选择列表:可用的下一步选择有哪些
- 剪枝条件:哪些路径可以提前终止
- 回溯操作:如何恢复到上一步状态
7.2 扩展学习资源
- 项目仓库:https://gitcode.com/GitHub_Trending/le/LeetCode-Book
- 推荐章节:《剑指 Offer》第38题(字符串排列)、第12题(矩阵路径)
- 在线练习:LeetCode回溯算法标签页(https://leetcode.cn/tag/backtracking/)
7.3 下期预告
「动态规划解题框架:从记忆化搜索到状态转移方程」—— 用四步法解决所有DP问题
如果本文对你有帮助,请点赞+收藏+关注支持,你的鼓励是我持续创作的动力!
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



