31. Next Permutation

题目

整数数组的排列是将其成员排列成序列或线性顺序。

例如arr = [1,2,3],下面是arr的所有排列:[1,2,3], [1,3,2], [2, 1, 3], [2, 3, 1], [3,1,2], [3,2,1]。

整数数组的下一个排列是其整数的下一个字典顺序更大的排列。

更正式地说,如果数组的所有排列都根据其字典顺序在一个容器中排序,则该数组的下一个排列是排序容器中它后面的排列。

如果这样的排列是不可能的,则数组必须重新排列为尽可能低的顺序(即按升序排序)。

例如,arr = [1,2,3] 的下一个排列是 [1,3,2]。

同样,arr = [2,3,1] 的下一个排列是 [3,1,2]。

而 arr = [3,2,1] 的下一个排列是 [1,2,3] 因为 [3,2,1] 没有字典序更大的重排。

给定一个整数数组 nums,找到 nums 的下一个排列。

替换必须到位并且只使用常量的额外内存。

EX

Example 1:

Input: nums = [1,2,3]
Output: [1,3,2]

Example 2:

Input: nums = [3,2,1]
Output: [1,2,3]

Example 3:

Input: nums = [1,1,5]
Output: [1,5,1]

Constraints:

1 <= nums.length <= 100

0 <= nums[i] <= 100

思路

这个题目其实不难。

暴力算法

这一题初看是各种排列组合,比如数字 {1, 2, 3}

你可能需要考虑从 1 开始 1 + {2, 3} 的各种排列; 然后是 2 + {1, 3};最后是 3 + {1, 2}。

一种最粗暴的方法是按照题目的规则,先把所有的排列列出来,然后依次寻找。

比较的方式

但是实际上不需要这么麻烦。

  1. 如何判断有木有下一个呢?只要存在a[i-1] < a[i]的升序结构,就有,而且我们应该从右往左找

  2. 当发现a[i-1] < a[i]的结构时,从在[i, ∞]中找到最接近a[i-1]并且又大于a[i-1]的数字,由于降序,从右往左遍历即可得到k

  3. 然后交换a[i-1]与a[k],然后对[i, ∞]排序即可,排序只需要首尾不停交换即可,因为已经是降序

例子

比如 {1, 3, 2} 下一个排列是什么呢?

1) 如何判断有木有下一个呢?

我们从右往左寻找。

1 < 3 比较时,中断。此时 i = 1,对应元素 3。

int i = 0;
for(i = len-1; i > 0; i--) {
    // 小于
    if(nums[i-1] < nums[i]) {
        // 记录索引
        break;
    }
}

2)当发现 a[i-1] < a[i] 的结构时,从在[i, ∞]中找到最接近a[i-1]并且又大于a[i-1]的数字,由于降序,从右往左遍历即可得到k

然后交换a[i-1]与a[k]

//132  从右向左边,找到第一个小于等于 index 的数 && > index-1
//从 index=>len 找到大于 n
// 存在
if(i > 0) {
    int preVal = nums[i-1];     // 1
    int indexVal = nums[i];     // 3
    int k;
    for(k = len-1; k > i; k--) {
        // 
        if(nums[k] > preVal && nums[k] <= indexVal) {
            // 跳出循环
            break;
        }
    }
    // 交换 k 和 i-1
    swap(nums, k, i-1);
}

{1 3 2}, 此时 i = 1

从右往左遍历,在 nums[k] 比 preVal 1 大,且小于等于 nums[i] = 3。

此时的 k = 2, 交换位置 nums[2] 和 nums[i-1]。得到:{2, 3, 1}。

3) 然后对[i, ∞]排序即可,排序只需要首尾不停交换即可,因为已经是降序

// 将 i => len-1 执行排序
reverse(nums, i, nums.length-1);

java 实现

    public void nextPermutation(int[] nums) {
        //fast-return
        int len = nums.length;
        if(len == 1) {
            return;
        }

        // 132 倒叙查看
        int i = 0;
        for(i = len-1; i > 0; i--) {
            // 小于
            if(nums[i-1] < nums[i]) {
                // 记录索引
                break;
            }
        }

        //132  从右向左边,找到第一个小于等于 index 的数 && > index-1
        //从 index=>len 找到大于 n
        // 存在
        if(i > 0) {
            int preVal = nums[i-1];
            int indexVal = nums[i];
            int k;
            for(k = len-1; k > i; k--) {
                if(nums[k] > preVal && nums[k] <= indexVal) {
                    // 跳出循环
                    break;
                }
            }
            // 交换 k 和 i-1
            swap(nums, k, i-1);
        }

        // 将 i => len-1 执行排序
        reverse(nums, i, nums.length-1);
    }

    /**
     * 交换
     * @param nums 数组
     * @param one 第一个下标
     * @param two 第二个下标
     */
    private static void swap(int[] nums, int one, int two) {
        int temp = nums[one];
        nums[one] = nums[two];
        nums[two] = temp;
    }

    /**
     * 逆序
     * @param nums 数组
     * @param startIndex 开始
     * @param endIndex 结束
     */
    private static void reverse(int[] nums, int startIndex, int endIndex) {
        while (startIndex < endIndex) {
            swap(nums, startIndex++, endIndex--);
        }
    }

46. Permutations

题目

给定一个包含不同整数的数组 nums,返回所有可能的排列。

您可以按任何顺序返回答案。

例子

Example 1:

Input: nums = [1,2,3]
Output: [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

Example 2:

Input: nums = [0,1]
Output: [[0,1],[1,0]]

Example 3:

Input: nums = [1]
Output: [[1]]

Constraints:

1 <= nums.length <= 6

-10 <= nums[i] <= 10

All the integers of nums are unique. nums 的所有整数都是唯一的。

分析

所有的排列组合,这一题使用回溯解决就非常自然。

java 实现

    public List<List<Integer>> permute(int[] nums) {
        // 这个个数实际上可以预测:就是 nums ! 阶乘。
        final int len = nums.length;
        List<List<Integer>> results = new ArrayList<>();

        // 避免扩容
        List<Integer> tempList = new ArrayList<>(len);
        backtrack(results, tempList, nums);
        return results;
    }

    private void backtrack(List<List<Integer>> results, List<Integer> tempList, int[] nums) {
        // 什么时候停止
        final int len = nums.length;
        if(tempList.size() == len) {
            results.add(new ArrayList<>(tempList));
        } else {
            // 回溯
            for (int current : nums) {
                // 元素不能重复
                if (tempList.contains(current)) {
                    continue;
                }

                tempList.add(current);

                // 下一个元素
                backtrack(results, tempList, nums);

                // 回溯
                tempList.remove(tempList.size() - 1);
            }
        }
    }

60. Permutation Sequence

题目

集合 [1, 2, 3, …, n] 总共包含 n! 独特的排列。

通过按顺序列出和标记所有排列,我们得到以下 n = 3 的序列:

“123”
“132”
“213”
“231”
“312”
“321”

给定 n 和 k,返回第 k 个排列序列。

Ex

Example 1:

Input: n = 3, k = 3
Output: "213"

Example 2:

Input: n = 4, k = 9
Output: "2314"

Example 3:

Input: n = 3, k = 1
Output: "123"

Constraints:

1 <= n <= 9

1 <= k <= n!

V1-基于 backtrack 实现

我们在 T46 中已经知道了如何计算数字序列的排列组合,然后到底 k 个直接返回。

java 实现

    public String getPermutation(int n, int k) {
        List<List<Integer>> all = new ArrayList<>();

        List<Integer> tempList = new ArrayList<>();
        backtrack(all, tempList, n, k, 1);

        // 返回列表中的最后一个。
        List<Integer> integers = all.get(k-1);
        StringBuilder stringBuilder = new StringBuilder();
        for(Integer i : integers) {
            stringBuilder.append(i);
        }
        return stringBuilder.toString();
    }

    // 如何可以簡化這個操作呢?
    // 其實我們只關心第 K 個元素
    private void backtrack(List<List<Integer>> all, List<Integer> tempList,
                           int n, int k, int start) {
        if(tempList.size() == n) {
            // 满了
            all.add(new ArrayList<>(tempList));

            // 如果大小已经够了,直接剪枝
            if(all.size() >= k) {
                return;
            }
        }

        for(int i = 1; i <= n; i++) {
            // 跳过重复的元素
            if(tempList.contains(i)) {
                continue;
            }

            tempList.add(i);
            // 下一个元素
            backtrack(all, tempList, n, k, start+1);
            // 移除,回溯
            tempList.remove(tempList.size()-1);
        }
    }

不过很不幸,在 85/200 的时候,执行超时。

V2-换种思路

这就是 hard 题目的坑爹之处。

也就是需要追求一种妙手解法,才能解决这个问题。

“Explain-like-I’m-five” Java Solution in O(n)

思路

I'm sure somewhere can be simplified so it'd be nice if anyone can let me know. 

The pattern was that:

say n = 4, you have {1, 2, 3, 4}

If you were to list out all the permutations you have

1 + (permutations of 2, 3, 4)

2 + (permutations of 1, 3, 4)

3 + (permutations of 1, 2, 4)

4 + (permutations of 1, 2, 3)

我确定某个地方可以简化,所以如果有人可以让我知道,那就太好了。

模式是:

说 n = 4,你有 {1, 2, 3, 4}

如果你要列出你拥有的所有排列

1 +(2、3、4 的排列)

2 +(1、3、4 的排列)

3 +(1、2、4 的排列)

4 +(1、2、3 的排列)

We know how to calculate the number of permutations of n numbers... n! 

So each of those with permutations of 3 numbers means there are 6 possible permutations. 

Meaning there would be a total of 24 permutations in this particular one. So if you were to look for the (k = 14) 14th permutation, it would be in the

3 + (permutations of 1, 2, 4) subset.

To programmatically get that, you take k = 13 (subtract 1 because of things always starting at 0) and divide that by the 6 we got from the factorial, which would give you the index of the number you want. In the array {1, 2, 3, 4}, k/(n-1)! = 13/(4-1)! = 13/3! = 13/6 = 2. The array {1, 2, 3, 4} has a value of 3 at index 2. So the first number is a 3.

我们知道如何计算n个数的排列数……n!

因此,每个具有 3 个数字排列的数字都意味着有 6 种可能的排列。

这意味着在这个特定的排列中总共会有 24 种排列。

所以如果你要寻找 (k = 14) 第 14 个排列,它会在

3 +(1、2、4 的排列)子集。

要以编程方式获得它,您需要 k = 13(减去 1,因为事物总是从 0 开始)并将其除以我们从阶乘中得到的 6,这将为您提供所需数字的索引。

在数组{1, 2, 3, 4}中,k/(n-1)! = 13/(4-1)! = 13/3! = 13/6 = 2.

数组 {1, 2, 3, 4} 在索引 2 处的值为 3。因此第一个数字是 3。

Then the problem repeats with less numbers.

The permutations of {1, 2, 4} would be:

1 + (permutations of 2, 4)

2 + (permutations of 1, 4)

4 + (permutations of 1, 2)

But our k is no longer the 14th, because in the previous step, we've already eliminated the 12 4-number permutations starting with 1 and 2. So you subtract 12 from k.. which gives you 1. Programmatically that would be...

然后问题以更少的数字重复。

{1, 2, 4} 的排列是:

1 +(2、4 的排列)

2 +(1、4 的排列)

4 +(1、2 的排列)

但是我们的 k 不再是第 14 个,因为在上一步中,我们已经消除了以 1 和 2 开头的 12 个 4 数排列。

所以你从 k 中减去 12.. 得到 1。

以编程方式就是这样。 k = k - (index from previous) * (n-1)! = k - 2*(n-1)! = 13 - 2*(3)! = 1

In this second step, permutations of 2 numbers has only 2 possibilities, meaning each of the three permutations listed above a has two possibilities, giving a total of 6. We're looking for the first one, so that would be in the 1 + (permutations of 2, 4) subset.

Meaning: index to get number from is k / (n - 2)! = 1 / (4-2)! = 1 / 2! = 0.. from {1, 2, 4}, index 0 is 1


so the numbers we have so far is 3, 1... and then repeating without explanations.


{2, 4}

k = k - (index from pervious) * (n-2)! = k - 0 * (n - 2)! = 1 - 0 = 1;

third number's index = k / (n - 3)! = 1 / (4-3)! = 1/ 1! = 1... from {2, 4}, index 1 has 4

Third number is 4


{2}

k = k - (index from pervious) * (n - 3)! = k - 1 * (4 - 3)! = 1 - 1 = 0;

third number's index = k / (n - 4)! = 0 / (4-4)! = 0/ 1 = 0... from {2}, index 0 has 2

Fourth number is 2


Giving us 3142. If you manually list out the permutations using DFS method, it would be 3142. Done! It really was all about pattern finding.

在第二步中,2 个数字的排列只有 2 种可能性,这意味着上面列出的三种排列中的每一种都有两种可能性,总共有 6 种可能性。

我们正在寻找第一种,所以它会在 1 + (2、4 的排列)子集。

含义:从中获取数字的索引是 k / (n - 2)! = 1 / (4-2)! = 1 / 2! = 0 来自 {1, 2, 4},索引 0 为 1

所以我们目前拥有的数字是 3、1… 然后不加解释地重复。

个人感受

这里的巧妙之处在于,我们在给定个数之后,首先统计每个数字对应的排列组合个数。

然后不断的重复这个过程,得到结果。

这里的 k 可谓用的出神入化。

java 实现

public String getPermutation(int n, int k) {
    List<Integer> numbers = new ArrayList<>();
    int[] factorial = new int[n+1];
    StringBuilder sb = new StringBuilder();
    // create an array of factorial lookup
    int sum = 1;
    factorial[0] = 1;
    for(int i=1; i<=n; i++){
        sum *= i;
        factorial[i] = sum;
    }
    // factorial[] = {1, 1, 2, 6, 24, ... n!}
    // create a list of numbers to get indices
    for(int i=1; i<=n; i++){
        numbers.add(i);
    }
    // numbers = {1, 2, 3, 4}
    k--;
    for(int i = 1; i <= n; i++){
        int index = k/factorial[n-i];
        sb.append(numbers.get(index));
        numbers.remove(index);
        k-=index*factorial[n-i];
    }
    return String.valueOf(sb);
}

我们以 {1,2,3,4} 第 14 个为例子。

factorial: [1, 1, 2, 6, 24]

SB:3, index: 2, k: 13, nums: [1, 2, 3, 4]

SB:31, index: 0, k: 1, nums: [1, 2, 4]

SB:314, index: 1, k: 1, nums: [2, 4]

SB:3142, index: 0, k: 0, nums: [2]

参考资料

https://leetcode.com/problems/next-permutation/description/

https://leetcode.com/problems/permutations/

https://leetcode.com/problems/permutation-sequence/discuss/22507/%22Explain-like-I’m-five%22-Java-Solution-in-O(n)