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

二、题目分析
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--;
}
}
思路简要说明:
- 找交界点
i:从右往左扫,第一个满足nums[i] < nums[i+1]的位置 - 找替换对象
j:在i右边从右往左扫,第一个比nums[i]大的位置 - 交换
nums[i]和nums[j] - 反转
i+1到末尾,把降序变升序 - 时间复杂度 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 都找不到,说明本来就是最大排列,整个反转回去当最小排列即可。