Leetcode【回溯框架 递归】| 46. 全排列

这篇博客详细介绍了如何使用回溯算法解决LeetCode中的46. 全排列问题。通过动态规划递归和优化(结合回溯)的方式,阐述了如何找到给定序列的所有可能排列。博客内容包括决策树的回溯方法,强调了在递归过程中做选择和撤销选择的重要性,并给出了具体的解题框架。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目

给定一个 没有重复 数字的序列,返回其所有可能的全排列。
示例:
 输入: [1,2,3]
 输出:
 [
 [1,2,3],
 [1,3,2],
 [2,1,3],
 [2,3,1],
 [3,1,2],
 [3,2,1]
 ]
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/permutations

解题

动态规划递归

运用动态规划的思想,数组中n个元素的全排列,可以由n-1个元素的全排列递推而来。每次从数组中挑选出来一个元素放在第一个,加上剩下n-1个元素的全排列 (递归)
主题思想就是如何将原问题拆分为小规模的等价问题。

 dp[n] = ∑ i = 1 n \sum_{i=1}^n i=1nnums[i] & dp[n-1] (除nums[i]的n-1个元素的全排列)
 eg:dp[1,2,3] = [1,dp[2,3]] + [2,dp[1,3]] + [3,dp[1,2]]

代码参考这里

class Solution {
    public List<List<Integer>> permute(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        //判断特殊情况
        if(nums==null||nums.length==0){
            return res;
        }
        //遍历n个数,分别选择第i个数放在第一个,剩下的递归
        for(int i=0;i<nums.length;i++){
        	//去掉nums[i](这个元素被用来排列在第一位)的剩下n-1个元素用数组存储起来,以便之后递归求dp[n-1]
            int[] newnums = new int[nums.length-1];
            int m = 0;
            int n = 0;
            while(n<newnums.length){
                if(m!=i){
                    newnums[n] = nums[m];
                    n++;
                    m++;
                }else{
                    m++;
                }
            }
            //递归 求dp[n-1]用tmp存储起来,此时tmp里存储的就是已经递归求解出的剩余n-1个元素所有的全排列
            List<List<Integer>> tmp = permute(newnums);
            //temp为空时说明,此时只有一个元素,就是选出排列在第一位的nums[i],将其加入结果就行了。
            if(tmp.size()==0){
                List<Integer> list = new LinkedList<>();
                list.add(nums[i]);
                res.add(list);
            }else{
            	//将dp[n-1]即tmp中所有可能的全排列依次求出来,将nums[i]插到第一位,就是dp[n]中将nums[i]放在第一个的所有可能的全排列
                for(List<Integer> list:tmp){
                    LinkedList<Integer> list2 = new LinkedList<>(list);
                    list2.addFirst(nums[i]);
                    res.add(list2);
                }
            }
        }
        return res;
    }
}

在这里插入图片描述

动态规划优化(结合回溯)

交换+回溯

参考这里
每次选择nums[i]不用额外挑选出来,通过①nums[i]与第一位交换位置②递归③撤销交换(第一步)这三步实现nums[i]&dp[n-1]的原地操作。其实是结合了回溯的思想。

  1. 第一种方法即下面剪枝+回溯中介绍的方法是决策的时候通过判断选择的这个元素是否已经被选择过而决定这次是否选择这个元素,哪些为剩下的元素(用trace.contains()来判断)。
  2. 这个方法是每进行一次选择如第i次选择,将选择的元素放在原数组num的第i个位置上(交换),这样数组中i后面的元素都是还未选择过的剩下的元素(通过交换每次将剩下的元素都放在一起了),然后对后面的元素进行下一层选择/递归即可。比如nums = {1,2,3,4,5},当我们选择第一个元素为4的时候,我们将4放在第一个位置上,即与第一个位置上的元素进行交换nums变为{4,2,3,1,5},此时已选择序列为{4},待选择序列未{2,3,1,5},然后从第二个位置开始即待选择序列中递归进行第二个元素的选择。
class Solution {
    public List<List<Integer>> res;
    public List<Integer> trace;
    public List<List<Integer>> permute(int[] nums) {
        res = new ArrayList<>();
        backtrack(nums,0);
        return res;
    }
    public void backtrack(int[] nums, int begin){//begin为当前选择的第i个元素
        if(begin == nums.length){
            //就是最后一个元素第nums.length-1个元素已被选择完
            trace = new ArrayList<>();
            //所有选择完毕,在num中的位置也都交换完毕,可以直接全部添加
            for(int num:nums){
                trace.add(num);
            }
            res.add(trace);
            return;
        }else{
            //进行第i次选择,选择的范围为剩余的元素begin到nums.length
            for(int i = begin; i < nums.length; i++){
                //选择i,即将i放在begin的位置上,剩余的元素就是begin+1到nums.length
                //trace.add(nums[i]);//如果在选择的时候添加删减元素,会比交换到最后 一起添加元素 慢
                swap(nums,begin,i);
                //从剩余元素中继续下一次选择
                backtrack(nums,begin+1);
                //撤销选择,交换的位置再换回来,加入的元素也删除
                swap(nums,begin,i);
                //trace.remove(trace.size()-1);//在选择中添加元素,就不用最后一起添加了
            }
        }
    }
    public void swap(int[] nums, int i, int j){
        int tmp = nums[i];
        nums[i] = nums[j];
        nums[j] = tmp;
    }
}

在这里插入图片描述

回溯 决策树

剪枝+回溯

参考这里

思想回溯思想,实质可以理解为树的深度优先搜索实现递归实现。
解决一个回溯问题,其实就是一个决策树的遍历过程,主要解决三个部分:

  1. 路径: 已做出的选择(在这道题中就是对nums[]进行排列从0到n-1依次选择的元素顺序)
  2. 选择列表: 当前可以做的全部选择(这道题中就是排列选择到第i个元素时还剩下哪些数可以选)
  3. 结束条件: 到达决策树底层,无法再做选择的条件。(这道题中就是选择到了最后一个元素此次排列完毕)

解法框架:

LinkeList<> res = new LinkedList<>();
public void recursive(路径,选择列表){
  if(满足结束条件){
    res.add(结果);
  }
  for(选择:选择列表){
    做出选择;
    recursive(路径,选择列表);//递归剩下的
    撤销选择;
  }
}

重点:在递归之前做选择,在递归之后撤销选择。在上一种解法中交换与撤销交换也是同理。

举例:以[1,2,3]为例,枚举出它的全排列的过程如下:(树的结点表示当前要做决策/选择,路径表示做出的选择,遍历的树所有的路径就是所求全排列结果)
全排列示例图

class Solution {
    List<List<Integer>> res = new LinkedList<>();
    public List<List<Integer>> permute(int[] nums) {
        if(nums.length == 0) return res;
        recursive(nums,new LinkedList<Integer>());
        return res;
    }
    public void recursive(int[] nums, LinkedList<Integer> trace){
        //结束的条件,trace的深度等于所有元素的数量,即已遍历完所有元素
        if(trace.size() == nums.length){
            res.add(new LinkedList(trace));
            return;
        }
        //在选择列表中选出trace
        for(int i = 0; i < nums.length; i++){
            //剪枝,就是当前元素已经在组合中
            if(trace.contains(nums[i])) continue;
            //做出当前选择,然后递归 就做出了第一种路径的选择
            trace.add(nums[i]);
            recursive(nums,trace);
            //然后撤销当前选择,进行下一条路径的选择
            trace.removeLast();
        }  
    }
}

在这里插入图片描述

<think>好的,用户想了解回溯算法与递归的关系以及具体的实现示例。我需要先理清回溯递归的基本概念,然后结合它们之间的联系进行说明。回溯算法通常用于解决需要遍历所有可能解的问题,比如排列组合、子集问题等,而递归则是实现回溯的一种自然方式,因为递归允许函数自我调用,处理更小规模的子问题。 首先,回溯算法的核心思想是试探和回溯。当选择一条路径进行探索时,如果发现当前路径不能达到目标,就回退到上一步,选择其他可能的路径继续尝试。这种“试错”机制非常适合用递归来实现,因为递归的栈结构可以保存每一步的状态,方便回溯时恢复。 接下来,我需要找一个典型的例子来展示回溯算法的递归实现。比如全排列问题,这是一个经典的回溯应用场景。以数组[1,2,3]的全排列为例,递归函数需要生成所有可能的排列组合。在每一步递归中,选择一个元素加入当前路径,并标记为已使用,然后递归处理剩余元素。当路径长度等于原数组长度时,说明找到了一个有效排列,将其加入结果列表。完成递归调用后,需要撤销之前的选择(回溯),以便尝试其他可能性。 然后,结合用户提供的引用内容,特别是引用[1]中提到递归回溯与栈的关系,可以解释递归调用栈如何模拟回溯的过程。同时,引用[2]中的回溯算法步骤需要应用到示例中,如判断终止条件、遍历所有可能的选择等。此外,引用[3]提到的回溯算法与动态规划的区别,可以简要提及,帮助用户理解回溯的特点,即寻找所有解而非最优解。 在编写代码示例时,需要注意递归函数的参数设计,比如当前路径、使用标记数组等。同时,要确保每一步选择后正确标记已使用的元素,并在回溯时取消标记,避免影响后续的选择。代码注释要清晰,说明每个步骤的作用,帮助用户理解执行流程。 最后,生成相关问题时要围绕回溯递归的关系、优化方法、应用场景等,引导用户进一步深入学习。例如,如何分析回溯算法的时间复杂度,或者如何优化剪枝以减少不必要的递归调用。 整个过程中,要确保使用正确的中文表达,数学公式和代码块符合给定的格式要求,并正确添加引用标记。同时,保持回答结构清晰,逐步引导用户理解回溯算法的实现及其与递归的关系。</think>### 回溯算法与递归的关系及实现示例 回溯算法是一种通过**试探和回退**来遍历所有可能解的算法,其核心思想与递归紧密相关。递归提供了回溯算法中“状态保存与恢复”的天然实现方式,而回溯则是递归在搜索问题中的具体应用策略[^1]。 --- #### 1. 回溯算法的递归实现框架 回溯算法通常包含以下步骤(参考引用[2]): 1. **判断合法性**:若当前路径不满足约束条件,立即返回。 2. **终止条件**:若当前路径满足解的要求,保存结果并返回。 3. **遍历选择**:对所有可能的分支进行递归尝试。 4. **回溯恢复**:撤销当前选择,回到上一步状态。 ```python def backtrack(路径, 选择列表): if 满足终止条件: 保存结果 return for 选择 in 选择列表: if 选择不合法: # 剪枝优化 continue 做选择(将选择加入路径) backtrack(新路径, 新选择列表) # 递归调用 撤销选择(从路径中移除选择) # 关键回溯步骤 ``` --- #### 2. 示例:全排列问题(LeetCode 46) **问题描述**:给定数组 `[1,2,3]`,生成所有不重复的排列。 **递归回溯实现**: ```python def permute(nums): def backtrack(path, used): if len(path) == len(nums): # 终止条件:路径长度等于数组长度 result.append(path.copy()) return for i in range(len(nums)): if not used[i]: # 选择未使用的元素 used[i] = True path.append(nums[i]) backtrack(path, used) # 递归调用 path.pop() # 回溯:撤销选择 used[i] = False # 回溯:恢复状态 result = [] backtrack([], [False]*len(nums)) return result print(permute([1,2,3])) # 输出:[[1,2,3],[1,3,2],[2,1,3],...] ``` **关键步骤解析**: - `used` 数组标记元素是否被使用,避免重复选择。 - 递归调用前记录选择(`path.append`),递归返回后撤销选择(`path.pop()`),实现回溯。 - 时间复杂度为 $O(n \cdot n!)$,空间复杂度为 $O(n)$[^3]。 --- #### 3. 回溯递归的关系 1. **递归回溯的载体**:回溯通过递归的栈结构保存中间状态,递归调用栈天然支持“前进-回退”操作。 2. **剪枝优化**:在递归中通过条件判断提前终止无效分支(如 `if not used[i]`),减少计算量。 3. **深度优先搜索(DFS)**:回溯本质是DFS在解空间树上的应用,递归实现了隐式的栈操作。 --- #### 4. 回溯算法的典型应用 - 组合问题(如子集、组合总和) - 排列问题(如全排列、N皇后) - 分割问题(如回文分割) - 棋盘问题(如数独、八皇后) ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值