这道题乍一看很抽象,但它其实是一个贪心 + 数组操作的经典问题。
真正的难点不在代码,而在于理解:什么才叫“下一个”排列?
本文将从直觉、数学含义到代码实现,一步步拆解这道题。
一、什么是「下一个排列」?
题目要求:
给定一个整数数组 nums,将其重新排列成字典序中下一个更大的排列。
关键点有两个:
- 必须比当前排列大
- 必须是所有更大排列中“最小的那个”
如果当前排列已经是最大的(完全降序),则返回最小排列(完全升序)。
举几个例子:
[1,2,3] → [1,3,2]
[1,3,2] → [2,1,3]
[3,2,1] → [1,2,3]
所以问题本质是:
如何让数组“刚好”变大一点点,而不是跳得太远?
二、核心思想:尽量少改动高位
在排列中:
- 越靠左的数,权重越大
- 越靠右的数,权重越小
想要“刚好变大”,策略一定是:
优先在右侧动手,能不改左边就不改左边
这也是整个算法“从右往左”的根本原因。
三、算法三步走(整体思路)
第一步:从右往左,找到第一个可以“变大”的位置
我们从右向左寻找第一个满足:
nums[i] < nums[i + 1]
代码:
int i = n - 2;
while (i >= 0 && nums[i] >= nums[i + 1]) {
i--;
}
这一步的含义是:
- 跳过所有已经是“降序”的后缀
- 找到最右边那个还有上升空间的位置
如果没找到(i < 0),说明整个数组是降序的,已经是最大排列。
第二步:在右侧找到一个“刚好比它大的数”并交换
如果找到了这个 i:
[ …… nums[i] | 右侧递减序列 ]
右侧一定是降序的。
我们要做的是:
在右侧找到一个 比 nums[i] 大,但尽量小 的数
因为要“刚好变大”。
代码:
int j = n - 1;
while (nums[j] <= nums[i]) {
j--;
}
swap(nums, i, j);
为什么从右往左找?
- 右侧是降序
- 从最右边开始,第一个满足条件的数就是“最小的更大值”
第三步:反转 i 之后的部分
交换之后,右侧依然是降序的,但现在我们要让整体排列尽可能小。
做法很简单:
把 i 右边的部分变成升序
代码:
reverse(nums, i + 1, n - 1);
因为之前是降序,直接反转即可。
四、完整代码实现(Java)
class Solution {
public void nextPermutation(int[] nums) {
int n = nums.length;
int i = n - 2;
// 1. 从右往左,找到第一个 nums[i] < nums[i + 1]
while (i >= 0 && nums[i] >= nums[i + 1]) {
i--;
}
// 2. 如果找到了这样的 i,在右侧找一个刚好比它大的数
if (i >= 0) {
int j = n - 1;
while (nums[j] <= nums[i]) {
j--;
}
swap(nums, i, j);
}
// 3. 反转 i 之后的部分
reverse(nums, i + 1, n - 1);
}
private void swap(int[] nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
private void reverse(int[] nums, int left, int right) {
while (left < right) {
swap(nums, left++, right--);
}
}
}
五、为什么这个算法一定正确?
总结一下逻辑闭环:
- 从右往左找第一个可上升点,保证高位尽量不动
- 在右侧选“最小的更大数”,保证增量最小
- 右侧重排为最小序列,保证整体最小
这正好符合「下一个排列」的定义。
六、复杂度分析
- 时间复杂度:O(n)
- 空间复杂度:O(1)
- 完全原地修改数组,符合题目要求
七、总结一句话版本
下一个排列的本质是:
在最右侧找到一个可以“微调”的位置,让整体刚好变大,然后把后缀压到最小。