一、题目描述
整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。
例如,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]
二、解题思路总览
| 方法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 字典序算法 | O(n) | O(1) | 找破坏点 + 替换 + 翻转 |
| 暴力枚举 | O(n! * n) | O(n!) | 生成所有排列找下一个 |
字典序算法是标准解法,一次遍历完成。
三、方法一:字典序算法(推荐)
3.1 核心思想
从一个排列生成下一个更大排列的步骤:
- 找破坏点 :从后往前找第一个升序对
nums[i] < nums[i+1],i是破坏点 - 找替换点 :在
[i+1, n)区间从后往前找第一个大于nums[i]的元素 - 交换 :交换
nums[i]和nums[j] - 翻转 :
[i+1, n)区间变成升序(原来降序,反转即可)
3.2 算法流程图
示例1: nums = [1, 2, 3] -> [1, 3, 2]
+------------------------------------------------------------+
| Step 1: 找破坏点 i(从后往前找第一个升序对) |
+------------------------------------------------------------+
从后往前遍历:
i=1: nums[1]=2 < nums[2]=3 -> 找到!i=1
[1, 2, 3]
↑ i
+------------------------------------------------------------+
| Step 2: 找替换点 j(从后往前找第一个大于 nums[i] 的元素) |
+------------------------------------------------------------+
从后往前遍历:
j=2: nums[2]=3 > nums[1]=2 -> 找到!j=2
[1, 2, 3]
↑ j
+------------------------------------------------------------+
| Step 3: 交换 nums[i] 和 nums[j] |
+------------------------------------------------------------+
交换: nums[1]=2 <-> nums[2]=3
[1, 3, 2]
+------------------------------------------------------------+
| Step 4: 翻转 [i+1, n) 区间 |
+------------------------------------------------------------+
[i+1=2, n=3) 区间只有 [2],反转后仍是 [2]
最终结果: [1, 3, 2]
示例2: nums = [2, 3, 1] -> [3, 1, 2]
+------------------------------------------------------------+
| Step 1: 找破坏点 i |
+------------------------------------------------------------+
从后往前遍历:
i=1: nums[1]=3 > nums[2]=1 -> 下降,不满足
i=0: nums[0]=2 < nums[1]=3 -> 找到!i=0
[2, 3, 1]
↑ i
+------------------------------------------------------------+
| Step 2: 找替换点 j |
+------------------------------------------------------------+
从后往前遍历:
j=2: nums[2]=1 <= nums[0]=2 -> 不满足
j=1: nums[1]=3 > nums[0]=2 -> 找到!j=1
[2, 3, 1]
↑ j
+------------------------------------------------------------+
| Step 3: 交换 nums[i] 和 nums[j] |
+------------------------------------------------------------+
交换: nums[0]=2 <-> nums[1]=3
[3, 2, 1]
+------------------------------------------------------------+
| Step 4: 翻转 [i+1, n) 区间 |
+------------------------------------------------------------+
[i+1=1, n=3) 区间是 [2, 1]
反转后: [1, 2]
最终结果: [3, 1, 2]
示例3: nums = [3, 2, 1] -> [1, 2, 3]
+------------------------------------------------------------+
| Step 1: 找破坏点 i |
+------------------------------------------------------------+
从后往前遍历:
i=1: nums[1]=2 > nums[2]=1 -> 下降,不满足
i=0: nums[0]=3 > nums[1]=2 -> 下降,不满足
没有找到破坏点!
+------------------------------------------------------------+
| Step 2: 整个数组是降序,没有更大的排列 |
+------------------------------------------------------------+
执行 Step 4: 反转整个数组
[3, 2, 1] -> [1, 2, 3]
最终结果: [1, 2, 3]
3.3 完整代码
cpp
class Solution {
public:
void nextPermutation(vector<int>& nums) {
int n = nums.size();
// Step 1: 从后往前找第一个升序对 nums[i] < nums[i+1]
int i = n - 2;
while (i >= 0 && nums[i] >= nums[i + 1]) {
i--;
}
// Step 2: 找替换点并交换
if (i >= 0) {
int j = n - 1;
while (nums[j] <= nums[i]) {
j--;
}
swap(nums[i], nums[j]);
}
// Step 3: 反转 [i+1, n) 区间
reverse(nums.begin() + i + 1, nums.end());
}
};
3.4 代码逐行解析
cpp
int i = n - 2;
while (i >= 0 && nums[i] >= nums[i + 1]) {
i--;
}
- 从后往前遍历,找第一个
nums[i] < nums[i+1]的位置 nums[i] >= nums[i+1]意味着是降序,继续往前找- 如果找不到(
i < 0),说明整个数组是降序,没有更大的排列
cpp
if (i >= 0) {
int j = n - 1;
while (nums[j] <= nums[i]) {
j--;
}
swap(nums[i], nums[j]);
}
- 在
[i+1, n)区间从后往前找第一个大于nums[i]的元素 - 为什么要从后往前?因为要找最小的比
nums[i]大的元素 - 交换这两个元素
cpp
reverse(nums.begin() + i + 1, nums.end());
- 反转
[i+1, n)区间 - 这个区间原来是降序(因为之前是从后往前找的)
- 反转后变成升序,得到最小的排列
3.5 复杂度分析
| 复杂度 | 值 | 说明 |
|---|---|---|
| 时间 | O(n) | 最多遍历三遍:找破坏点、找替换点、反转 |
| 空间 | O(1) | 只用常数个变量 |
四、方法二:暴力枚举(不推荐)
4.1 核心思想
生成数组的所有排列,按字典序排序,找到当前排列的下一个。
4.2 完整代码
cpp
class Solution {
public:
void nextPermutation(vector<int>& nums) {
vector<vector<int>> permutations;
sort(nums.begin(), nums.end());
do {
permutations.push_back(nums);
} while (next_permutation(nums.begin(), nums.end()));
// 找到当前排列的下一个
// ...(省略)
}
};
4.3 复杂度分析
| 复杂度 | 值 | 说明 |
|---|---|---|
| 时间 | O(n! * n) | 生成所有排列 |
| 空间 | O(n!) | 存储所有排列 |
五、方法对比总结
| 方法 | 时间复杂度 | 空间复杂度 | 推荐指数 |
|---|---|---|---|
| 字典序算法 | O(n) | O(1) | ★★★★★ |
| 暴力枚举 | O(n! * n) | O(n!) | ★☆☆☆☆ |
字典序算法的核心思想:
1. 找破坏点:从后往前找第一个升序对
- 破坏点是全局的最高位变化点
- 在它之后的部分是降序的
2. 找替换点:从后往前找第一个大于破坏点元素的值
- 因为是从后往前找,所以找到的是最小的比破坏点大的值
3. 交换 + 翻转:
- 交换后,[破坏点+1, 末尾) 仍然是降序
- 反转后变成升序,得到最小排列
六、面试追问 FAQ
| 问题 | 回答 |
|---|---|
| Q: 为什么从后往前找破坏点? | 从后往前是为了找到最短的变动前缀,保持高位不变,只修改最低位 |
| Q: 为什么从后往前找替换点? | 从后往前找第一个大于 numsi 的,可以保证替换后剩余部分最小 |
| Q: 反转区间为什么是升序? | 因为 [i+1, n) 原来是降序(从后往前找破坏点时保证的),反转后变成最小升序 |
| Q: 如果没有破坏点怎么办? | 说明整个数组是降序,没有更大的排列,直接反转整个数组得到最小排列 |
| Q: 如何求前一个排列? | 反过来:找第一个降序对(numsi > numsi+1),然后交换,再反转 |
| Q: 字典序算法的应用场景? | 字符串的全排列、组合数学、next_permutation 函数实现 |
七、相关题目
| 题目 | 难度 | 关键点 |
|---|---|---|
| 31. 下一个排列 | 中等 | 字典序算法 |
| 46. 全排列 | 中等 | 回溯生成所有排列 |
| 47. 全排列 II | 中等 | 去重全排列 |
| 60. 排列序列 | 中等 | 第 k 个排列 |
| 556. 下一个更大元素 III | 中等 | 字符串版本的下一个排列 |
八、总结
| 要点 | 说明 |
|---|---|
| 核心思想 | 字典序算法:找破坏点 + 找替换点 + 交换 + 翻转 |
| 破坏点 | 从后往前找第一个升序对 nums[i] < nums[i+1] |
| 替换点 | 从后往前找第一个大于 nums[i] 的元素 |
| 翻转 | [i+1, n) 区间反转变成升序 |
| 时间复杂度 | O(n) |
| 空间复杂度 | O(1) |
| 特殊情况 | 没有破坏点时反转整个数组 |