LeetCode 31:下一个排列|右找小,右找大,交换,右反转

一、题目

给定一个整数数组 nums,表示一个排列。

要求将它原地修改成:

字典序中紧挨着当前排列的下一个更大排列。

如果已经是最大的排列,则变成最小排列。

例如:

复制代码
[1,2,3]

下一个排列:

复制代码
[1,3,2]

而:

复制代码
[3,2,1]

已经是最大排列,所以变成:

复制代码
[1,2,3]

二、什么叫"下一个排列"?

例如:

复制代码
[1,2,3]

所有排列按从小到大排列:

复制代码
123
132
213
231
312
321

所以:

复制代码
123

的下一个排列就是:

复制代码
132

不是随便找到一个更大的排列,例如:

复制代码
213

虽然也更大,但它不是"紧挨着"的下一个。

因此整道题真正的要求是:

比当前排列大,但增大的幅度要尽可能小。


三、核心思想:尽量从右边修改

假设:

复制代码
1 2 7 4 3 1

如果想得到一个更大的排列,我们当然不希望一上来修改最左边。

因为越靠左的位置,权重越大。

例如:

复制代码
127431

如果把第一位 1 改大:

复制代码
2xxxxx

整体一下子就变大很多。

所以我们的目标是:

尽量保持前面的数字不动,只修改尽可能靠右的位置。

这就是为什么第一步一定:

复制代码
从右往左找

四、最终算法只有三步

整道题可以压缩成:

复制代码
1. 从右往左找第一个 nums[i] < nums[i+1]

2. 从右往左找第一个 nums[j] > nums[i]
   然后交换 nums[i] 和 nums[j]

3. 反转 i+1 到末尾

可以记口诀:

复制代码
找小
找大
交换
反转

五、第一步:为什么找 numsi < numsi+1

代码:

复制代码
int i = nums.length - 2;

while (i >= 0 && nums[i] >= nums[i + 1]) {
    i--;
}

最终希望找到:

复制代码
nums[i] < nums[i+1]

例如:

复制代码
[1,2,7,4,3,1]

从右向左看:

复制代码
3 > 1
4 > 3
7 > 4

这些都在下降。

继续:

复制代码
2 < 7

于是:

复制代码
i = 1
nums[i] = 2

此时:

复制代码
1 2 | 7 4 3 1
  ↑
  i

六、为什么后面的降序这么重要?

后缀:

复制代码
7 4 3 1

已经是降序。

对于这几个数字来说:

复制代码
7431

已经是它们能够组成的最大排列。

换句话说:

只重新排列后面的 7、4、3、1,已经无法得到比当前更大的排列。

所以如果整个数组还要继续变大:

复制代码
必须修改 nums[i]

也就是这里的:

复制代码
2

因此:

复制代码
从右找到第一个 nums[i] < nums[i+1]

实际是在寻找:

最靠右、还能让整个排列变大的位置。


七、为什么不能从左边找?

因为我们的目标不是:

复制代码
找到任意一个更大的排列

而是:

复制代码
找到最接近当前排列的更大排列

所以要:

复制代码
尽可能保持高位不变

因此必须从:

复制代码
右边

开始尝试。


八、第二步:应该把 numsi 换成谁?

现在:

复制代码
1 2 | 7 4 3 1

已经找到:

复制代码
nums[i] = 2

要让排列变大,就必须把这个 2 换成一个:

复制代码
> 2

的数字。

右边:

复制代码
7 4 3 1

比 2 大的有:

复制代码
7
4
3

应该选谁?

如果选:

复制代码
7

变化太大。

如果选:

复制代码
4

还是比必要的大。

最好的就是:

复制代码
3

所以目标是:

找到右侧所有大于 nums[i] 的数字中最小的那个。


九、为什么从右边找第一个大于 numsi 的数?

代码:

复制代码
int j = nums.length - 1;

while (nums[j] <= nums[i]) {
    j--;
}

因为我们已经知道:

复制代码
i 后面的部分是降序

例如:

复制代码
7 4 3 1

从右往左看:

复制代码
1 3 4 7

所以第一个:

复制代码
> 2

的就是:

复制代码
3

而且它正好是:

比 2 大的最小数字。

因此:

复制代码
swap(nums, i, j);

十、交换以后发生什么?

原来:

复制代码
1 2 7 4 3 1

交换:

复制代码
2 和 3

得到:

复制代码
1 3 7 4 2 1

现在前两位:

复制代码
1 3

已经保证:

复制代码
新排列 > 原排列

但是:

复制代码
1 3 7 4 2 1

还不是"下一个排列"。

因为后面的:

复制代码
7 4 2 1

太大了。


十一、第三步:为什么需要把后缀变成最小?

我们已经通过:

复制代码
2 → 3

让整个排列变大。

既然高位已经满足:

复制代码
比原来大

那么后面的数字就应该:

尽可能小。

于是希望:

复制代码
7 4 2 1

变成:

复制代码
1 2 4 7

最终:

复制代码
1 3 1 2 4 7

这才是距离原排列最近的更大排列。


十二、为什么不需要 Arrays.sort()?

理论上,可以把:

复制代码
i+1 ~ n-1

排序成升序。

但我们已经知道:

复制代码
原后缀是降序

交换以后,它仍然保持非递增结构。

因此只需要:

复制代码
反转

就可以得到升序。

例如:

复制代码
7 4 2 1

反转:

复制代码
1 2 4 7

所以:

复制代码
reverse(nums, i + 1, nums.length - 1);

时间复杂度:

复制代码
O(n)

而不用:

复制代码
O(n log n)

的排序。


十三、完整例子:1,2,7,4,3,1

原数组:

复制代码
1 2 7 4 3 1

第一步:找 i

从右:

复制代码
3 >= 1
4 >= 3
7 >= 4

继续:

复制代码
2 < 7

所以:

复制代码
i = 1
nums[i] = 2

第二步:找 j

从右找:

复制代码
> 2

先看:

复制代码
1

不行。

再看:

复制代码
3

可以。

所以:

复制代码
j = 4
nums[j] = 3

交换:

复制代码
1 3 7 4 2 1

第三步:反转后缀

反转:

复制代码
7 4 2 1

得到:

复制代码
1 2 4 7

最终:

复制代码
1 3 1 2 4 7

所以:

复制代码
[1,2,7,4,3,1]

的下一个排列:

复制代码
[1,3,1,2,4,7]

十四、再看最简单的 1,2,3

复制代码
1 2 3

第一步:

复制代码
2 < 3

所以:

复制代码
i = 1

第二步:

复制代码
3 > 2

所以:

复制代码
j = 2

交换:

复制代码
1 3 2

后缀只有一个:

复制代码
2

反转不变。

结果:

复制代码
1 3 2

正确。


十五、再看 1,3,2

原数组:

复制代码
1 3 2

第一步从右:

复制代码
3 >= 2

继续:

复制代码
1 < 3

所以:

复制代码
i = 0
nums[i] = 1

第二步从右找:

复制代码
> 1

最右边:

复制代码
2

满足。

交换:

复制代码
2 3 1

然后反转:

复制代码
3 1

变成:

复制代码
1 3

最终:

复制代码
2 1 3

排列顺序:

复制代码
123
132
213

完全正确。


十六、特殊情况:完全降序

例如:

复制代码
3 2 1

这是当前所有数字能够组成的:

复制代码
最大排列

执行:

复制代码
int i = nums.length - 2;

开始:

复制代码
i = 1

判断:

复制代码
2 >= 1

所以:

复制代码
i = 0

继续:

复制代码
3 >= 2

所以:

复制代码
i = -1

也就是说:

没有找到任何一个 nums[i] < nums[i+1]

这说明整个数组:

复制代码
完全非递增

已经是最大排列。


十七、i = -1 怎么处理?

代码:

复制代码
if (i >= 0) {
    ...
}

因为现在:

复制代码
i = -1

所以交换部分直接跳过。

最后:

复制代码
reverse(nums, i + 1, nums.length - 1);

这里:

复制代码
i + 1 = 0

所以相当于:

复制代码
reverse(nums, 0, nums.length - 1);

反转整个数组:

复制代码
3 2 1
↓
1 2 3

正好得到最小排列。

所以这个模板连特殊情况都自然处理了。


十八、为什么第一步判断是 >=?

代码:

复制代码
while (i >= 0 && nums[i] >= nums[i + 1]) {
    i--;
}

这里不是:

复制代码
nums[i] > nums[i+1]

而是:

复制代码
nums[i] >= nums[i+1]

因为数组可能有重复元素。

例如:

复制代码
1 5 5 4

后缀中的:

复制代码
5 5 4

仍然属于:

复制代码
非递增

所以相等也要继续向左找。

最终我们寻找的是:

复制代码
严格满足 nums[i] < nums[i+1]

的位置。


十九、为什么第二步判断是 <=?

代码:

复制代码
while (nums[j] <= nums[i]) {
    j--;
}

我们的目标是:

复制代码
找到严格大于 nums[i] 的数字

所以:

复制代码
小于
或者
等于

都不能用。

必须跳过:

复制代码
nums[j] <= nums[i]

直到:

复制代码
nums[j] > nums[i]

二十、完整 Java 代码

java 复制代码
class Solution {

    public void nextPermutation(int[] nums) {

        int n = nums.length;

        // 1. 从右找第一个 nums[i] < nums[i+1]
        int i = n - 2;

        while (i >= 0 && nums[i] >= nums[i + 1]) {
            i--;
        }

        // 2. 找到以后,从右找第一个 > nums[i] 的数字
        if (i >= 0) {

            int j = n - 1;

            while (nums[j] <= nums[i]) {
                j--;
            }

            swap(nums, i, j);
        }

        // 3. 后缀反转成最小排列
        reverse(nums, i + 1, n - 1);
    }

    private void swap(int[] nums, int i, int j) {

        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }

    private void reverse(int[] nums, int left, int right) {

        while (left < right) {

            swap(nums, left, right);

            left++;
            right--;
        }
    }
}

二十一、swap 怎么写?

交换数组两个位置:

复制代码
private void swap(int[] nums, int i, int j) {

    int temp = nums[i];

    nums[i] = nums[j];

    nums[j] = temp;
}

就是最标准的:

复制代码
借一个 temp

二十二、reverse 怎么写?

复制代码
private void reverse(int[] nums, int left, int right) {

    while (left < right) {

        swap(nums, left, right);

        left++;

        right--;
    }
}

例如:

复制代码
7 4 2 1
↑     ↑
l     r

交换:

复制代码
1 4 2 7

然后:

复制代码
  ↑ ↑
  l r

交换:

复制代码
1 2 4 7

完成。


二十三、为什么一定是 i+1 开始反转?

因为:

复制代码
0 ~ i

这一部分已经决定了:

复制代码
新排列比原排列更大

尤其是:

复制代码
nums[i]

刚刚已经替换成了更大的数字。

为了让新排列:

复制代码
尽可能小

只能调整:

复制代码
i+1 到末尾

所以:

复制代码
reverse(nums, i + 1, n - 1);

二十四、整道题真正的贪心思想

这道题本质上是一个非常漂亮的贪心。

要求:

复制代码
比当前大
但是尽可能小

所以:

第一层贪心

复制代码
尽可能从右边修改

因此找:

复制代码
最靠右的 nums[i] < nums[i+1]

第二层贪心

既然必须把:

复制代码
nums[i]

变大,那就:

复制代码
只变大一点点

因此找:

复制代码
右侧最小的、严格大于 nums[i] 的数字

第三层贪心

既然高位已经变大,那么低位:

复制代码
尽可能小

所以后缀改成:

复制代码
升序

整个逻辑就是:

复制代码
高位尽量不动
↓
必须变的位置只变大一点
↓
后面的部分尽可能小

二十五、为什么这题不是回溯?

看到:

复制代码
排列

很容易想到前面的:

复制代码
回溯

但这道题并不是要求:

复制代码
生成所有排列

而是已知一个排列:

复制代码
直接找到下一个排列

所以如果先生成所有排列,再排序,再找当前的下一个:

复制代码
复杂度非常高

完全没必要。

这道题通过当前数组的:

复制代码
局部单调性

可以直接在:

复制代码
O(n)

时间里得到答案。


二十六、复杂度分析

第一步:

复制代码
从右找 i

最多扫描:

复制代码
O(n)

第二步:

复制代码
从右找 j

最多:

复制代码
O(n)

第三步:

复制代码
reverse

最多:

复制代码
O(n)

总复杂度:

复制代码
O(n)

不是:

复制代码
O(3n)

因为常数忽略。

所以:

复制代码
时间复杂度:O(n)

只用了:

复制代码
i
j
temp
left
right

几个变量。

所以:

复制代码
空间复杂度:O(1)

二十七、面试怎么讲?

可以这样回答:

下一个排列要求找到比当前排列大的最小排列,因此要尽量保持高位不变。

我先从右往左找到第一个 nums[i] < nums[i+1] 的位置,此时 i 后面的部分已经是非递增序列,说明只调整后缀已经无法得到更大的排列。

然后从右向左找到第一个严格大于 nums[i] 的元素。由于后缀是非递增的,所以这个元素就是能够替换 nums[i] 的最小更大值,然后交换两者。

交换以后,高位已经保证比原排列大。为了让整个新排列尽可能小,再把 i+1 到末尾的后缀反转成升序。

如果一开始没有找到 i,说明原数组完全非递增,已经是最大排列,直接反转整个数组即可。

时间复杂度 O(n),额外空间复杂度 O(1)。


二十八、最容易写错的地方

1. i 从哪里开始?

数组:

复制代码
0 1 2 ... n-2 n-1

需要比较:

复制代码
nums[i]
nums[i + 1]

所以最后一个合法的 i 是:

复制代码
n - 2

因此:

复制代码
int i = n - 2;

2. 第一个 while 写反

正确:

复制代码
while (i >= 0 && nums[i] >= nums[i + 1]) {
    i--;
}

它是在:

跳过右侧的非递增部分。

最后得到:

复制代码
nums[i] < nums[i+1]

3. 第二步要找严格大于

正确:

复制代码
while (nums[j] <= nums[i]) {
    j--;
}

退出时:

复制代码
nums[j] > nums[i]

4. 交换以后不能直接结束

错误:

复制代码
swap(nums, i, j);
return;

还必须:

复制代码
reverse(nums, i + 1, n - 1);

因为要求的是:

复制代码
刚好下一个

而不是:

复制代码
任意更大的排列

5. 完全降序不能漏掉

例如:

复制代码
3 2 1

最终:

复制代码
i = -1

所以一定要把交换放进:

复制代码
if (i >= 0)

但:

复制代码
reverse(...)

必须放在 if 外面。

正确结构:

复制代码
if (i >= 0) {
    ...
}

reverse(nums, i + 1, n - 1);

这样:

复制代码
i=-1

时自动变成:

复制代码
reverse(nums, 0, n - 1);

二十九、面试前 30 秒速记

看到:

复制代码
下一个排列

先想到:

复制代码
比当前大,但尽可能小

口诀:

复制代码
找小
找大
交换
反转

模板:

java 复制代码
int i = nums.length - 2;

while (i >= 0 && nums[i] >= nums[i + 1]) {
    i--;
}

if (i >= 0) {

    int j = nums.length - 1;

    while (nums[j] <= nums[i]) {
        j--;
    }

    swap(nums, i, j);
}

reverse(nums, i + 1, nums.length - 1);

理解:

复制代码
从右找下降点
→ 尽量少动高位

从右找刚好更大的数
→ 当前位只增大一点

反转后缀
→ 后面的数字尽可能小

三十、最终记忆模型

我最终可以把这道题压成这样:

复制代码
例如:

1 2 7 4 3 1

第一步:
找最靠右还能变大的位置

1 [2] 7 4 3 1


第二步:
找右边最小但比 2 大的数字

1 [2] 7 4 [3] 1


第三步:
交换

1 3 7 4 2 1


第四步:
后缀变最小

7 4 2 1
↓
1 2 4 7


答案:

1 3 1 2 4 7

所以这题真正应该记住的不是四五十行解释,而是:

下一个排列 = 尽量从右改变;该位置只变大一点;剩余后缀变得最小。

对应代码就是:

复制代码
从右找 nums[i] < nums[i+1]
↓
从右找 nums[j] > nums[i]
↓
swap
↓
reverse(i+1, n-1)

这就是完整的「下一个排列」模板。

相关推荐
边境悍匪1 小时前
蜗牛学苑 Java 智能体学习 Day40|Vue 工程化与 ElementPlus 思维导图复盘
java·vue.js·学习
霸道流氓气质2 小时前
通义千问 VL & Audio:图像、视频、语音多模态 Java 集成深度解析
java·开发语言·音视频
lemon_sjdk2 小时前
WCPoint、WebPageClient和Accessor类源码
java·安全·webkit·javafx
vx-Biye_Design2 小时前
springboot中国传统节日宣传平台49078-计算机课程设计、毕业设计
java·前端·vue.js·spring boot·后端·课程设计·idea
野生技术架构师2 小时前
从向量检索到混合搜索优化:Spring AI 2.0.1 + pgvector RAG 实战
java·人工智能·spring
彧azz2 小时前
B树原理与C语言实现
c语言·数据结构·b树
蓝速科技2 小时前
酒店门店 AI 数字人前台场景适配与落地指南
大数据·运维·数据结构·数据库·人工智能·科技
橘子汽水1682 小时前
Leetcode 198,118打家劫舍,杨辉三角
算法·leetcode
IT枫斗者枫哥2 小时前
CompletableFuture 超时了,任务还在跑?用三个实验分清超时与取消
java