【LeetCode Hot100 刷题日记 (99/100)】31. 下一个排列 —— 数组、双指针、原地操作、字典序💡

5 阅读5分钟

📌 题目链接: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 世纪提出,现代广泛用于排列生成。

算法思想
要让新排列 刚好比当前大一点,需做到三点:

  1. 找到最右边的“可提升”位置(即存在右侧更大值的位置);
  2. 用右侧最小的“更大值”替换它
  3. 将右侧部分变为最小字典序(升序)

📐 算法步骤详解(以 [4,5,2,6,3,1] 为例)

  1. 从右往左找第一个 nums[i] < nums[i+1]
    → 找到 i = 2nums[2]=2 < nums[3]=6
    → 此时 [i+1, n)严格非递增(降序),无法通过内部调整变大。

  2. [i+1, n) 中从右往左找第一个 nums[j] > nums[i]
    → 找到 j = 4nums[4]=3 > 2,且是最小的满足条件的值)
    → 交换 nums[i]nums[j][4,5,3,6,2,1]

  3. 反转 [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
  • 为何不用排序而用反转?
  • 如何处理重复元素?(本算法天然支持!因为比较用 >=>=,跳过相等情况)

🧩 解题思路(分步拆解)

  1. 定位“转折点”
    从末尾开始,找到第一个破坏“非递增”趋势的位置 i。这是唯一能通过交换使排列变大的位置。

  2. 寻找最优替换值
    i 右侧(已知为降序),从右向左找第一个大于 nums[i] 的值,确保替换后增量最小。

  3. 重置后缀为最小状态
    交换后,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!💪

📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!