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



总结

  

这次我意识到了

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

原文地址:https://www.cnblogs.com/RicardoIsLearning/p/12047752.html

时间: 2024-11-09 02:18:19

【算法练习题】力扣练习题——数组(4):下一个排列的相关文章

小算法-计算下一个排列

2 8 5 3 1 1.从后往前,找到第一个逆序的数 pivot 2.从后往前,找到第一个比pivot大的数 change 3.交换 pivot 和 change的值 4.把pivot这个位置后面的数 reverse,就是 8 5 2 1变成 1 2 5 8 最终为3 1 2 5 8 #include <iostream> #include <vector> #include <algorithm> using namespace std; /* * num.begin

【算法题7】寻找下一个排列

来自:LeetCode 37 实现获取下一个排列的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列. 如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列). 必须原地修改,只允许使用额外常数空间. 以下是一些例子,输入位于左侧列,其相应输出位于右侧列.1,2,3 → 1,3,23,2,1 → 1,2,31,1,5 → 1,5,1 解决方案: 参见博客:Next lexicographical permutation algorithm 代码如下: 1 class

【LeetCode】下一个排列【找规律】

实现获取下一个排列的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列. 如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列). 必须原地修改,只允许使用额外常数空间. 以下是一些例子,输入位于左侧列,其相应输出位于右侧列. 1,2,3 → 1,3,2 3,2,1 → 1,2,3 1,1,5 → 1,5,1 来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/next-permutation 分析: 方法1:直接调

已知一个排列求下一个排列(NOIP2004P4 火星人 题解)转

题目描述略) 本题题意为求给定长度为 n 的数列的后第 m 个全排列(字典序). 对于一个给定的数列 a[0 .. n-1],求其下一个字典序的全排列算法如下: 从右向左查询最大的下标 i (0 ≤ i ≤ n-1) 使得 a[i] < a[i+1]: 从左向右查询最小的元素 a[j] (i+1 ≤ j ≤ n-1) 使得 a[i] < a[j]: 交换 a[i] 和 a[j]: 逆置翻转 a[i+1 .. n-1]. 算法分析:我们可以发现,第一步求出的 i 下标表示 a[i+1 .. n-

LeetCode 31. 下一个排列(Next Permutation)

题目描述 实现获取下一个排列的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列. 如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列). 必须原地修改,只允许使用额外常数空间. 以下是一些例子,输入位于左侧列,其相应输出位于右侧列. 1,2,3 → 1,3,2 3,2,1 → 1,2,3 1,1,5 → 1,5,1 解题思路 由于各个排列按照字典序排序,所以以 1,3,2 → 2,1,3为例,寻找下一个排列的步骤是: 首先找到从后往前第一个升序数对,在此例中即(1

Leetcode_31【下一个排列】

文章目录: 题目 脚本一 脚本一逻辑 题目: 实现获取下一个排列的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列. 如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列). 必须原地修改,只允许使用额外常数空间. 以下是一些例子,输入位于左侧列,其相应输出位于右侧列.1,2,3 → 1,3,23,2,1 → 1,2,31,1,5 → 1,5,1 脚本一:[用时:50ms][ 转载] class Solution: def nextPermutation(self,

【LeetCode】下一个排列与全排列问题

(一)下一个排列 题目(Medium):31. 下一个排列 题目描述: ??实现获取下一个排列的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列. ??如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列). ??必须原地修改,只允许使用额外常数空间. ??以下是一些例子,输入位于左侧列,其相应输出位于右侧列. ??1,2,3 → 1,3,2 ??3,2,1 → 1,2,3 ??1,1,5 → 1,5,1 解题思路: ??本题有一个比较固定的算法思路,可以总结为以下

lintcode 中等题:next permutation下一个排列

题目 下一个排列 给定一个整数数组来表示排列,找出其之后的一个排列. 样例 给出排列[1,3,2,3],其下一个排列是[1,3,3,2] 给出排列[4,3,2,1],其下一个排列是[1,2,3,4] 注意 排列中可能包含重复的整数 解题 和上一题求上一个排列应该很类似 1.对这个数,先从右到左找到递增序列的前一个位置,peakInd 2.若peakInd = -1 这个数直接逆序就是答案了 3.peakInd>= 0 peakInd这个位置的所,和 peakInd 到nums.size() -1

Leetcode:Next Permutation 下一个排列

Next Permutation: Implement next permutation, which rearranges numbers into the lexicographically next greater permutation of numbers. If such arrangement is not possible, it must rearrange it as the lowest possible order (ie, sorted in ascending ord