全排列专项

140 阅读1分钟

第k个排列

题目

image.png

版本1 正确

        public String getPermutation(int n, int k) {
            int[] factorial = new int[n];
            factorial[0] = 1;
            // 存一下1, 2, 3...n, 每个阶乘的数目
            for (int i = 1; i < n; ++i) {
                factorial[i] = factorial[i - 1] * i;
            }

            --k;
            StringBuffer ans = new StringBuffer();
            int[] valid = new int[n + 1];
            // valid数组用来存储n个数字, 哪些是未使用过的
            Arrays.fill(valid, 1);
            for (int i = 1; i <= n; ++i) {
                // 计算得到的是次序, 也就是第几小的数字, 这里是关键, 计算得到的是次序, 而不是数字
                int order = k / factorial[n - i] + 1;
                for (int j = 1; j <= n; ++j) {
                    // 遇见valid[j] = 0, 即该数字被使用过, 就会被跳过
                    // 遇见valid[j] = 1, 就表示该数字可以选择, 但是目标是选择第order小
                    order -= valid[j];
                    if (order == 0) {
                        ans.append(j);
                        valid[j] = 0;
                        break;
                    }
                }
                k %= factorial[n - i];
            }
            return ans.toString();
        }

正确的原因

(1) 通过除法, 可以依次确定每一位的数字, 例如 123, 132, 213, 231, 312, 321. 此时n=3, 目标k = 3, 那么(n - 1) !一共有2种, 此时k / 2 + 1 = 2, 就表面这一位数字是1, 2, 3 三个数字种第2小的数字

(2) 当2被选择后, 就确定是在1, 3种第几小的数字

全排列

题目

image.png

版本1 正确

    List<List<Integer>> ans = new ArrayList<>();
    boolean [] visited;
    public List<List<Integer>> permute(int[] nums) {

        // 没有重复数字的全排列
        // 利用回溯
        visited = new boolean[nums.length];
        backtrack(new ArrayList<>(), nums);
        return ans;
    }

    public void backtrack(List<Integer> temp, int [] nums) {
        if (temp.size() == nums.length) {
            ans.add(new ArrayList<>(temp));
            return;
        }
        
        for (int i = 0; i < nums.length; i ++) {
            if (!visited[i]) {
                // 做出选择
                temp.add(nums[i]);
                visited[i] = Boolean.TRUE;
                backtrack(temp, nums);
                
                // 撤销选择
                temp.remove(temp.size() - 1);
                visited[i] = Boolean.FALSE;
            }
        }
        
    }

正确的原因

  1. 因为元素不重复, 简单的对元素进行即可, 注意不要选择重复元素

全排列II

题目

image.png

版本1 正确

    List<List<Integer>> ans = new ArrayList<>();
    public List<List<Integer>> permuteUnique(int[] nums) {
        if (nums.length == 0) {
            return ans;
        }

        // 返回数组元素的全排列
        // 利用回溯来枚举

        // 一定不要忘了排序, 才能够去重
        Arrays.sort(nums);
        boolean [] used = new boolean[nums.length];
        backtrack(nums, used, new ArrayList<>());

        return ans;


    }

    public void backtrack(int [] nums, boolean [] used, List<Integer> temp) {
        if (temp.size() == nums.length) {
            ans.add(new ArrayList<>(temp));
            return;
        }

        // 每次的选择都是全体的nums
        for (int i = 0; i < nums.length; i ++) {
            // 排除重复选择
            if (used[i] || i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
                // nums[i] == nums[i - 1] && !used[i - 1]条件的目的是为了不生成重复的排列
                // 当nums排序完后, 假设现在是[1, 2, 2, 2, 3]
                // 假设当前i = 1, 那么对应的元素就是2, 选择2, 然后得到了假设1, 2, 2, 2, 3这个排列。    此时递归处于第二层, 正在填充第二个数字
                // 然后回退到当前函数, 此时i = 2, 此时也是在填充第二个数字, 但是我们之前已经选过2了, 因此不能再在第二个数字这里选2了
                // 因此这里i = 2就命中了nums[i] == nums[i - 1] && !used[i - 1], 注意此时的used[1] = Boolean.False的。
                // 同理i = 3也不会被选到.
                // 递归函数嵌套的层数, 代表了在填充第几个数字, 因此这个条件保证了, 在同一个for循环中, 对于重复的元素, 只会填充第一个, 后续的重复元素,
                // 在当前for循环里都会被跳过
                continue;
            }

            // 做出选择
            temp.add(nums[i]);
            used[i] = Boolean.TRUE;
            backtrack(nums, used, temp);

            // 撤销选择
            temp.remove(temp.size() - 1);
            used[i] = Boolean.FALSE;
        }
    }

正确的原因

(1) 原数组需要排序, 排序完后, 如何去重的逻辑需要注意