GitHub_Trending/le/LeetCode-Book:递归与回溯算法实战指南

GitHub_Trending/le/LeetCode-Book:递归与回溯算法实战指南

【免费下载链接】LeetCode-Book 《剑指 Offer》 Python, Java, C++ 解题代码,LeetBook《图解算法数据结构》配套代码仓 【免费下载链接】LeetCode-Book 项目地址: https://gitcode.com/GitHub_Trending/le/LeetCode-Book

你还在为递归超时抓狂?3大剪枝策略+4类经典问题通解

读完你将获得

  • 递归与回溯算法的通用解题框架(附流程图)
  • 排列/组合/子集/搜索四大类问题的模板代码
  • 3种高效剪枝策略(去重/可行性/最优性)的实战应用
  • Python/Java/C++多语言实现对比分析
  • 从LeetCode中等题到Hard题的进阶路径

一、算法框架:从暴力递归到智能回溯

1.1 递归算法的数学本质

递归(Recursion)是一种直接或间接调用自身函数的算法思想,其数学基础是数学归纳法

  • 基础情况(Base Case):无需递归即可解决的最小子问题
  • 归纳步骤(Inductive Step):将原问题分解为规模更小的子问题

mermaid

斐波那契数列的递归实现(存在严重重复计算):

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) 剔除无效路径。

mermaid

回溯算法模板代码

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 无重复元素的全排列

问题特征:给定不含重复数字的数组,返回所有可能的全排列。

核心思想:通过元素交换固定当前位置,递归处理剩余位置。

mermaid

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 组合总和(无限选取)

问题特征:给定无重复元素数组和目标数,找出所有可以使数字和为目标的组合,元素可重复使用。

关键策略:排序后通过起始索引控制选择顺序,避免重复组合。

mermaid

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探索,走过的路径做标记避免重复。

mermaid

多语言实现对比

语言核心实现时间复杂度空间复杂度
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)

    1. 爬楼梯(简单递归)
    1. 二叉树的最大深度(DFS)

进阶级(Medium)

    1. 全排列
    1. 组合总和
    1. 单词搜索

挑战级(Hard)

    1. N皇后
    1. 解数独
    1. 连接词(字典树+回溯)

七、总结与资源推荐

7.1 回溯算法关键点

  1. 状态表示:如何记录当前探索的路径信息
  2. 选择列表:可用的下一步选择有哪些
  3. 剪枝条件:哪些路径可以提前终止
  4. 回溯操作:如何恢复到上一步状态

7.2 扩展学习资源

  • 项目仓库:https://gitcode.com/GitHub_Trending/le/LeetCode-Book
  • 推荐章节:《剑指 Offer》第38题(字符串排列)、第12题(矩阵路径)
  • 在线练习:LeetCode回溯算法标签页(https://leetcode.cn/tag/backtracking/)

7.3 下期预告

「动态规划解题框架:从记忆化搜索到状态转移方程」—— 用四步法解决所有DP问题

如果本文对你有帮助,请点赞+收藏+关注支持,你的鼓励是我持续创作的动力!

【免费下载链接】LeetCode-Book 《剑指 Offer》 Python, Java, C++ 解题代码,LeetBook《图解算法数据结构》配套代码仓 【免费下载链接】LeetCode-Book 项目地址: https://gitcode.com/GitHub_Trending/le/LeetCode-Book

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值