每日一题——第k个排列

菜鸡每日一题系列打卡60

每天一道算法题目 

小伙伴们一起留言打卡

坚持就是胜利,我们一起努力!

题目描述(引自LeetCode)

给出集合[1,2,3,…,n],其所有元素共有n!种排列。按大小顺序列出所有排列情况,并一一标记,当n = 3时, 所有排列如下:

"123"
"132"
"213"
"231"
"312"
"321"

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

示例 1:
输入: n = 3, k = 3
输出: "213"


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

说明:

  • 给定n的范围是[1, 9]。

  • 给定k的范围是[1,  n!]。

题目分析

这又是一道有关排列的题目,之前已经做过好几道关于排列的题目,菜鸡将其集中整理,放在文末的相关链接里,感兴趣的小伙伴可以进行对比学习。

另外,其实,按照数学家的思维(把新问题转化为原先的问题),这道题目完全可以依据我们先前做过的每日一题——下一个排列进行解答。从第一个排列开始,按照下一个排列中的方法执行k - 1次即可得到答案,时间复杂度为O(nk)。因此,并非leetcode官网所说的不能使用下一个排列中的方法进行求解,只不过时间复杂度较高而已。Talk is cheap, show me the code! 菜鸡将在代码实现中给出这种解法的代码。

既然上述方法的时间复杂度比较高,我们来寻求一个更高效的方法。已知n个数字组成的排列有n!种,可以由小到大的顺序将其平均分为n组,这n组排列有一个特点,每一组内部的(n - 1)!种排列的第一个数字都相同。这样,我们就可以根据k的值确定其分组,也就可以确定第一个数字。然后对剩余的数字利用同样的方式进行递归,即可得到结果。

话不多说,上代码!小伙伴们可以对两种解题方式进行比较学习。

代码实现

// 完全依据下一个排列中的算法求解
class Solution {
    public String getPermutation(int n, int k) {
        int[] nums = new int[n];
        for (int i = 0; i < n; i++) {
            nums[i] = i + 1;
        }
        // 从第一个排列开始找k - 1个排列
        for (int i = 1; i < k; i++) {
            nextPermutation(nums);
        }
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < n; i++) {
            builder.append(nums[i]);
        }
        return builder.toString();
    }
    // 下一个排列
    public void nextPermutation(int[] nums) {
        if (nums == null) return;
        int i = nums.length - 1, k = i;
        while (i > 0 && nums[i] <= nums[i - 1]) i--;
        if (i > 0) {
            int j = k;
            while (nums[j] <= nums[i - 1]) j--;
            swap(nums, i - 1, j);
        }
        while (i < k) swap(nums, i++, k--);
    }
    // 交换
    private void swap(int[] nums, int i, int j) {
        int tmp = nums[i];
        nums[i] = nums[j];
        nums[j] = tmp;
    }
}
// 将n!种排列分为n组,根据k值确定分组,选取第一个数字
// 然后对剩余的数字按上述方式进行递归,直到n为1
class Solution {
    // 记录是否访问
    private boolean[] visited;


    public String getPermutation(int n, int k) {
        int factorial = 1;
        for (int i = 2; i < n; i++) factorial *= i;
        visited = new boolean[n];
        return permutate(n, k, factorial);
    }


    private String permutate(int n, int k, int factorial) {
        // 标记访问位置
        int flag = 0;
        // 偏移
        int offset = k % factorial;
        // 分组
        int group = k / factorial + (offset > 0 ? 1 : 0);
        while (flag < visited.length && group > 0) if (!visited[flag++]) group--;
        visited[flag - 1] = true;
        // 递归
        if(n > 1) return String.valueOf(flag) + permutate(n - 1, offset == 0 ? factorial : offset, factorial / (n - 1));
        // 递归终止条件
        else return String.valueOf(flag);
    }


}

代码分析

对代码进行分析,第一种方式的时间复杂度为O(nk),空间复杂度为O(n);而第二种方式的时间复杂度为O(n^2),空间复杂度为O(n)。由于k的最大值远远大于n,因此,最坏情况下,第二种方式的时间效率更高。

执行结果

下一个排列解法的执行结果

分组递归解法的执行结果

相关链接

每日一题——全排列

每日一题——全排列II

每日一题——下一个排列

学习 | 工作 | 分享

????长按关注“有理想的菜鸡

只有你想不到,没有你学不到

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值