下一个排列 图解题解
从右往左找下降点、换最小更大值、翻转尾部——三步原地得到字典序的下一个排列。
想让一个六位数变成紧接着大一点的那个:从个位往左找第一个「比右邻小」的数位(下降点),再从最右边找一个比它大的最小数字和它对调,最后把下降点右边的部分翻转成升序——改动幅度最小,结果比原数大一级。
这道题到底在问什么
- 输入
- nums = [1,2,3]
- 输出
- [1,3,2]
- 输入
- nums = [3,2,1]
- 输出
- [1,2,3]
- 输入
- nums = [1,1,5]
- 输出
- [1,5,1]
最优解:为什么这么做
一句话答案:LeetCode 31 下一个排列的标准解是三步法:从右往左找到第一个满足 nums[i] < nums[i+1] 的位置,在它右侧的降序后缀里从右往左挑刚好比它大的数交换,再把后缀反转成升序,就得到字典序上刚好更大的排列;若数组已完全降序则整体反转成最小排列。原地交换,时间 O(n)、空间 O(1)。
这道题真正在问什么
把数组原地重排成字典序里紧挨着它、刚好更大的那个排列,比如 [1,2,3] 的下一个是 [1,3,2];如果它已经是这些数字的最大排列(完全降序,如 [3,2,1]),就回绕成最小排列(升序)。「刚好更大」四个字是全部难点:让排列变大很容易,让它变大得最少,才需要把排列的结构想清楚。
为什么从右往左找第一个升序对
想让一个数变大又涨幅最小,改动的位置应该尽量靠右——就像给数字加一要先动个位。从最右端往左看,只要相邻两数还是降序的,这一段就已经是这些数字能排出的最大值,在这段内部怎么折腾都变不大。第一个出现 nums[i] < nums[i+1] 的位置,就是从右数第一个「还有抬高余地」的位:它右边整段降序、已经到顶,所以下一个排列必须在这一位动手,且它左边的全部保持不动——若去动更左的位,跨过的排列就不止一个,得到的不再是「下一个」。
交换的对象为什么是刚好大一点的数
既然要抬高这一位,就得从它右边的后缀里挑一个比它大的数换上来;而为了整体「刚好更大」,要挑比它大的数里最小的那个。后缀恰好是降序的,所以从最右端往左遇到的第一个大于 nums[i] 的数就是目标,把两者交换即可。有重复元素时要注意判断的边界:查找时遇到小于等于 nums[i] 的都继续往左,保证不会选中相等的数——换一个相等的数等于没变大,排列原地踏步。
为什么交换之后还必须反转后缀
交换只完成了「抬高变动位」这半件事,此刻后缀仍是降序——换上去和换下来的两个数在大小上正好卡在原位的邻居之间,插回去不破坏降序——也就是说后缀还停在它的最大排列上。而我们要的是所有比原排列大的排列中最小的那个:变动位左边不动、变动位已定,后缀就应该取最小排列,即升序。后缀既然已是降序,用头尾双指针原地反转一遍就变成升序,这一步用 O(n) 的反转替代了排序,空间也保持 O(1)。
复杂度与三个常见翻车点
三个步骤各是一趟线性扫描或反转,时间 O(n);全程只用几个下标原地交换,空间 O(1)。最常见的错:一是交换后忘了反转后缀,得到的排列比原来大得多、不是「下一个」;二是整个数组已完全降序、找不到升序对时,忘了把整体反转成升序而直接返回原数组;三是有重复元素时把第一步的判断写成严格大于就停,在相等处误停会选错位置——相等也要继续往左找。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3记住这条「从右找第一个变小的位 i、和它右边刚好比它大的数换、再把 i 后面升序排(反转)」,下面每帧都在套它。
- 4第①步从右往左检查相邻对:下标 7 的值 3 不小于右邻下标 8 的 2(还是降的)。降序说明这一段已经排到最大,没法在这里变大,继续往左找。
- 5第①步从右往左检查相邻对:下标 6 的值 5 不小于右邻下标 7 的 3(还是降的)。降序说明这一段已经排到最大,没法在这里变大,继续往左找。
- 6第①步从右往左检查相邻对:下标 5 的值 6 不小于右邻下标 6 的 5(还是降的)。降序说明这一段已经排到最大,没法在这里变大,继续往左找。
- 7第①步从右往左检查相邻对:下标 4 的值 7 不小于右邻下标 5 的 6(还是降的)。降序说明这一段已经排到最大,没法在这里变大,继续往左找。
- 8第①步从右往左检查相邻对:下标 3 的值 8 不小于右邻下标 4 的 7(还是降的)。降序说明这一段已经排到最大,没法在这里变大,继续往左找。
- 9第①步从右往左检查相邻对:下标 2 的值 9 不小于右邻下标 3 的 8(还是降的)。降序说明这一段已经排到最大,没法在这里变大,继续往左找。
- 10第①步从右往左检查相邻对,走到下标 1 与 2:nums[1]=4 比右边的 9 小,这是从右数第一个「升序对」!它的左数下标 i=1 就是我们要变大的那一位。停止扫描。
- 11锁定 i=1(值 4)。它右边下标 2 到 8(绿色)是一整段降序后缀,已经是这些数能排出的最大值。所以要让整体变大,只能抬高 i 这一位。
- 12第②步从最右往左找比 nums[i]=4 大的数:下标 8 的 2 还不够大(≤ nums[i]),标灰排除、继续往左。
- 13第②步从最右往左找比 nums[i]=4 大的数:下标 7 的 3 还不够大(≤ nums[i]),标灰排除、继续往左。
- 14第②步又从最右往左找:下标 6 的值 5 大于 nums[i]=4,这就是后缀里「刚好比 nums[i] 大」的数(因为后缀降序,从右数第一个超过它的,增幅最小)。锁定 j=6,停止。
- 15锁定 j=6(值 5),它是后缀里刚好比 nums[i]=4 大一点的数。准备把下标 i=1(值 4)与下标 j=6(值 5)交换,让最高变动位抬高得尽量少。
- 16交换完成:下标 1 变成 5,下标 6 变成 4(两个绿格)。最高变动位已经比原来大了,但右边的后缀此刻还是降序、偏大,接下来要把它压到最小。
- 17第③步:交换后,下标 2 到 8 这段后缀(绿色)仍然是降序、是它能排的最大。我们要的是「刚好大一点」,所以把这段反转成升序,让后缀变成最小,增幅才最小。
- 18反转用双指针从两端向中间:交换下标 2(值 9)和下标 8(值 2)。
- 19换好了:下标 2 现在是 2,下标 8 现在是 9。两个指针各向中间收一格,继续反转剩下的部分。
- 20反转用双指针从两端向中间:交换下标 3(值 8)和下标 7(值 3)。
- 21换好了:下标 3 现在是 3,下标 7 现在是 8。两个指针各向中间收一格,继续反转剩下的部分。
- 22反转用双指针从两端向中间:交换下标 4(值 7)和下标 6(值 4)。
- 23换好了:下标 4 现在是 4,下标 6 现在是 7。两个指针各向中间收一格,继续反转剩下的部分。
- 24后缀已经从降序翻成升序、取到最小。整个数组就是字典序里紧挨着原来、刚好比它大的下一个排列:[1, 5, 2, 3, 4, 6, 7, 8, 9]。三步从右扫 + 一次反转,O(n) 时间、O(1) 额外空间。
⚠️ 容易写错的地方
✗ 错:第②步交换后忘了反转后缀
✓ 对:交换后必须反转 i+1..末尾
交换只抬高了一位,后缀仍是降序(偏大),不反转得到的不是「下一个」而是大得多的排列
✗ 错:找不到 i 时直接返回原数组
✓ 对:整体反转成升序
完全降序说明已是最大排列,题目要求此时回到最小排列(升序)
✗ 错:第①步用 > 而非 >=(有重复元素时)
✓ 对:nums[i] >= nums[i+1] 才跳过
相等也属于「非升序」,要继续往左;用 > 会在相等处误停
完整代码(Python / C++ / Java)
Python
def nextPermutation(nums):
n = len(nums)
i = n - 2
while i >= 0 and nums[i] >= nums[i+1]: # ① 从右找第一个变小的位 i
i -= 1
if i >= 0:
j = n - 1
while nums[j] <= nums[i]: # ② 从右找刚好比 nums[i] 大的 j
j -= 1
nums[i], nums[j] = nums[j], nums[i] # 交换
lo, hi = i + 1, n - 1 # ③ 反转 i 后面的后缀
while lo < hi:
nums[lo], nums[hi] = nums[hi], nums[lo]
lo += 1; hi -= 1C++
void nextPermutation(vector<int>& nums){
int n = nums.size(), i = n - 2;
while(i >= 0 && nums[i] >= nums[i+1]) i--; // ① 找变小位 i
if(i >= 0){
int j = n - 1;
while(nums[j] <= nums[i]) j--; // ② 找刚好更大的 j
swap(nums[i], nums[j]); // 交换
}
reverse(nums.begin() + i + 1, nums.end()); // ③ 反转后缀
}Java
public void nextPermutation(int[] nums) {
int n = nums.length, i = n - 2;
while (i >= 0 && nums[i] >= nums[i + 1]) i--; // ① 找变小位 i
if (i >= 0) {
int j = n - 1;
while (nums[j] <= nums[i]) j--; // ② 找刚好更大的 j
int t = nums[i]; nums[i] = nums[j]; nums[j] = t; // 交换
}
int lo = i + 1, hi = n - 1; // ③ 反转后缀
while (lo < hi) {
int t = nums[lo]; nums[lo] = nums[hi]; nums[hi] = t;
lo++; hi--;
}
}复杂度
时间
O(n)
最坏扫两遍 + 反转一遍,都是线性
空间
O(1)
只用 i/j/lo/hi 几个下标,原地交换
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 下一个排列 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
不用三步法,最朴素的解法是什么?为什么不可取?+
枚举出所有排列、按字典序排序、找到当前排列的下一个。逻辑直观但要 O(n!) 时间和空间,n 稍大就爆炸,面试不可接受。三步法用「下一个」的结构性质,O(n) 一趟搞定。
为什么第①步停在「第一个升序对」就一定对?+
从右往左第一个 nums[i] < nums[i+1] 的 i,保证 i 右边整段是降序(已是最大)。固定 i 左边不动、只在 i 及其右边做最小改动,才能得到「紧挨着的下一个」。若动更左的位,跨度会比这个大,就不是「下一个」了。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 下一个排列 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。