下一个排列 图解题解
从右往左找下降点、换最小更大值、翻转尾部——三步原地得到字典序的下一个排列。
想让一个六位数变成紧接着大一点的那个:从个位往左找第一个「比右邻小」的数位(下降点),再从最右边找一个比它大的最小数字和它对调,最后把下降点右边的部分翻转成升序——改动幅度最小,结果比原数大一级。
这道题到底在问什么
- nums
- [1,2,7,4,3,1]
- 输出
- [1,3,1,2,4,7]
先想最直接的笨办法
暴力是把所有排列都列出来、排好序、找到当前这个的下一个。n 个数有 n 的阶乘种排列,光是 10 个数就三百多万种,直接超时。(动画第 3 步)
最优解:一步一步想明白
- 3暴力是把所有排列都列出来、排好序、找到当前这个的下一个。n 个数有 n 的阶乘种排列,光是 10 个数就三百多万种,直接超时。
- 4高位不动、只动靠右的位,增幅才最小。所以我们从右往左找:第一个「还能往上抬一点」的位置。
- 5右段 7>4>3>1 一路递减,到 2 才被打破从最右边的 1 开始往左看。1 比 3 小?不,1 在 3 右边、比它小,继续。我们要找的是「左边比右边小」的那个台阶。
- 61 < 3 < 4 < 7:从右看一路是上坡,没法动7、4、3、1 这一段从左到右是递减的,它本身已经是最大排法,动它只会变小。所以越过它,继续往左。
- 72 < 7:终于找到能抬高的位置,i=1走到下标 1,值是 2,它右边的 7 比它大。这就是下降点 i。我们要把这一位的 2 换成右边一个稍大的数。
- 8在 i 右边的后缀里,从右往左找第一个 > 2 的数现在盯住 i 右边这段。从最右的 1 开始往左找:1 比 2 大吗?不大,跳过。
- 91 不行、3 可以:3 是右段里>2 的最小数再往左是 3,比 2 大,停。因为右段是递减的,从右数第一个比 2 大的,正好就是「比 2 大但大得最少」的那个,它就是要换上高位的数。
- 10高位 2 换成 3:前缀刚好抬高了一格交换 i 和 j:2 和 3 互换。注意看下标 1 变成了 3、下标 4 变成了 2。前缀 1,3 已经比原来大,这一步保证了「确实变大」。
- 11换完之后,i 右边 7,4,2,1 依然从大到小排换完后,i 右边的 7,4,2,1 还是从大到小。前缀已经抬高了,尾巴就该取「最小」才整体最接近——但现在它是最大,得把它翻成最小。
- 12l=2、r=5:交换两端 7 和 1用左右双指针反转后缀。l 在 2、r 在 5,交换两端:7 和 1 互换。后缀正从「最大」一步步翻成「最小」。
- 13l=3、r=4:交换 4 和 2,指针相遇即结束l 走到 3、r 走到 4,交换 4 和 2。再走一步两指针就相遇,反转结束。后缀变成 1,2,4,7,是这个前缀下最小的尾巴。
- 14前缀抬一格 + 尾巴取最小 = 刚好大一点点结果出来了:[1,3,1,2,4,7]。高位被尽量靠右地抬高一格、尾巴换成升序最小,这就是字典序里紧挨着的下一个排列。
- 17一句话本质:把尽量靠右的高位抬高最小的一格,再把它后面的尾巴翻成升序最小,整体就刚好大一点点。
- 19整段递减,找不到下降点 i,i 退到 -1[3,2,1] 已经是最大排列。从右往左每一步都是「左大于右」,i 一直左移到越界。这时不交换,直接进第三步反转整段。
- 20i=-1,反转 0 到末尾,回到升序最小因为 i+1 等于 0,第三步反转的是整个数组,[3,2,1] 翻成 [1,2,3],正好是字典序最小的排列。这就是「最大变最小」的兜底分支。
- 24这套「找降点-换大数-翻后缀」是排列类的原子操作。会了它,再去看全排列 lc46 的回溯、以及康托展开求第 k 个排列,会顺很多。点开数组专题或问问小欧 AI 私教继续练。
⚠️ 容易写错的地方
✗ 错:找下降点用 nums[i] > nums[i+1]
✓ 对:要用 nums[i] >= nums[i+1] 才继续左移
遇到相等也得继续往左跳,否则在 [1,5,1] 这类有重复值的数据上会停错位置
✗ 错:找 j 时用 nums[j] < nums[i]
✓ 对:要用 nums[j] <= nums[i] 才继续左移
带等号才能跳过和 nums[i] 相等的数,保证换上来的是严格更大的最小值
✗ 错:i < 0 时就不处理后缀
✓ 对:无论是否找到 i,最后都要反转 i+1 到末尾
整体递减(已是最大)时 i=-1,反转 0 到末尾正好把它变回最小排列,这是题目要求的兜底
完整代码(Python / C++ / Java)
Python
class Solution:
def nextPermutation(self, nums):
n = len(nums)
i = n - 2
# 1. 从右往左找下降点 i
while i >= 0 and nums[i] >= nums[i + 1]:
i -= 1
# 2. 若存在 i,右段找刚好大于 nums[i] 的 j 并交换
if i >= 0:
j = n - 1
while nums[j] <= nums[i]:
j -= 1
nums[i], nums[j] = nums[j], nums[i]
# 3. 反转 i 之后的后缀(递减翻成递增)
l, r = i + 1, n - 1
while l < r:
nums[l], nums[r] = nums[r], nums[l]
l += 1
r -= 1C++
class Solution {
public:
void nextPermutation(vector<int>& nums) {
int n = nums.size(), i = n - 2;
// 1. 从右往左找下降点 i
while (i >= 0 && nums[i] >= nums[i + 1]) i--;
// 2. 右段找刚好大于 nums[i] 的 j 并交换
if (i >= 0) {
int j = n - 1;
while (nums[j] <= nums[i]) j--;
swap(nums[i], nums[j]);
}
// 3. 反转 i 之后的后缀
reverse(nums.begin() + i + 1, nums.end());
}
};Java
class Solution {
public void nextPermutation(int[] nums) {
int n = nums.length, i = n - 2;
// 1. 从右往左找下降点 i
while (i >= 0 && nums[i] >= nums[i + 1]) i--;
// 2. 右段找刚好大于 nums[i] 的 j 并交换
if (i >= 0) {
int j = n - 1;
while (nums[j] <= nums[i]) j--;
int t = nums[i]; nums[i] = nums[j]; nums[j] = t;
}
// 3. 反转 i 之后的后缀
for (int l = i + 1, r = n - 1; l < r; l++, r--) {
int t = nums[l]; nums[l] = nums[r]; nums[r] = t;
}
}
}套路模板 · 排列推进三步骨架(挖空自测)
# 字典序「下一个排列」三步骨架,括号处自己补:
# 第 1 步:从右往左找下降点 i
i = n - 2
while i >= 0 and nums[i] ___ nums[i+1]: # 填 >=
i -= 1
# 第 2 步:i 存在则在右段找最小的更大数 j 交换
if i >= 0:
j = n - 1
while nums[j] ___ nums[i]: # 填 <=
j -= 1
swap(i, j)
# 第 3 步:无论 i 是否存在,都反转 i+1..末尾
reverse(nums, ___ , n - 1) # 起点填 i + 1// 三步骨架:__ 处自己补判据
int i = n - 2;
while (i >= 0 && nums[i] /*__*/ >= nums[i+1]) i--;
if (i >= 0) {
int j = n - 1;
while (nums[j] /*__*/ <= nums[i]) j--;
swap(nums[i], nums[j]);
}
reverse(nums.begin() + /*__*/ i + 1, nums.end());// 三步骨架:__ 处自己补判据
int i = n - 2;
while (i >= 0 && nums[i] /*__*/ >= nums[i+1]) i--;
if (i >= 0) {
int j = n - 1;
while (nums[j] /*__*/ <= nums[i]) j--;
int t = nums[i]; nums[i] = nums[j]; nums[j] = t;
}
for (int l = i + 1, r = n - 1; l < r; l++, r--) {
int t = nums[l]; nums[l] = nums[r]; nums[r] = t;
}复杂度
时间复杂度
O(n)
找 i 最多扫一遍、找 j 最多扫一遍、反转后缀最多扫一遍,三趟都是线性,合起来仍是 O(n)
空间复杂度
O(1)
只用了 i、j、l、r 几个下标变量,原地交换,没有额外数组
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 下一个排列 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么从右往左找下降点,而不是从左往右?+
要让排列「只大一点点」,就得改动尽量靠右的高位。从右往左找到的第一个下降点,正是能抬高且影响最小的那一位。
为什么换上来的 j 一定是「右段里大于 nums[i] 的最小值」?+
右段是递减的,从右往左第一个比 nums[i] 大的数,自然就是其中大于它的最小者,换上去前缀增幅最小。
交换后为什么是反转而不是排序?复杂度差别?+
后缀交换前后都保持递减,反转一次就得到升序最小,O(n);若改用排序则是 O(n log n),没必要。
如果输入已经是最大排列怎么办?+
找不到下降点,i = -1,跳过交换,直接反转整个数组得到升序最小排列,符合题意的「最大变最小」。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。