LeetCode Hot100刷题——全排列

46.全排列

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

示例 1:

输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

示例 2:

输入:nums = [0,1]
输出:[[0,1],[1,0]]

示例 3:

输入:nums = [1]
输出:[[1]]

提示:

  • 1 <= nums.length <= 6
  • -10 <= nums[i] <= 10
  • nums 中的所有整数 互不相同

回溯算法

基本思想

        回溯算法的基本思想是在问题的解空间树中,按照深度优先搜索的策略,从根节点出发深度探索解空间树。当探索到某一节点时,先判断该节点是否包含问题的解,如果包含,就从该节点出发继续探索下去,如果该节点不包含问题的解,则逐层向其祖先节点回溯。若用回溯法求问题的所有解时,要回溯到根,且根节点的所有可行的子树都要已被搜索遍才结束。而若使用回溯法求任一个解时,只要搜索到问题的一个解就可以结束。

算法步骤

  1. 定义解空间:明确问题的解空间结构,它通常是一个树或图的形式。例如,在排列组合问题中,解空间可能是所有可能的排列或组合。
  2. 确定搜索策略:一般采用深度优先搜索(DFS),从根节点开始,递归地探索解空间。
  3. 约束条件判断:在搜索过程中,对于每个节点,检查它是否满足问题的约束条件。如果不满足,则剪去该节点及其子树,不再继续搜索。
  4. 回溯操作:当搜索到某个节点无法继续前进时,回溯到上一个节点,尝试其他可能的选择。

常见应用场景

1. 排列组合问题
  • 全排列问题:给定一个没有重复数字的序列,返回其所有可能的全排列。
  • 组合问题:给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
2. 棋盘问题
  • N 皇后问题:在 n×n 的棋盘上放置 n 个皇后,使得它们彼此之间不能相互攻击(即任意两个皇后都不能处于同一行、同一列或同一斜线上)。

复杂度分析

  • 时间复杂度:回溯算法的时间复杂度通常是指数级的,因为它需要遍历解空间中的大部分甚至所有可能的解。具体的时间复杂度取决于问题的规模和解空间的大小。
  • 空间复杂度:主要由递归栈的深度决定,通常为O(n) ,其中 n 是问题的规模。此外,还可能需要额外的空间来存储中间结果和最终结果。

解题思路

        全排列的话,比如示例中的[1,2,3],每个元素都要出现在每个位置上,而且不能重复使用元素。所以回溯的基本思路应该是,每次选择一个未被使用的元素,加入到当前路径中,直到路径长度等于原数组长度,这时候就得到一个排列,然后回溯回去,尝试其他可能性。

        比如,对于nums数组中的每个元素,如果未被使用,就把它加入当前路径,标记为已使用,然后递归地进行下一层选择。当路径长度等于数组长度时,将当前路径的拷贝加入结果列表。递归返回后,需要撤销刚才的选择,也就是从路径中移除最后一个元素,并标记该元素为未使用,这样才能继续尝试其他可能性。

代码实现

        需要一个结果列表来保存所有可能的排列,还需要一个临时列表来记录当前的路径。另外,为了标记哪些元素已经被使用过,可以用一个布尔数组来记录每个元素是否被使用过。

class Solution {
    public List> permute(int[] nums) {
        List> result = new ArrayList<>(); //结果列表
        //输出所有全排列
        backtrack(nums, new ArrayList<>(), new boolean[nums.length], result);
        return result;
    }

    private void backtrack(int[] nums, List current, boolean[] used, List> result){
        //终止条件,当当前排列的长度等于数组的长度时,说明已经生成了一个完整的排列
        if(current.size() == nums.length){
            result.add(new ArrayList<>(current)); //注意要创建副本(拷贝),因为后续回溯过程会修改current列表
            return;  //结束当前递归调用,回溯到上一层
        }

        for(int i = 0; i < nums.length; i++){
            if(!used[i]){
                used[i] = true;
                current.add(nums[i]);
                // 递归调用回溯函数,继续生成排列
                backtrack(nums,current,used,result);
                // 回溯操作:撤销之前的选择
                // 从当前排列中移除最后添加的元素
                current.remove(current.size() - 1);
                // 标记当前元素为未使用,以便后续可以再次使用
                used[i] = false;
            }
        }
    }
}

        需要注意的是,Java中的List是引用类型,所以在将path添加到结果列表时,必须创建一个新的ArrayList,否则后续对path的修改会影响结果中已保存的列表。也就是说,当路径完成时,应该res.add(new ArrayList<>(path)),而不是直接添加path本身。

backtracka函数解释​​​​​​​

  • nums:这是原始的整数数组,我们要基于这个数组生成所有可能的排列。
  • current:它是一个 List,用于存储当前正在生成的排列。在递归过程中,会不断地向这个列表中添加元素,以逐步构建出一个完整的排列。
  • used:这是一个布尔类型的数组,长度和 nums 数组相同。used[i] 表示 nums[i] 这个元素是否已经在当前排列 current 中被使用过。如果 used[i] 为 true,则表示 nums[i] 已被使用;如果为 false,则表示未被使用。
  • result:这是一个二维列表,用于存储最终生成的所有排列。当一个完整的排列生成后,会将其添加到 result 中。

你可能感兴趣的:(leetcode,算法,数据结构)