Appearance
下一个排列
更新: 7/4/2026 字数: 0 字 时长: 0 分钟
题目描述
给定一个整数数组 nums,找出它按字典序排列的下一个排列。
- 「下一个排列」是指:把数字重新排列后,得到的所有排列中,字典序恰好比当前大一位的那个。
- 如果当前排列已经是最大(数字整体降序),则它没有更大的排列,此时要返回最小排列(整体升序)。
- 必须原地修改,只允许使用常数级额外空间。
例如:
[1,2,3] → [1,3,2];[3,2,1] → [1,2,3];[1,1,5] → [1,5,1]。

思路拆解
先想暴力解法
最直接的想法:生成所有排列,按字典序排好,再找到当前排列的下一个。
点击展开暴力解法(O(n·n!),仅供理解)
js
function nextPermutationBrute(nums) {
// 1) 生成全排列
const perms = []
const used = new Array(nums.length).fill(false)
const path = []
const dfs = () => {
if (path.length === nums.length) { perms.push([...path]); return }
for (let i = 0; i < nums.length; i++) {
if (used[i]) continue
used[i] = true; path.push(nums[i])
dfs()
path.pop(); used[i] = false
}
}
dfs()
// 2) 按字典序排序 + 去重后,定位当前排列的下一个
perms.sort((a, b) => a.join(',') < b.join(',') ? -1 : 1)
const key = nums.join(',')
for (let k = 0; k < perms.length; k++) {
if (perms[k].join(',') === key) {
const next = perms[(k + 1) % perms.length] // 越界则回到最小
for (let t = 0; t < nums.length; t++) nums[t] = next[t]
return nums
}
}
}排列有 个,排序与生成都极其昂贵,只能应付极小规模,无法接受。
推导最优解法:三步法
关键洞察:字典序的大小由从左到右第一个能变大的位置决定。为了让排列只变大一点点,我们要尽量在靠右的位置做改动。
观察一个降序后缀的性质:一段完全降序的序列已经是它自己所有排列中最大的,无法再变大。所以要变大,必须找到降序后缀左边那一位——它才是可以被抬高的“突破口”。
由此得到经典三步法:
- 找低谷:从右向左找到第一个满足 的位置
i(记作 pivot)。它右边是一段降序。若找不到,说明整体降序、已是最大排列。 - 找替换:从右向左找到第一个比 大的元素 ,交换
a[i]与a[j]。因为右侧是降序,从右找到的第一个更大值,正好是“刚好比低谷大的最小值”,交换后整体只增大了最小的幅度。 - 反转后缀:交换后
i+1..n-1仍是降序(最大),把它反转为升序(最小),这样后缀取到最小,整体就成了“恰好大一点”的下一个排列。
核心思路
低谷右侧永远是降序。抬高低谷位(用右侧刚好更大的最小值替换),再把后缀“压”到最小(反转成升序),就得到严格大于当前、且增幅最小的排列。

动画演示
下面用样例 [1, 5, 8, 4, 7, 6, 5, 3, 1] 完整演示三步法。点击「下一步 / 自动播放」观察低谷 i、替换 j、反转区间的变化:
三步法 · 下一个排列演示
样例输入:
[1, 5, 8, 4, 7, 6, 5, 3, 1]当前阶段:准备
10
51
82
43
74
65
56
37
18
低谷 i扫描指针替换 j反转区间
目标:找出字典序恰好比当前排列大的下一个排列。三步法:找低谷 → 找替换 → 反转后缀。
步骤 0 / 13
完整代码
js
function nextPermutation(nums) {
const n = nums.length
// 步骤①找低谷:从右向左找第一个 nums[i] < nums[i+1]
let i = n - 2
while (i >= 0 && nums[i] >= nums[i + 1]) i--
if (i >= 0) {
// 步骤②找替换:从右向左找第一个 > nums[i] 的数
let j = n - 1
while (j > i && nums[j] <= nums[i]) j--
;[nums[i], nums[j]] = [nums[j], nums[i]] // 交换低谷与替换值
}
// 步骤③反转后缀 i+1..n-1(降序 → 升序,取到最小)
let l = i + 1, r = n - 1
while (l < r) {
;[nums[l], nums[r]] = [nums[r], nums[l]]
l++; r--
}
return nums
}易错点
交换/反转这两行前建议加分号 ;,避免 JS 自动分号插入(ASI)把上一行末尾与 [...] = [...] 解构语句连成一体导致解析错误。
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 暴力枚举全排列 | 生成并排序所有排列,不可用 | ||
| 三步法(最优) | 至多三次线性扫描,原地交换与反转 |
三步法的每一步都是从一端到另一端的线性扫描或反转,总操作数与 成正比;除若干指针外不使用额外空间,因此空间为 。
边界与易错点
- 已是最大排列:数组整体降序(如
[3,2,1])时找不到低谷,i = -1。此时步骤③会反转整个数组0..n-1,直接得到最小排列[1,2,3],逻辑天然覆盖,无需特判。 - 反转不能漏:只交换
i与j而忘记反转后缀,会得到一个比目标大的排列(后缀还是降序=偏大),结果错误。 - 步骤②的方向:必须从右向左找第一个大于
nums[i]的数。因为右侧是降序,这样找到的是“刚好更大的最小值”,才能保证增幅最小。 - 相等元素:条件用
nums[i] >= nums[i+1]和nums[j] <= nums[i](含等号),保证对含重复数字的排列同样正确,如[1,1,5] → [1,5,1]。 - 单元素/空数组:
n <= 1时循环不执行,数组保持不变,安全。 - 原地修改:题目要求原地、常数空间,切勿新建数组返回。