leetcode46 --- 全排列

题目:

给定一个没有重复 数字的序列,返回其所有可能的全排列。

示例:
在这里插入图片描述


题解:

以下文字内容全部来组leetcode题解区:
https://leetcode-cn.com/problems/permutations/solution/hui-su-suan-fa-python-dai-ma-java-dai-ma-by-liweiw/

「回溯」算法(深度优先遍历 + 状态重置 + 剪枝)

首先介绍“回溯”算法的应用。

  1. “回溯”算法也叫“回溯搜索”算法,主要用于在一个庞大的空间里搜索我们所需要的问题的解。我们每天使用的“搜索引擎”就是帮助我们在庞大的互联网上搜索我们需要的信息。“搜索”引擎的“搜索”和“回溯搜索”算法的“搜索”意思是一样的。

  2. “回溯”指的是“状态重置”,可以理解为“回到过去”、“恢复现场”,是在编码的过程中,是为了节约空间而使用的一种技巧。

  3. 而回溯其实是“深度优先遍历”特有的一种现象。之所以是“深度优先遍历”,是因为我们要解决的问题通常是在一棵树上完成的,在这棵树上搜索需要的答案,一般使用深度优先遍历。


“全排列”就是一个非常经典的“回溯”算法的应用。我们知道,N 个数字的全排列一共有 N!N! 这么多个。

以数组 [1, 2, 3] 的全排列为例。

(1)我们先写以 1 开头的全排列,它们是:[1, 2, 3], [1, 3, 2]
(2)再写以 2 开头的全排列,它们是:[2, 1, 3], [2, 3, 1]
(3)最后写以 3 开头的全排列,它们是:[3, 1, 2], [3, 2, 1]

我们只需要按顺序枚举每一位可能出现的情况,已经选择的数字在接下来要确定的数字中不能出现。按照这种策略选取就能够做到不重不漏,把可能的全排列都枚举出来。

(1)在枚举第一位的时候,有 3 种情况。
(2)在枚举第二位的时候,前面已经出现过的数字就不能再被选取了;
(3)在枚举第三位的时候,前面 2 个已经选择过的数字就不能再被选取了。
这样的思路,我们可以用一个树形结构表示。看到这里的朋友,建议自己先尝试画一下“全排列”问题的树形结构。
在这里插入图片描述

使用编程的方法得到全排列,就是在这样的一个树形结构中进行编程,具体来说,就是执行一次深度优先遍历,从树的根结点到叶子结点形成的路径就是一个全排列。


说明:

  1. 每一个结点表示了“全排列”问题求解的不同阶段,这些阶段通过变量的“不同的值”体现;
  2. 这些变量的不同的值,也称之为“状态”;
  3. 使用深度优先遍历有“回头”的过程,在“回头”以后,状态变量需要设置成为和先前一样;
  4. 因此在回到上一层结点的过程中,需要撤销上一次选择,这个操作也称之为“状态重置”;
  5. 深度优先遍历,可以直接借助系统栈空间,为我们保存所需要的状态变量,在编码中只需要注意遍历到相应的结点的时候,状态变量的值是正确的,具体的做法是:往下走一层的时候,path 变量在尾部追加,而往回走的时候,需要撤销上一次的选择,也是在尾部操作,因此 path 变量是一个栈。
  6. 深度优先遍历通过“回溯”操作,实现了全局使用一份状态变量的效果。

如何编码:

  1. 首先这棵树除了根结点和叶子结点以外,每一个结点做的事情其实是一样的,即在已经选了一些数的前提,我们需要在剩下还没有选择的数中按照顺序依次选择一个数,这显然是一个 递归结构

  2. 递归的终止条件是,数已经选够了,因此我们需要一个变量来表示当前递归到第几层,我们把这个变量叫做 depth;

  3. 这些结点实际上表示了搜索(查找)全排列问题的不同阶段,为了区分这些不同阶段,我们就需要一些变量来记录为了得到一个全排列,程序进行到哪一步了,在这里我们需要两个变量:
    (1)已经选了哪些数,到叶子结点时候,这些已经选择的数就构成了一个全排列;
    (2)一个布尔数组 used,初始化的时候都为 false 表示这些数还没有被选择,当我们选定一个数的时候,就将这个数组的相应位置设置为 true ,这样在考虑下一个位置的时候,就能够以 O(1)O(1) 的时间复杂度判断这个数是否被选择过,这是一种“以空间换时间”的思想。
    (3)我们把这两个变量称之为 “状态变量”,它们表示了我们在求解一个问题的时候所处的阶段。

  4. 在非叶子结点处,产生不同的分支,这一操作的语义是:在还未选择的数中依次选择一个元素作为下一个位置的元素,这显然得通过一个循环实现。

  5. 另外,因为是执行深度优先遍历,从较深层的结点返回到较浅层结点的时候,需要做 “状态重置”,即“回到过去”、“恢复现场”,我们举一个例子。

从 [1, 2, 3] 到 [1, 3, 2] ,深度优先遍历是这样做的,从 [1, 2, 3] 回到 [1, 2]
的时候,需要撤销刚刚已经选择的数 3,因为在这一层只有一个数 3 我们已经尝试过了,因此程序回到上一层,需要撤销对 2
的选择,好让后面的程序知道,选择 3 了以后还能够选择 2。

这种在遍历的过程中,从深层结点回到浅层结点的过程中所做的操作就叫 “回溯”


代码实现:

import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

class Solution {
    public List<List<Integer>> permute(int[] nums) {
        // 结果集
        List<List<Integer>> res = new ArrayList<> ();
        if(nums.length == 0){
            return res;
        }
        // 遍历路径
        Stack<Integer> path = new Stack<> ();
        // 是否使用过
        boolean[] used = new boolean[nums.length];

        dfs(nums,0,path,used,res);
        return res;

    }

    private void dfs(int[] nums, int depth, Stack<Integer> path, boolean[] used, List<List<Integer>> res) {
        if(depth == nums.length){
            res.add (new ArrayList(path));
            return;
        }
        for(int i = 0;i < nums.length;i++){
            if(used[i]) {
                continue;
            }
            path.add (nums[i]);
            used[i] = true;
            dfs (nums,depth+1,path,used,res);
            // 回溯
            path.pop ();
            used[i] = false;
        }
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值