leetcode 31.下一个排列

分析LeetCode第31题,探讨如何原地修改数组以找到字典序中下一个更大的排列。当无法找到更大排列时,需将数组变为最小排列(升序)。文章详细介绍了算法思路,包括字典序概念、原地修改要求、代码设计及规律总结。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目

实现获取下一个排列的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列。

如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。

必须原地修改,只允许使用额外常数空间。

题目分析

1、字典序含义

在数学中,字典或词典顺序(也称为词汇顺序,字典顺序,字母顺序或词典顺序)是基于字母顺序排列的单词按字母顺序排列的方法。 这种泛化主要在于定义有序完全有序集合(通常称为字母表)的元素的序列(通常称为计算机科学中的单词)的总顺序。
对于数字1、2、3…n的排列,不同排列的先后关系是从左到右逐个比较对应的数字的先后来决定的。例如对于5个数字的排列 12354和12345,排列12345在前,排列12354在后。按照这样的规定,5个数字的所有的排列中最前面的是12345,最后面的是 54321。
按照题目要求,需要输出该序列的下一个排列,例如输入 nums={1,3,2,4,5} ,则应输出下一个排列 {1, 3, 2, 5, 4}

2、原地修改且额外空间为常数

题目中要求原地修改,因此我们尽量只对原数组进行操作;
额外空间为常数,则我们不能另外建立空间为n的辅助数组来进行计算;

3、如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)

若当前数组为字典中最大值,则其下一个数组为字典中最小值。

代码设计

1、代码初步构想

若我们抛开题目来看,对于任意一串数字,它的后一位数字都是修改其最小项,使其+1得到的,例如:

数字 542
若求它的后一位数字,只需要在其个位数上+1即可得到该数字的后一位数字 543

因此,该题的求解应该从数组尾部开始入手。

2、代码整体框架

我们可以定义一个切割点 index 从尾部往前分割数组,对于分割下来的数组进行判断,使得其 字典序
往后一位,此时,数组整体的 字典序 也就往后排了一位。因此,数组整体的下一个排序问题,被修正成了切割数组的下一个排列问题。

3、进一步讨论

如同上面所说的,数字 542 的下一个数是 543 一样,当我们要寻找一个数组的下一个字典序时,会发现如下现象:

假设当前数组 nums = {2, 3, 5, 8, 7} ,其下一个字典序应该是 {2, 3, 7, 5, 8} ,按照上述算法从后往前进行拆分,会有如下现象:
①切割数组: {7}
    此时仅有一个数字,继续切割;
①切割数组: {8, 7}
    此时,该序列为倒序序列,不存在下一个字典数,继续切割;
①切割数组: {5, 8, 7}
    此时,该序列不为倒序序列,存在下一个字典序,因此寻找该序列的下一个字典序。我们很容易找到比5大的下一个数字是7,因此将7放入5的位置;剩下来的数字要求排列的字典序最低,因此,该序列应该排列为: {7, 5, 8} ,由于排列了一次字典序,切割数组排列了下一个字典序相对于全体数组排列了字典序,因此,排列完成。

我们会发现,我们需要在 数字5 后面寻找比数字5大一点点的数与之替换,剩下的数则应该排列成最小值,也就是升序排列。

规律性总结

我们由上述的分析可以发现, 倒序排列 是循环继续的条件, 升序排列 则是输出的结果。因此,我们的算法大致如下:

1、从后往前切割数组
2、对切割数组进行倒序判断,是否前一个数大于等于后一个数
3、若是倒序数组,则将其变成升序,切割符向前移动一位,对新的切割数组进行判断
4、若不是倒序数组,则寻找后续数组中比切割符位置大的最小的数,剩余的数升序排列
5、若整个数组都是倒序数组,则对整个数组升序排列

代码如下,包括部分修正:

public void nextPermutation(int[] nums) {
    
    int max = nums[nums.length-1];  //初始化切割数组最大值
    
    for(int i = nums.length-2; i >= 0; i--) { // 循环切割数组
        if(nums[i] >= max) {  //若为逆序
            max = nums[i];
            for(int j = i ; j < nums.length-1; j++) { //逆序转顺序
                nums[j] = nums[j+1];
            }
            nums[nums.length-1] = max;
            continue;
        }else {
            int temp = nums[i];
            for(int j = i+1; j < nums.length; j++) { //寻找可互换的数字
                if(temp < nums[j]) {
                    nums[i] = nums[j];
                    nums[j] = temp;
                    break;
                }
            }
            break;
        }
    }
}

代码解释:
①设置最大值。是方便是否逆序的判断,从后往前切割数组时,若当前数组是逆序的,那么后一个切割数组中,新进入的数若大于当前切割数组中最大的数,那么下一个切割数组必然是逆序的;
②升序排列操作。若为逆序,则将新加入的数字直接放入切割数组末尾,切割数组的其他数字全部往前移动一位即可;若不为逆序,由于后续数字都是升序排列好的,因此,我们仅需从前往后寻找后续数字中第一个大于该数字的数与之互换,得到的即是下一个字典序。
③边界情况。该程序仅需考虑初始状态的情况。由于题目要求,如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列),该条件与程序中不断修正的升序算法不谋而合,因此,若到循环末尾也未能进行数字互换操作,数组本身已经替换为升序数组,无需进一步操作。

附:测试程序

public static void main(String[] args) {
    NextPermutation N = new NextPermutation();
    int[] nums = {1, 3, 2, 4, 5};
    N.nextPermutation(nums);
    for(int i = 0; i < nums.length; i++) {
        System.out.println(nums[i]);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值