• 0

  • 0

LeetCode算法系列Java版 46. 全排列

2星期前

白菜Java自习室 涵盖核心知识

力扣原题

46. 全排列

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

示例:

输入: [1,2,3]
输出:
[
  [1,2,3],
  [1,3,2],
  [2,1,3],
  [2,3,1],
  [3,1,2],
  [3,2,1]
]
复制代码

回溯算法

回溯法 采用试错的思想,它尝试分步的去解决一个问题。在分步解决问题的过程中,当它通过尝试发现现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其它的可能的分步解答再次尝试寻找问题的答案。

回溯法通常用最简单的递归方法来实现,在反复重复上述的步骤后可能出现两种情况:

  • 找到一个可能存在的正确的答案;
  • 在尝试了所有可能的分步方法后宣告该问题没有答案。

解题思路

我们尝试在纸上写 3 个数字、4 个数字、5 个数字的全排列,相信不难找到这样的方法。以数组 [1, 2, 3] 的全排列为例。

  • 先写以 1 开头的全排列,它们是:[1, 2, 3], [1, 3, 2],即 1 + [2, 3] 的全排列(注意:递归结构体现在这里。);
  • 再写以 2 开头的全排列,它们是:[2, 1, 3], [2, 3, 1],即 2 + [1, 3] 的全排列;
  • 最后写以 3 开头的全排列,它们是:[3, 1, 2], [3, 2, 1],即 3 + [1, 2] 的全排列。

总结搜索的方法:按顺序枚举每一位可能出现的情况,已经选择的数字在 当前 要选择的数字中不能出现。按照这种策略搜索就能够做到 不重不漏。这样的思路,可以用一个树形结构表示。递归的终止条件是: 一个排列中的数字已经选够

看到这里的朋友,建议先尝试自己画出「全排列」问题的树形结构。

说明:

  • 每一个结点表示了求解全排列问题的不同的阶段,这些阶段通过变量的「不同的值」体现;
  • 使用深度优先遍历有「回溯」的过程,因此在回到上一层结点的过程中,需要撤销上一次的选择;
  • 深度优先遍历,借助系统栈空间,保存所需要的变量,在编码中只需要注意遍历到相应的结点的时候,保存变量的值,具体的做法是:往下走一层的时候,path 变量在尾部追加,而往回走的时候,需要撤销上一次的选择,也是在尾部操作,因此 path 变量是一个栈;
  • 深度优先遍历通过「回溯」操作,实现了全局使用一份 path 变量的效果。

使用编程的方法得到全排列,就是在这样的一个树形结构中完成 遍历,从树的根结点到叶子结点形成的路径就是其中一个全排列。

代码实现

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

class Solution {

    List<List<Integer>> result = new ArrayList<>();

    public List<List<Integer>> permute(int[] nums) {
        // 阶段变量 path 是一个栈
        Stack<Integer> path = new Stack<>();
        this.backtrack(nums, path);
        return result;
    }

    private void backtrack(int[] nums, Stack<Integer> path) {
        if (nums.length <= 0) {
            // 递归的终止条件是: 一个排列中的数字已经选够
            result.add(new ArrayList<>(path));
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            // 下一阶段剩下得数组: [1, 2, 3] -> 2 + [1, 3]
            int[] temp = new int[nums.length - 1];
            System.arraycopy(nums, 0, temp, 0, i);
            System.arraycopy(nums, i + 1, temp, i, nums.length - i - 1);
            // 递进
            path.push(nums[i]);
            backtrack(temp, path);
            // 回溯
            path.pop();
        }
    }

    public static void main(String[] args) {
        int[] nums = new int[]{1, 2, 3};
        Solution solution = new Solution();
        List<List<Integer>> result = solution.permute(nums);
        System.out.println(result);
    }

}
复制代码

输出结果:

[[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]
复制代码

复杂度分析

时间复杂度: O ( N × N ! ) O(N \times N!)

非叶子结点的个数,依次为(按照层数来):

1 + A N 1 + A N 2 + + A N N 1 = 1 + N ! ( N 1 ) ! + N ! ( N 2 ) ! + + N ! 1 + A_N^1 + A_N^2 + \cdots + A_N^{N-1} = 1 + \cfrac{N!}{(N - 1)!} + \cfrac{N!}{(N - 2)!} + \cdots + N!

说明:根结点为 1,计算复杂度的时候忽略; A N 1 A_N^1 表示排列数,计算公式为 A n m = n ! ( n m ) ! A_n^m = \cfrac{n!}{(n - m)!}

在第 1 层,结点个数为 N 个数选 1 个的排列,故为 A N 1 A_N^1

在第 2 层,结点个数为 N 个数选 2 个的排列,故为 A N 2 A_N^2

N ! ( N 1 ) ! + N ! ( N 2 ) ! + + N ! = N ! ( 1 ( N 1 ) ! + 1 ( N 2 ) ! + + 1 ) N ! ( 1 + 1 2 + 1 4 + + 1 2 N 1 ) < 2 N ! \cfrac{N!}{(N - 1)!} + \cfrac{N!}{(N - 2)!} + \cdots + N! = N! \left( \cfrac{1}{(N - 1)!} + \cfrac{1}{(N - 2)!} + \cdots + 1 \right) \le N! \left( 1 + \cfrac{1}{2} + \cfrac{1}{4} + \cdots + \cfrac{1}{2^{N - 1}} \right) < 2N!

将常系数 2 视为 1,每个内部结点循环 N 次,故非叶子结点的时间复杂度为 O ( N × N ! ) O(N \times N!)

最后一层共 N ! N! 个叶节点,在叶子结点处拷贝需要 O ( N ) O(N) ,叶子结点的时间复杂度也为 O ( N × N ! ) O(N \times N!)

空间复杂度: O ( N × N ! ) O(N \times N!)

  • 递归树深度 log N \log N
  • 全排列个数 N ! N! ,每个全排列占空间 N。取较大者。
免责声明:文章版权归原作者所有,其内容与观点不代表Unitimes立场,亦不构成任何投资意见或建议。

java

0

相关文章推荐

未登录头像

暂无评论