文章目录
前言
本专栏文章为《LeetCode 热题 100》的刷题题解,相关内容如有侵权,立即删除。
一、题目
1、原题链接
2、题目描述
二、个人思路整理
1、思路分析
思路关键:
- 尽量在低位(右侧)调整:越靠右改变,增幅越小。
- 用"稍大一点"的较小数替换它,并将后面的数字按升序重排,使增幅最小化。
核心步骤:
- 从后向前寻找第一个相邻升序对
(i, i + 1),满足nums[i] < nums[i + 1]- 此时,区间
[i + 1, n - 1]必定是一个单调不增(降序)的序列,它已经达到了局部字典序的最大值,无法在内部通过重排变得更大。 - 如果遍历到最左侧都没找到这样的
i(即整个数组降序,如[3, 2, 1]),说明已经是全局最大,直接跳到步骤 3 进行全数组反转即可。
- 此时,区间
- 在降序区间
[i + 1, n - 1]中从后向前寻找第一个满足nums[j] > nums[i]的元素- 由于该区间是降序的,从后往前找遇到的第一个大于
nums[i]的元素nums[j],就是该区间里刚好大于nums[i]的最小值。 - 交换
nums[i]和nums[j]。交换后,区间[i + 1, n - 1]依然保持严格的单调不增性质。
- 由于该区间是降序的,从后往前找遇到的第一个大于
- 反转区间
[i + 1, n - 1]- 为了让新生成的排列尽量小,必须让低位数字按升序排列。
- 因为区间
[i + 1, n - 1]本身已经是降序排列,直接使用双指针将其就地翻转,即可在 O ( n ) O(n) O(n) 时间内变更为升序。
以数组
[1, 2, 7, 4, 3, 1]为例:
- 从后往前看,
7 -> 4 -> 3 -> 1是降序,第一个出现nums[i] < nums[i+1]的位置是2 < 7,所以索引i = 1(数值为2)。- 从末尾往前在
[7, 4, 3, 1]中找第一个大于2的数,找到3(索引j = 4),交换它们,数组变为[1, 3, 7, 4, 2, 1]。- 将索引
i + 1到末尾的部分[7, 4, 2, 1]进行翻转,得到[1, 2, 4, 7]。- 最终结果为
[1, 3, 1, 2, 4, 7]。
2、解题代码
cpp
class Solution {
public:
void nextPermutation(vector<int>& nums) {
int n = nums.size();
// 从倒数第二个元素开始向前扫描
int i = n - 2;
// 步骤1:从后向前寻找第一个破坏单调不增(降序)的元素 nums[i]
// 使得 nums[i] < nums[i + 1]
while (i >= 0 && nums[i] >= nums[i + 1]) {
i--;
}
// 步骤2:如果找到了符合条件的 i(说明当前排列不是完全降序的全局最大排列)
if (i >= 0) {
int j = n - 1;
// 在区间 [i + 1, n - 1] 中从后向前寻找第一个严格大于 nums[i] 的元素
// 由于该区间本身是降序的,从后往前找到的一个数就是刚好大于 nums[i]
while (j >= 0 && nums[j] <= nums[i]) {
j--;
}
// 交换 nums[i] 和 nums[j],交换后 [i + 1, n - 1] 依然保持降序
swap(nums[i], nums[j]);
}
// 步骤3:反转 [i + 1, n - 1] 区间
// 将原先的降序区间反转为升序区间,从而使低位增幅最小,得到下一个字典序排列
// 若 i < 0(即原数组整体降序),则整体反转为最小升序排序
reverse(nums.begin() + i + 1, nums.end());
}
};
复杂度分析
- 时间复杂度: O ( n ) O(n) O(n),至多进行两次线性扫描和一次区间反转。
- 空间复杂度: O ( 1 ) O(1) O(1),纯原地指针交换,符合常数额外空间的要求。
三、知识风暴
字典序(Lexicographical Order) 是本题的核心概念。它本质上是把排列看作字符串,按「从左到右逐位比较」的方式确定大小关系:第一位不同则第一位决定大小,第一位相同则比较第二位,依此类推。本题要求「下一个排列」,就是在字典序意义下找到比当前排列「刚好大一点」的那个排列。
算法核心思想:
- 字典序的单调性:一个排列的字典序大小,由「第一个不同的位置」决定。越靠右的数字变化,对整体字典序的增幅越小------这正是「尽量在低位调整」这一思路的理论依据。
- 降序区间的意义 :从后向前扫描时,一旦遇到
nums[i] < nums[i + 1],说明[i + 1, n - 1]区间已经是降序(即该区间在字典序上已达局部最大)。此时必须进位到nums[i],才能得到更大的排列。 - 与贪心的区别 :贪心每一步只做当前最优选择、不回溯;而本题的「从后向前找第一个升序对」本质上是一种有策略的贪心------它先锁定增幅最小的进位位置,再在降序区间内选择「刚好大于」的交换对象,从而保证整体增幅最小。
常见对比:字典序 vs 数值大小
- 字典序 :按位比较,
[1, 2, 10]的字典序小于[1, 2, 3](因为1 < 3),但数值上10 > 3。本题的「下一个排列」严格按字典序定义。 - 数值大小:直接比较整体数值,与字典序在位数相同时结论一致,但本题关注的是排列的字典序关系,而非数值大小。
- 共同点:两者都依赖「从左到右逐位比较」的规则。区别在于字典序不关心数字的数值大小,只关心每一位的相对顺序。
双指针与原地算法的设计思想:
- 核心思想 :在降序区间
[i + 1, n - 1]中,用双指针从两端向中间交换,即可在 O ( n ) O(n) O(n) 时间内将降序反转为升序,无需额外数组。 - 与本题的联系 :交换
nums[i]与nums[j]后,区间[i + 1, n - 1]依然保持降序(因为nums[j]是区间内刚好大于nums[i]的最小值),因此可以直接用双指针反转,无需重新排序。 - 注意事项 :若整个数组本身就是降序(如
[3, 2, 1]),说明当前已是全局最大排列,此时直接整体反转即可得到最小排列,完成「循环」回到起点。
使用要点:
- 边界处理 :
i从n - 2开始向前扫描,避免越界;j从n - 1开始向前寻找,保证找到的是「刚好大于」的元素。 - 比较符号 :寻找升序对时用
nums[i] >= nums[i + 1]跳过相等元素(相等不构成严格升序);寻找交换对象时用nums[j] <= nums[i]跳过,确保严格大于。 - 反转区间 :
reverse(nums.begin() + i + 1, nums.end())统一处理「找到i」和「整体降序」两种情况,代码简洁。 - 结果返回 :本题为原地修改,无需返回值,直接操作
nums即可。
算法变体与扩展:
- 上一个排列(Previous Permutation):与本题对称,改为「从后向前找第一个降序对」并交换「刚好小于」的元素,思路完全一致。
- 排列的字典序排名(LeetCode 60):给定排列求其字典序排名,或给定排名求排列,本质是「康托展开」与「逆康托展开」,与本题共享字典序的核心思想。
- 全排列(LeetCode 46):回溯法枚举所有排列,与本题的「单步求下一个」形成对比------前者求全集,后者求后继。
- 第 k 个排列(LeetCode 60) :在字典序全排列中直接定位第 k 个,可用「阶乘数系统」跳过大量枚举,与本题的字典序规则一脉相承。相关 LeetCode 例题:
- 46. 全排列(回溯枚举所有排列)
- 47. 全排列 II(含重复元素,去重回溯)
- 60. 排列序列(字典序排名 / 第 k 个排列)
- 31. 下一个排列(本题,原地求后继)

