31. 下一个排列
题目描述
整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。
- 例如,
arr = [1,2,3],以下这些都可以视作arr的排列:[1,2,3]、[1,3,2]、[3,1,2]、[2,3,1]。
整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。
- 例如,
arr = [1,2,3]的下一个排列是[1,3,2]。 - 类似地,
arr = [2,3,1]的下一个排列是[3,1,2]。 - 而
arr = [3,2,1]的下一个排列是[1,2,3],因为[3,2,1]不存在一个字典序更大的排列。
给你一个整数数组 nums ,找出 nums 的下一个排列。
必须 原地 修改,只允许使用额外常数空间。
示例 1:
text
输入:nums = [1,2,3]
输出:[1,3,2]示例 2:
text
输入:nums = [3,2,1]
输出:[1,2,3]示例 3:
text
输入:nums = [1,1,5]
输出:[1,5,1]提示:
1 <= nums.length <= 1000 <= nums[i] <= 100
思路拆解
我们要找到一个大于当前序列的新序列,且变大的幅度尽可能小。
从后向前查找第一个升序对:
- 我们需要找到两个相邻元素
nums[i]和nums[i+1],满足nums[i] < nums[i+1]。 - 这样,
nums[i]就是需要被替换成更大数字的位置(称为“较小数”),因为交换nums[i]后,序列变大的幅度主要由高位决定,越靠右的高位变动幅度越小。 - 如果在整个数组中都找不到这样的相邻元素,说明数组已经是降序排列(最大排列),直接将其反转为升序(最小排列)即可。
- 我们需要找到两个相邻元素
从后向前查找第一个比
nums[i]大的数:- 在
i之后的部分(即nums[i+1]到nums[n-1]),由于步骤 1 中i是第一个满足升序的位置,所以nums[i+1]到nums[n-1]必然是降序的。 - 我们需要在这个降序区间中,找到第一个比
nums[i]大的数nums[j](称为“较大数”)。 - 因为
nums[i+1]到nums[n-1]是降序的,所以从后往前找,第一个满足nums[j] > nums[i]的j就是我们要找的最小的“较大数”。
- 在
交换
nums[i]和nums[j]:- 交换后,
i位置变大了,整个序列的字典序变大了。 - 但是,
i之后的部分可能还不是最小的排列。
- 交换后,
反转
i之后的部分:- 由于交换前
nums[i+1]到nums[n-1]是降序的,交换后(nums[j]换到i,nums[i]换到j),这一段依然保持降序性质(因为nums[j]是第一个比nums[i]大的,交换后该位置变小了,但对于降序序列来说不影响整体趋势,严谨证明略,但直观上成立)。 - 为了让变大的幅度尽可能小,我们需要让
i之后的部分变为升序(最小排列)。 - 因为这部分目前是降序的,直接 反转 即可变为升序。
- 由于交换前
代码实现
javascript
/**
* @param {number[]} nums
* @return {void} Do not return anything, modify nums in-place instead.
*/
var nextPermutation = function(nums) {
const n = nums.length;
let i = n - 2;
// 1. 从后向前找到第一个升序对 (nums[i] < nums[i+1])
while (i >= 0 && nums[i] >= nums[i + 1]) {
i--;
}
if (i >= 0) {
// 2. 如果找到了 i,则在 i 后面找到第一个比 nums[i] 大的数
let j = n - 1;
while (j > i && nums[j] <= nums[i]) {
j--;
}
// 3. 交换 nums[i] 和 nums[j]
[nums[i], nums[j]] = [nums[j], nums[i]];
}
// 4. 反转 i + 1 之后的所有元素,使其变为升序
let left = i + 1;
let right = n - 1;
while (left < right) {
[nums[left], nums[right]] = [nums[right], nums[left]];
left++;
right--;
}
};运行演示
假设 nums = [1, 2, 3]
n = 3,i = 1。nums[1] (2) < nums[2] (3),循环不执行,i停在1。i >= 0,进入if块。j = 2。nums[2] (3) > nums[1] (2),循环不执行,j停在2。- 交换
nums[1]和nums[2]->nums变为[1, 3, 2]。 - 反转
i + 1(即2) 之后的元素:left = 2,right = 2。left < right不成立,循环结束。
- 结果:
[1, 3, 2]。
假设 nums = [3, 2, 1]
i = 1。nums[1] (2) >= nums[2] (1),i--->i = 0。nums[0] (3) >= nums[1] (2),i--->i = -1。i < 0,不执行if块。- 反转
0 + 1(即0) 之后的元素(整个数组):left = 0,right = 2。交换3和1->[1, 2, 3]。left++,right--->left = 1,right = 1。- 结束。
- 结果:
[1, 2, 3]。
复杂度分析
- 时间复杂度:$O(n)$,其中
n是数组的长度。最坏情况下需要扫描数组两次(一次找i,一次找j)并反转一次。 - 空间复杂度:$O(1)$,只使用了常数个额外空间。
