📌 题目链接:31. 下一个排列 - 力扣(LeetCode)
🔍 难度:中等 | 🏷️ 标签:数组、双指针、原地操作、字典序
⏱️ 目标时间复杂度:O(n)
💾 空间复杂度:O(1)
🧠 题目分析
给定一个整数数组 nums,要求 原地 修改为它的 字典序下一个更大的排列。若当前已是最大排列(即严格降序),则返回最小排列(升序)。
📘 什么是“字典序”?
字典序(Lexicographical Order)类似于英文单词在字典中的排序方式:
- 比较两个序列从左到右第一个不同的元素;
- 若
a[i] < b[i],则a的字典序小于b。
例如:
[1,2,3] < [1,3,2] < [2,1,3] < ... < [3,2,1]
🎯 关键约束
- 必须原地修改(in-place);
- 只允许 O(1) 额外空间;
- 不能生成所有排列再找下一个(会超时且违反空间限制)。
这是一道经典的 组合数学 + 双指针 题,也是 C++ STL 中 std::next_permutation 的底层实现逻辑。
🔑 核心算法及代码讲解
本题的核心是 “字典序下一个排列”的构造算法,由 Narayana Pandita 在 14 世纪提出,现代广泛用于排列生成。
✅ 算法思想:
要让新排列 刚好比当前大一点,需做到三点:
- 找到最右边的“可提升”位置(即存在右侧更大值的位置);
- 用右侧最小的“更大值”替换它;
- 将右侧部分变为最小字典序(升序)。
📐 算法步骤详解(以 [4,5,2,6,3,1] 为例)
-
从右往左找第一个
nums[i] < nums[i+1]
→ 找到i = 2(nums[2]=2 < nums[3]=6)
→ 此时[i+1, n)是 严格非递增(降序),无法通过内部调整变大。 -
在
[i+1, n)中从右往左找第一个nums[j] > nums[i]
→ 找到j = 4(nums[4]=3 > 2,且是最小的满足条件的值)
→ 交换nums[i]和nums[j]→[4,5,3,6,2,1] -
反转
[i+1, n)使其升序(最小字典序)
→ 反转[6,2,1]→[1,2,6]
→ 最终结果:[4,5,3,1,2,6]
💡 为什么反转即可?
因为[i+1, n)原本是降序,交换后仍保持降序(因为nums[j]是刚好大于nums[i]的最小值),所以直接反转 = 升序 = 最小排列。
🧪 边界情况处理
- 若整个数组是降序(如
[3,2,1]),则i = -1,直接反转整个数组 →[1,2,3]。
💻 C++ 核心代码(带行注释)
void nextPermutation(vector<int>& nums) {
int i = nums.size() - 2;
// Step 1: 从右往左找第一个 nums[i] < nums[i+1]
while (i >= 0 && nums[i] >= nums[i + 1]) {
i--;
}
if (i >= 0) {
// Step 2: 从右往左找第一个 nums[j] > nums[i]
int j = nums.size() - 1;
while (j >= 0 && nums[i] >= nums[j]) {
j--;
}
// 交换 nums[i] 和 nums[j]
swap(nums[i], nums[j]);
}
// Step 3: 反转 [i+1, end) 使其升序
reverse(nums.begin() + i + 1, nums.end());
}
✅ 面试高频考点:
- 能否手写
next_permutation?- 为何不用排序而用反转?
- 如何处理重复元素?(本算法天然支持!因为比较用
>=和>=,跳过相等情况)
🧩 解题思路(分步拆解)
-
定位“转折点”
从末尾开始,找到第一个破坏“非递增”趋势的位置i。这是唯一能通过交换使排列变大的位置。 -
寻找最优替换值
在i右侧(已知为降序),从右向左找第一个大于nums[i]的值,确保替换后增量最小。 -
重置后缀为最小状态
交换后,i右侧仍为降序,要使其字典序最小,只需反转成升序。
🌟 关键洞察:
字典序的“下一个” = 局部最小改动 + 后缀最小化。
📊 算法分析
| 项目 | 分析 |
|---|---|
| 时间复杂度 | O(n) —— 最多两次遍历 + 一次反转 |
| 空间复杂度 | O(1) —— 仅用几个指针变量 |
| 稳定性 | 支持重复元素(如 [1,1,5] → [1,5,1]) |
| 原地性 | 完全满足,无额外数组 |
| 面试价值 | ⭐⭐⭐⭐⭐ —— 经典算法,考察思维严谨性与边界处理 |
💬 面试官可能追问:
- 如果要实现
prev_permutation(上一个排列)怎么做?- 能否用此算法生成所有排列?
- 为什么不能用
sort(nums.begin()+i+1, nums.end())?
→ 虽然正确,但sort是 O(k log k),而reverse是 O(k),更优!
💻 完整代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
class Solution {
public:
void nextPermutation(vector<int>& nums) {
int i = nums.size() - 2;
while (i >= 0 && nums[i] >= nums[i + 1]) {
i--;
}
if (i >= 0) {
int j = nums.size() - 1;
while (j >= 0 && nums[i] >= nums[j]) {
j--;
}
swap(nums[i], nums[j]);
}
reverse(nums.begin() + i + 1, nums.end());
}
};
// 测试
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
Solution sol;
// 测试用例 1
vector<int> nums1 = {1,2,3};
sol.nextPermutation(nums1);
for (int x : nums1) cout << x << " "; // 输出: 1 3 2
cout << "\n";
// 测试用例 2
vector<int> nums2 = {3,2,1};
sol.nextPermutation(nums2);
for (int x : nums2) cout << x << " "; // 输出: 1 2 3
cout << "\n";
// 测试用例 3
vector<int> nums3 = {1,1,5};
sol.nextPermutation(nums3);
for (int x : nums3) cout << x << " "; // 输出: 1 5 1
cout << "\n";
return 0;
}
/**
* @param {number[]} nums
* @return {void} Do not return anything, modify nums in-place instead.
*/
var nextPermutation = function(nums) {
let i = nums.length - 2;
// Step 1: find first index i such that nums[i] < nums[i+1]
while (i >= 0 && nums[i] >= nums[i + 1]) {
i--;
}
if (i >= 0) {
// Step 2: find smallest number greater than nums[i] from right
let j = nums.length - 1;
while (j >= 0 && nums[i] >= nums[j]) {
j--;
}
// Swap
[nums[i], nums[j]] = [nums[j], nums[i]];
}
// Step 3: reverse suffix to make it smallest lex order
let left = i + 1;
let right = nums.length - 1;
while (left < right) {
[nums[left], nums[right]] = [nums[right], nums[left]];
left++;
right--;
}
};
// Test cases
console.log("Test 1:");
let arr1 = [1,2,3];
nextPermutation(arr1);
console.log(arr1); // [1,3,2]
console.log("Test 2:");
let arr2 = [3,2,1];
nextPermutation(arr2);
console.log(arr2); // [1,2,3]
console.log("Test 3:");
let arr3 = [1,1,5];
nextPermutation(arr3);
console.log(arr3); // [1,5,1]
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!