leetcode 46&60 求全排列类题目

276 阅读8分钟

最近在leetcode做了道全排列的题目,相比网上的常规解法有一定优化,所以记录下来。第一部分是简单的求全排列,第二部分是按字符序求全排列,并返回指定位置的值

1. 求出全排列

1.1 题目详情(leetcode 46. 全排列)

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

示例:

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

1.2 思路

一个分治的思想就搞定了,全排列就是每次从序列组合里面挑选一个值作为生成序列的第一个值,剩下的元素作为子序列。然后子序列再挑出来一个值,作为生成的第一个值,剩下的元素再作为子序列的子序列。

我们拿[1,2,3,4]作为例子。

  1. 第一步
    序列:(1,2,3,4)
    取一个值出来,有下面的可能
    1 (2,3,4)
    2 (1,3,4)
    3 (1,2,4)
    4 (1,2,3)
    这是第一步的过程,生成了部分目标序列和子序列,我们拿1 (2,3,4)示范第二步
  2. 第二步
    序列: 1 (2,3,4)
    取一个值出来,有下面的可能
    12 (3,4)
    13 (2,4)
    14 (2,3)
    第三步拿14 (2,3)示范
  3. 第三步
    序列: 14(2,3)
    取一个值出来,有下面的可能
    142 (3)-> 1423
    143 (2)-> 1432
    基本到达这部就能得出来所有的结果了。

总结 我们有一个序列,要求全排列的话,我们只用每次从这个序列里面拿出来一个值,添加到我们生成的全排列字符串的末尾。然后再对子序列进行这样的操作,一直到结束。

1.3 实现代码

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

public class Permutations_46 {    
    public List<List<Integer>> permute(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        getPermute(result, nums, 0);
        return result;
    }

    private void getPermute(List<List<Integer>> result, int[] nums, int deep) {
        // 递归出口,当递归了nums.length也就是已经生成了一个排列时,存储起来
        if (deep == nums.length - 1) {
            // 深拷贝,并存储
            ArrayList<Integer> list = new ArrayList<>();
            for (int value : nums) {
                list.add(value);
            }
            result.add(list);
        }
        // 不满足出口,继续递归
        else {
            // 每次都从序列里面挑出来一个值添加到排列序列后面,然后再进行递归
            for (int i = deep; i < nums.length; i++) {
                swap(deep, i, nums);  // 交换值
                getPermute(result, nums, deep + 1);  // 继续递归
                swap(i, deep, nums);  // 恢复值
            }
        }
    }

    private void swap(int deep, int i, int[] nums) {
        if (deep == i) return;
        int tmp = nums[deep];
        nums[deep] = nums[i];
        nums[i] = tmp;
    }
}

2. 返回第K个排列

2.1 题目详情(leetcode 60 第K个排列)

给出集合 [1,2,3,…,n],其所有元素共有 n! 种排列。

按大小顺序列出所有排列情况,并一一标记,当 n = 3 时, 所有排列如下:

"123"
"132"
"213"
"231"
"312"
"321"
给定 n 和 k,返回第 k 个排列。

说明:

给定 n 的范围是 [1, 9]。
给定 k 的范围是[1, n!]。
示例 1:

输入: n = 3, k = 3 输出: "213"

示例 2:

输入: n = 4, k = 9 输出: "2314"

2.2 思路

我们这里使用三个解法来做,每一个都比前一个更优化

  • 解法一: 使用上面的求全排列的解法,然后再最后排序
  • 解法二: 优化上面的全排列算法,直接求出第K个排列
  • 解法三: 研究排列的规律,直接找到第K个排列

2.3 解法一

这个解法目测会超时,并且我觉得很挫,不是最优的解法,略过不讲

2.4 解法二

2.4.1 思路

这里我们和开篇讲到的求全排列的方法有很强的联系。我们先来分析一下按字典序排列的全排列有何不同。

示例: 我们对比一下以为例子的按序全排列和第一步生成的非按序全排列(以4为例子)

字典序排列的全排列 非按序全排列
1, 2, 3, 4 1, 2, 3, 4
1, 2, 4, 3 1, 2, 4, 3
1, 3, 2, 4 1, 3, 2, 4
1, 3, 4, 2 1, 3, 4, 2
1, 4, 3, 2 1, 4, 3, 2
1, 4, 2, 3 1, 4, 2, 3
2, 1, 3, 4 2, 1, 3, 4
2, 1, 4, 3 2, 1, 4, 3
2, 3, 1, 4 2, 3, 1, 4
2, 3, 4, 1 2, 3, 4, 1
2, 4, 1, 3 2, 4, 3, 1
2, 4, 3, 1 2, 4, 1, 3
3, 1, 2, 4 3, 2, 1, 4
3, 1, 4, 2 3, 2, 4, 1
3, 2, 1, 4 3, 1, 2, 4
3, 2, 4, 1 3, 1, 4, 2
3, 4, 1, 2 3, 4, 1, 2
3, 4, 2, 1 3, 4, 2, 1
4, 1, 2, 3 4, 2, 3, 1
4, 1, 3, 2 4, 2, 1, 3
4, 2, 1, 3 4, 3, 2, 1
4, 2, 3, 1 4, 3, 1, 2
4, 3, 1, 2 4, 1, 3, 2
4, 3, 2, 1 4, 1, 2, 3

可以很明显的看到右边的序列是无序的,左边的序列我们可以看出来这样的规律:第一位的排列顺序是1,2,3,4,是从小到大排列的。在第一位是1的情况下,第二位的排列顺序的2,3,4,第一位是2的情况下,第二位的排列顺序为1,3,4。得出的结论是,在从子序列中选取值时,总是倾向于先选择能选择的最小的值,那我们的代码思路也和上面的类似,只是每次都选出来最小的值。

2.4.2 实现代码

class Solution {
    List<List<Integer>> sortResult = new ArrayList<>(); // 存储排序全排列序列的list
    List<Integer> tmpList = new ArrayList<>(); // 临时存储一个生成的序列
    public String getPermutation(int n, int k) {
        // 将值使用int数字存储起来,并约定正数是未使用的数字,负数是是用了的数字
        int[] words = new int[n];
        for (int i = 0; i < n; i++) {
            words[i] = i + 1;
        }
        getArrange(words, 0, n, k);
        // 生成返回值
        String value = "";
        for(int inner: sortResult.get(k-1)){
            value+=inner;
        }
        return value;
    }

    public void getArrange(int[] words, int deep, int n, int k) {
        // 出口条件,深度等于初始序列长度
        if (deep == n) {
            //深拷贝tmpList并存入结果list中
            ArrayList<Integer> tmp = new ArrayList<>();
            for (Integer inner : tmpList) {
                tmp.add(inner);
            }
            this.sortResult.add(tmp);
        }
        // 出口条件,如果已经得到了需要的第K个序列,则终止运算
        if (this.sortResult.size() == k) {
            return;
        }
        // 从小到大按顺序选取未使用的值,
        for (int i = 0; i < words.length; i++) {
            // 如果为正数,则未使用
            if (words[i] > 0) {
                // 选出来的值加入到tmpList
                tmpList.add(words[i]);
                // 标记元素已使用
                words[i] = -words[i];
                getArrange(words, deep + 1, n, k);
                 // 剪枝,出口条件,如果已经得到了需要的第K个序列,则终止运算
                if (this.sortResult.size() == k) {
                    return;
                }
                // 恢复状态
                words[i] = -words[i];
                tmpList.remove(tmpList.size() - 1);
            }
        }
    }
}

2.5 解法三

2.5.1 思路

解法三我们直接找规律求出来需要的值,我们还是使用4生成的有序全排列序列作为例子。 我们先看所有的序列
1, 2, 3, 4
1, 2, 4, 3
1, 3, 2, 4
1, 3, 4, 2
1, 4, 3, 2
1, 4, 2, 3
2, 1, 3, 4
2, 1, 4, 3
2, 3, 1, 4
2, 3, 4, 1
2, 4, 1, 3
2, 4, 3, 1
3, 1, 2, 4
3, 1, 4, 2
3, 2, 1, 4
3, 2, 4, 1
3, 4, 1, 2
3, 4, 2, 1
4, 1, 2, 3
4, 1, 3, 2
4, 2, 1, 3
4, 2, 3, 1
4, 3, 1, 2
4, 3, 2, 1
其中1,2,3,4开头的排列各有6个。在1开头的排列中,第二个字符是2,3,4的分别又占了两个;在2开头的排列中,第二个字符是1,3,4的分别又占了两个。

推广到5生成的有序全排列,1,2,3,4,5开头的排列各有24个; 6生成的有序全排列,1,2,3,4,5开头的排列各有120个; ......

在这个规律下我们可以根据给定的数字大小和要求的位置直接找到对应的数字,比如给定的n=4,k=10 我们先将k-1,因为计算机是从0开始计数的。下面的计算商指向了当前位置的值,余数用来计算后面序列的排列情况,除数为(n-1)!
9/6 = 1 ... 3 (商1,则以当前序列中第二大的数字开头) 2 (1,3,4)
3/2 = 1 ... 1 (商1,则以当前系列中第二大的数字开头) 2,3 (1,4)
1/1 = 1 ... 0 (商1,则以当前系列中第二大的数字开头) 2,3,4 (1)
最后2,3,4,1

2.5.2 实现代码

class Solution {
    public String getPermutation(int n, int k) {
        int[] words = new int[n];
        for (int i = 0; i < n; i++) {
            words[i] = i + 1;
        }
        // 位置
        int remain = k - 1;
        // 序列还剩几个值需要求
        int range = n;
        String value = "";
        while (true) {
            // 序列不等于0,反复做除法
            if (remain != 0) {
                int count = calcCount(range - 1);
                // 指向位置
                int position = remain / count;
                remain = remain % count;
                int innerPosi = 0;
                for (int i = 0; i < n; i++) {
                    if (words[i] > 0) {
                        if (innerPosi != position) {
                            innerPosi++;
                        } else {
                            value += words[i];
                            range--;
                            words[i] = -words[i];
                            break;
                        }
                    }
                }
            } 
            // 序列等于0,直接从小到大取未使用值
            else {
                for (int i = 0; i < n; i++) {
                    if (words[i] > 0) {
                        value += words[i];
                    }
                }
                break;
            }
        }
        return value;
    }

    // 生成除数
    public int calcCount(int k) {
        if (k <= 2) {
            return k;
        }
        return k * calcCount(k - 1);
    }
}

2.5.3 我们来看看耗时