zoukankan      html  css  js  c++  java
  • 【力扣算法】数组(4):下一个排列

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

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

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

    以下是一些例子,输入位于左侧列,其相应输出位于右侧列。
    1,2,3 → 1,3,2
    3,2,1 → 1,2,3
    1,1,5 → 1,5,1


    原题链接:https://leetcode-cn.com/problems/next-permutation


    题意分析:

    先给出几个实例

    1)123 → 132

    2)10203 → 10230

    3)102320  → 103022

    4)1079864  → 1084679

    5)321  → 123

    6)98700  → 00789

    以上是用LeetCode自带的控制台直接测试的,也算是本道题的一个小收获。

    本题的解题过程将随着实例的难度增加而不断拓展。

    解法一:暴力求解

    本道题用暴力求解是不可行的,时间复杂度是xx。

    解法二:我的解法(也是官方题解)

    PART1

    从实例(1)(2)可以看出,遍历应该从右往左进行。设遍历的循环变量为$i$,一旦找到$operatorname{nums}[i-1]$小于$operatorname{nums}[i]$,就说明对于当前数组排列下的整数,已经有一个比它大的数。从这点可以认为,本道题就是在做一次(大小)排列——一旦完成排列,就是所谓的“下一个”数,故操作结束

    所以第一代代码完成的就很快,如下(手写的、没有用IDE测试过)

    for(int i = nums.length - 1;i>=0 ; i--){
    	if(nums[i-1]>nums[i])//3,2,1
    		i--;
    	else {
    		swap(nums, nums[i-1], nums[i]);
    		break;
    	}
    }

    这里后接的代码就是用于满足实例(5)和(6)的。我当时的想法是,最终满足这两个实例的条件就是给到的数组是一个递减的数组。一旦有两个相邻位置不满足,那么就像上文所说的,一次交换之后就可以直接作为结果输出了。

    当然,我对这样的不定式是进行过推导的,当时所有的实例都是没有问题,后来我发现,问题就出现在“两个相邻位置”。

    PART2

    于是就看到了实例(3)的情况。这种情况出现的原因可以这样分析——

    假设整数为$(j+k-1)(j+k)(j+k-2) cdots(j)$,当对头两位数字进行交换时,得到的结果是$(j+k)(j+k-1)(j+k-2) cdots(j)$。从形式上看,得到是一个递减数列,但是从题目要求看却应该是$(j+k)(j)(j+1) cdots(j+k-1)$。换言之,真正的下一位,应该是最大的位数+1,然后其余位数进行递增排列。试想999到10000,9变成了10,其余的位数都是最小的0。

    所以代码应该是当进行大小交换后,对交换数字右边的全部数字进行递增排列

    这里涉及到两个问题:1)如何递增?2)在这个基础上不定式的推导是否安全?

    对于第一个问题,先要了解目标数列是什么情况?无序的、还是有序的?显然是有序的,且是递减的

    有数列${x-1, x, y, z, m, n}$,假设${y, z, m, n}$是无序的,若$m<n$,那么开始遍历时,就已经进行了交换(看上去就是实例(1)),由此根本不再涉及${x-1, x}$的交换,矛盾;如果不是递减的,同样可以采用$m<n$的例子说明,矛盾。

    然后对于这个递减数列如何变成递增呢?直接的算法就是排序算法。但是一来题目有要求,二来真的有必要么?由于是有序数列,递减变成递增还可以通过交换数列两端的数字完成。该部分代码如下:

    private void reverse(int[] nums, int L, int R) {
    	while(L<R){
    		swap(nums, L, R);
    		L++; R--;
    	}
    }
    

    这里又涉及到循环条件的考虑。若数列包含偶数个元素,自然$L<R$就足够满足;若是奇数个元素,似乎中间元素取不到,所以当时我想应该设置条件为$L leq R$,但是转念一想中间元素何须交换呢?所以当$L$和$R$都取到中间元素时,就可以终止了。

    对于第二个问题,考虑就比较复杂,这就涉及到$L$的取值,这里初步确定$L$等于循环变量$i$;$R$等于$nums.length-1$——

    如图a,当$i$取$nums .$length$-1$时,$L=R$,如无需进行颠倒数列的操作

     

    如图b,当$i$取$nums .$length$-2$时,$L=nums.length-2$,在交换之后、再进行一次颠倒即可(偶数个)。

     

    如图c,当$i$取$nums .$length$-3$时,$L=nums.length-3$,在交换之后、再进行一次颠倒即可(奇数个)。

     

    由此,可以推导至所有情况,故安全。

    这样的话,之前的程序可以改进为:

    for(int i = nums.length - 1;i>=0 ; i--){
    	if(nums[i-1]>nums[i])
    		i--;
    	else {
    		swap(nums, nums[i-1], nums[i]);
                    reverse(nums, i, nums.length - 1);
    		break;
    	}
    }
    

      

    PART3

    关于相邻的问题,还引出了实例(4)的情况。这种情况的实质就是,交换的对象不一定是相邻的,或者说,为了得到下一更大的数字,应该在出现需要交换时,在交换的数字(索引为$i-1$)右边的数列中找到比数字大的最小值进行交换。这里查找的依据仍然是这个数列是递减数列。代码如下

    private int search(int[] nums, int head, int headindex) {
    	int index = headindex;
    	while(index<nums.length) {
    		if(nums[index]<=head) {//2,3,2,0
    			index--; 
    			return index;
    		}
    		index++;
    	}
    	return --index;//1,2,3
    }
    

    代码的思路就是从$i$处开始自左往右遍历,找到第一个比$i-1$小的数字的位置$index$,由于是递减数列,所以交换位置应该是$index-1$。然后接下去就是swap操作。

    这里同样有两个问题——

    第一个问题已经用注释给出实例了。若要被交换的数字是$2$,数列是$320$,那么第一个比$2$小的数字是$0$,如此一来就是$2$和数列中的$2$进行交换了,这就达不成目的。所以应该设置比较条件是小于等于(第4行)

    第二个问题出现在实例为$123$的情况下,即数组越界。这种情况是因为$3>2$,循环变量只能继续迭代,但是已经满足终止条件(此时$index=nums.length$),由于返回是$index$,此时将其作为索引进行交换时,就会越界。查到原因后才将第10行代码改成--index一开始是index--,仍然错误

    那另一种解决方案就是知道这种情况的前提下,直接返回$nums.length-1$

    PART4:

    以上问题解决之后,就是面对实例(5)和实例(6)了,直接上源码——

     1 public int[] nextPermutation(int[] nums) {
     2     if(nums.length < 1 || nums == null)
     3         return nums;
     4     
     5     int i = nums.length - 1;
     6     while(i>=1) {
     7         if(nums[i]<=nums[i-1])
     8             i--;
     9         else {
    10             int head = nums[i-1];
    11             int swapindex = search(nums, head, i);
    12             swap(nums, i-1, swapindex);
    13             reverse(nums, i, nums.length - 1);
    14             break;
    15         }
    16     }
    17     if(i==0)
    18         reverse(nums, i, nums.length - 1);
    19     return nums;
    20 }
    21 
    22 private int search(int[] nums, int head, int headindex) {
    23     int index = headindex;
    24     while(index<nums.length) {
    25         if(nums[index]<=head) {//2,3,2,0
    26             index--; 
    27             return index;
    28         }
    29         index++;
    30     }
    31     return nums.length - 1;
    32 }
    33 
    34 private void swap(int[] nums, int a, int b) {
    35     int tmp = nums[a];
    36     nums[a] = nums[b];
    37     nums[b] =tmp;      
    38 }
    39 
    40 private void reverse(int[] nums, int L, int R) {
    41     while(L<R){
    42         swap(nums, L, R);
    43         L++; R--;
    44     }
    45 }

    第7到15行就是PART1的增强版,解决了实例(1)-(4)。这里需要关注的是第6行循环的条件。一开始很自然地以为是$i geq 0$,但是看第8行就知道,当$i=0$时,这行执行时就会数组越界。故循环条件为$i geq 1$。

    一旦$i=0$(while循环会导出循环变量),就说明整个数列是一个递减的数列、即是一个最大值,因此,下一个大值就是返回最小值了,直接进行颠倒操作。

    那么其实我在PART1的时候,考虑到的情况应该是987000 → 70089,所以我当时想的是,应该还要有额外的标记。但是给的结果就是000789,我也没有办法。。。


    总结

      

    这次我意识到了

    • 找合适实例的价值
    • 对于边界条件的推导也更有耐心
  • 相关阅读:
    python中字母的大小写转换
    十进制转换为16进制
    查找数组中出现次数超过一半的数
    leetcode二分查找
    leetcode 3 字符串
    leetcode链表篇
    leetcode数组篇
    重构二叉树
    矩阵的特征向量和特征值
    微软编程
  • 原文地址:https://www.cnblogs.com/RicardoIsLearning/p/12047752.html
Copyright © 2011-2022 走看看