【算法练习题】力扣练习题——数组(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])
		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,我也没有办法。。。


总结

  

这次我意识到了

  • 找合适实例的价值
  • 对于边界条件的推导也更有耐心

猜你喜欢

转载自www.cnblogs.com/RicardoIsLearning/p/12047752.html