Hot 100 --- 下一个排列

本文概览:本文讲解下一个排列:要让排列"只变大一点点",就得找从右往左第一个升序相邻对(它右边的降序段已经顶到头了),用降序段里比它大的最小数替换,再把右边那段反转成升序,让它变成新前缀下的最小排列


一、题目


二、题目分析

1. 题目要求

整数数组的一个排列 就是将其所有成员以序列或线性顺序排列。例如 arr = [1, 2, 3],以下这些都可以视作它的排列:[1,2,3]、[1,3,2]、[3,1,2]、[2,3,1] 等。

整数数组的下一个排列 是指其整数的下一个字典序更大的排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即元素按升序排列)。

必须原地修改,只允许使用额外常数空间。

示例 1 :nums = [1, 2, 3] → [1, 3, 2]

示例 2 :nums = [3, 2, 1] → [1, 2, 3]

示例 3 :nums = [1, 1, 5] → [1, 5, 1]

2. 怎么想这题?

先搞清楚"排列的大小"怎么比。 字典序是从左往右逐位比,第一个不一样的位置谁大,谁就更大。比如 [1,2,3] 和 [1,3,2]:第一位都是 1,第二位 2 < 3,所以 [1,3,2] 更大。

再想"下一个"到底意味着什么。 把 [1,2,3] 的所有排列按字典序排成一列:

复制代码
123, 132, 213, 231, 312, 321

123 的下一个就是 132。注意它并不是"随便找一个更大的"------比 123 大的排列有好几个,但排在 123 后面紧挨着 的只有 132。所以"下一个排列"要同时满足两件事:得比当前这个大,而且要大得尽可能少。

那怎么才能"只升一级"? 从字典序的比较规则往下推:

  • 两个排列谁大,取决于最靠左的那个不同位 。所以想让 [1,2,3] 往前走一小步,改动的位置越靠右越好------改个位只挪一小步,改首位就直接跳到另一大批排列去了。要改,就改最靠右的那个"能改的位置"。
  • 改完之后,剩下那几位还得排成它们最小的顺序。不然中间会隔着别的排列,得到的就不是"下一个",而是"下几个"。

哪一位才是"能改的位置"?从右往左看后缀。 从最右边开始往左扫,如果发现某一段是严格降序 的(一路递减),那这一段本身就已经是"这几个数能组成的最大排列"了------降序是最大的顺序,在它内部无论怎么重排都只会变小,靠它没法让整个排列变大。

那就继续往左退,直到第一次出现 nums[i] < nums[i+1]:这一位比它右边那位小,说明"这一段还没顶到头",把它换大一点,整个排列就能变大。而且因为是从右往左遇到的第一个,它就是那个"能改的最靠右的位置"------动它,新排列离当前最近。

所以这个位置(降序段左边第一个升序相邻对)就是"能升一级"的入口。

换成谁、换完还要做什么? 这两步决定了答案到底是"下一个"还是"下几个":换进来的数得比 nums[i] 大,否则排列没变大;但又要"大得尽量少",挑得太大就等于跳级。换完之后,i 后面那几位还是降序(也就是它们能组成的最大排列),而我们要的既然是"下一个",后面就应该取最小的排列。

3. 需要解决哪几个问题?

问题一:为什么"能改的位置"一定是从右往左第一个升序相邻对?比它更靠右的位置为什么动不了?

问题二:换成哪一个数,才算"只升一级"而不跳级?这个数藏在哪,怎么快速找到它?

问题三 :交换完就结束了吗?i 后面那一段该怎么处理,才能让它成为最小排列?

问题四 :如果从头扫到尾都没找到这个位置(比如 [3,2,1]),说明什么?该怎么收尾?


三、思路概览

java 复制代码
public void nextPermutation(int[] nums) {
    int n = nums.length;
    int i = n - 2;
    // 从右往左,找到第一个升序相邻对 nums[i] < nums[i+1]
    while (i >= 0 && nums[i] >= nums[i + 1]) {
        i--;
    }
    // 找到了就和右边"比它大的最小数"交换
    if (i >= 0) {
        int j = n - 1;
        while (j >= 0 && nums[j] <= nums[i]) {
            j--;
        }
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
    // 把 i 后面的降序段反转成升序(i = -1 时正好反转整个数组)
    int left = i + 1;
    int right = n - 1;
    while (left < right) {
        int temp = nums[left];
        nums[left] = nums[right];
        nums[right] = temp;
        left++;
        right--;
    }
}

思路简要说明:

  1. 找交界点 i :从右往左扫,第一个满足 nums[i] < nums[i+1] 的位置
  2. 找替换对象 j :在 i 右边从右往左扫,第一个比 nums[i] 大的位置
  3. 交换 nums[i] 和 nums[j]
  4. 反转 i+1 到末尾,把降序变升序
  5. 时间复杂度 O(n),空间 O(1)

四、思路详解

先定标准:一次交换要满足什么,才算"升一级"而不是"跳级"?

题目要的是字典序里紧挨着的下一个,所以改动必须同时满足两条:

  • 改的位置尽量靠右。两个排列谁大,由最靠左的不同位决定;改得越靠右,新排列和原排列挨得越近。
  • 改完之后,后面那几位排成最小顺序。否则新排列后面还会跟着一串比它小的排列,中间隔着东西,那就不是"紧接着的下一个"了。

把这两条套到具体操作上,就得到三个动作:先找到最靠右的那个"能改的位置",再把这一位换成"刚好大一点"的数,最后把后面那一段收拾成最小的顺序。下面逐个说明。

第一步:解决问题一------哪个位置才能改?

从右往左扫相邻两格 nums[k] 和 nums[k+1]:

  • 只要 nums[k] >= nums[k+1],就说明从 k 往右这一段是降序 的。降序段已经顶到了它自己的最大排列,在它内部重新排列只会变小,不可能让整个数组变大,所以继续往左找。
  • 一直往左,直到第一次遇到 nums[i] < nums[i+1]。

这个 i 就是能变大的最靠右的位置,它把数组切成了两半:

复制代码
[ ... 有潜力变大的部分 ... | i ] [ i+1 ... n-1 已经顶到头的降序段 ]

注意是"从右往左第一个",这正好同时满足了上面那两条标准里的第一条:比 i 更靠右的位置全在降序段里,动它们只会让排列变小,压根不是"能改的位置";而如果放着 i 不改编去动更靠左的位,改动幅度就大了,那就不是"下一个"而是"下很多个"了。比如 [1,2,3,5,4],从右往左第一个满足升序的是 3 < 5(下标 2),改动这里就够了;要是去动前面的 2,得到的就是 [1,3,...] 开头,早就跳过了一大批排列。

第二步:解决问题二------和谁交换?

现在要把 nums[i] 换成一个比它大的数 ,让排列变大。同时要"变大得最少",所以还得满足:换进来的这个数,得是比 nums[i] 大的那些数里最小的一个。

那么去哪找?只能去 i 右边那段里找。左边那部分不能动(动了位置更靠左、变化更大),而且 i 左边是"降序段的终点"之上的部分,它右边的才是有序可查的降序段。

i 右边 nums[i+1 .. n-1] 是降序 的。在降序序列里从右往左扫,遇到的第一个大于 nums[i] 的数,就是"比它大的最小的那个"------因为越往左越大,从右往左数的第一个就是最小的大于它的数。

举个直观的例子:[1, 2, 3, 5, 4] 里 nums[i] = 3,右边降序段是 5, 4。比 3 大的有 5 和 4,4 更小,所以换 4:

复制代码
换 5 → [1, 2, 5, 3, 4]    ← 跳得太远了,把"4 开头"的排列全跳过了
换 4 → [1, 2, 4, 5, 3]    ← 只大一点点,这才是"下一个"

所以挑 4 不挑 5:换 5 相当于一步跨过了中间一大批排列。

第三步:解决问题三------交换完为什么还要反转?

交换之后,nums[i] 变成了一个更大的值(3 变成 4),前缀 [1, 2, 4] 从此定下来。但接下来的目标是:在所有以 [1, 2, 4] 开头的排列里,取最小的那一个。

可刚刚换完的数组是 [1, 2, 4, 5, 3],它后面那一段 5, 3 仍然是降序 的。降序意味着最大 ------也就是说 [1,2,4,5,3] 是"124 开头"里最大的那个排列,而我们要的恰恰是"124 开头"里最小的那个。

所以要把 i 后面这一段从降序反转成升序。降序 → 升序,正好是从最大变成最小:

复制代码
[1, 2, 4, 5, 3]   反转后面两位   [1, 2, 4, 3, 5]
       ~~~~~    →                ~~~~~
       降序(最大)                  升序(最小)

这样得到的 [1, 2, 4, 3, 5] 才是"124 开头"里最小的排列,也就是真正的"下一个"。

为什么反转(O(n))就够了,不用排序? 因为换完之后,i 右边这一段本来就已经是降序的(降序序列里抽掉一个元素,剩下部分仍然降序),而降序反转过来就是升序------升序即最小。所以一次反转到位,不需要付出排序的代价。

第四步:解决问题四------整个数组降序怎么办?

如果第一步一路从右扫到最左边都没找到 nums[i] < nums[i+1](循环结束后 i = -1),说明整个数组都是降序 的,比如 [3, 2, 1]。它已经是最大的排列,按题目要求"不存在下一个更大的排列时,重排为字典序最小的排列",也就是要变成升序 [1, 2, 3]。

处理办法很简单:跳过交换那一步,直接反转整个数组。代码里 left = i + 1 = 0,反转范围正好是 0 到 n-1 的整个数组------同一个反转逻辑,顺手把特例也覆盖了,不用单独写分支。


五、完整执行过程

示例 1 的加强版:nums = [1, 2, 3, 5, 4] → [1, 2, 4, 3, 5]

复制代码
初始:  [1, 2, 3, 5, 4]

① 从右往左找第一个升序相邻对:
   i=3: nums[3]=5 >= nums[4]=4   → i=2
   i=2: nums[2]=3 <  nums[3]=5   → 停下,i = 2

② 从右往左找第一个比 nums[2]=3 大的:
   j=4: nums[4]=4 <= 3 ? 否 → 停下,j = 4

③ 交换 nums[2] 和 nums[4]:
   [1, 2, 4, 5, 3]

④ 反转 i+1=3 到 4:
   [1, 2, 4, 3, 5]

结果: [1, 2, 4, 3, 5] ✓

示例 2:nums = [3, 2, 1] → [1, 2, 3](整体降序)

复制代码
初始:  [3, 2, 1]

① 从右往左找升序相邻对:
   i=1: nums[1]=2 >= nums[2]=1 → i=0
   i=0: nums[0]=3 >= nums[1]=2 → i=-1
   i < 0,说明整体降序,跳过交换

② 反转 i+1=0 到 2(整个数组):
   [1, 2, 3]

结果: [1, 2, 3] ✓

示例 3:nums = [1, 1, 5] → [1, 5, 1](有重复元素)

复制代码
初始:  [1, 1, 5]

① i=1: nums[1]=1 >= nums[2]=5 ? 否 → 停下,i = 1
② j=2: nums[2]=5 <= nums[1]=1 ? 否 → 停下,j = 2
③ 交换 → [1, 5, 1]
④ 反转 i+1=2 到 2(只有一个元素,不用动)

结果: [1, 5, 1] ✓

六、代码细节

nums[i] >= nums[i+1] 里为什么用 >=(而不是 >)?

这决定了重复元素下的正确性。[1, 1, 5] 里 nums[1] 和 nums[2] 不相等还好说,但像 [1, 1] 这种,如果用 >,第一步就会在 nums[0] = nums[1] 那里停下、把 i 定在 0;可 [1, 1] 本身并没有下一个更大的排列。用 >= 才会继续往左扫,最后正确地判定为"整体降序",反转回 [1, 1]。相等也算降序,因为交换两个相等的数并不会让排列变大。

nums[j] <= nums[i] 里为什么用 <=?

要的是严格大于 nums[i] 的数,所以扫描时跳过所有"小于等于"的。

i < 0 时为什么不会出错?

交换那一段被 if (i >= 0) 兜住了;反转那一段 left = i + 1 = 0,正好反转整个数组,刚好就是"整体降序要变成升序"的答案。

为什么不用直接排序?

排序要 O(n log n),而且题目要求原地、常数空间。这里用"找 i → 找 j → 交换 → 反转"四步,每步都是线性的,整体 O(n),比排序快,也更贴合排列本身的结构。


七、复杂度分析

时间复杂度 O(n) :三次线性扫描------找 i 一次、找 j 一次、反转一次,都是往左或往右单方向推进。

空间复杂度 O(1):只用几个下标变量,交换和反转都在原数组上进行。


八、总结

这道题看起来绕,其实整套推导就建立在两个观察上:

观察一:降序段已经"顶到头"了。

一个降序的片段,它自己就是那一堆元素能组成的最大排列,在它内部怎么折腾都只会变小。所以真正能"变大"的位置,只能往左找,直到遇到第一个 nums[i] < nums[i+1] 的升序相邻对------这个交界点就是可改动的最靠右位置。

观察二:要"下一个",就得变大幅度最小。

  • 位置最小:i 选从右往左第一个能变大的位置;
  • 幅度最小:在右边降序段里挑"比 nums[i] 大的最小数"来换(降序段从右往左第一个大于它的就是它);
  • 收尾最小:换完后 i 后面还是降序(最大),反转成升序(最小),让它成为新前缀下的最小排列。

整段流程可以浓缩成一句话:从右往左找交界的 i,在它右边找刚好大一点的那个数换上去,再把后面反转成升序。 如果连 i 都找不到,说明本来就是最大排列,整个反转回去当最小排列即可。

相关推荐
Thinker QAQ1 小时前
并发编程(六):Atomic 的实现——从 Runtime 到 CPU
java·go·cas·并发编程·atomic·cpython
垆边人似月.1 小时前
字符串重排后最大字典序
算法·leetcode·职场和发展
码流子1 小时前
AI稽核精灵-Agent落地
大数据·人工智能·物联网·算法·系统架构
沐欣工作室_lvyiyi1 小时前
基于SpringBoot的博物馆预约系统设计与实现(论文+源码)
java·spring boot·后端·计算机毕业设计
微三云生态系统架构师-彭丹1 小时前
排队免单资金池专户存管与返本算法:队列模型与熔断保护
算法·熔断机制·排队免单·消费增值·资金池·队列系统·返本算法
wzdark1 小时前
矩阵快速幂算法在图路径计算中的应用4
线性代数·算法·矩阵
Dylan的码园1 小时前
A+B问题 进制处理【逻辑控制 运算符】
c++·算法
空奈qwq1 小时前
ANN 全连接神经网络入门指南:从神经元到深度学习的第一步
人工智能·python·深度学习·神经网络·算法·机器学习