【力扣100题】89.下一个排列

一、题目描述

整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。

例如,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 核心思想

从一个排列生成下一个更大排列的步骤:

  1. 找破坏点 :从后往前找第一个升序对 nums[i] < nums[i+1]i 是破坏点
  2. 找替换点 :在 [i+1, n) 区间从后往前找第一个大于 nums[i] 的元素
  3. 交换 :交换 nums[i]nums[j]
  4. 翻转[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)
特殊情况 没有破坏点时反转整个数组

相关推荐
HjhIron1 天前
面试常客:字符串算法从入门到进阶
算法·面试
吴佳浩1 天前
DeepSeek DSpark:Confidence-Scheduled Speculative Decoding 技术解析
人工智能·算法·deepseek
触底反弹1 天前
🧠 搞懂 Token,才算真正入门大模型——从分词原理到 Embedding 语义实战
javascript·人工智能·算法
vivo互联网技术1 天前
ICLR 2026 | 基于后验采样的图像恢复方法LearnIR:人脸去阴影、去雾
人工智能·算法·aigc
浮生望1 天前
JS字符串与回文算法:从包装类到双指针的面试进阶之路
javascript·算法
黄敬峰1 天前
面试必刷:从JS底层包装类到双指针,彻底搞懂字符串与回文算法
算法
地平线开发者2 天前
J6B vio scenario sample
算法
BothSavage2 天前
Trae远程开发中DeepSeek自定义模型4054错误的排查与修复
算法
小林ixn2 天前
从暴力到KMP:一道题彻底搞懂字符串匹配的前世今生
算法