(LeetCode-Hot100)31. 下一个排列

问题简介

LeetCode 31. 下一个排列

题目描述

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

  • 例如,arr = [1,2,3] ,以下这些都可以视作 arr 的排列:[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]

示例 4:

复制代码
输入:nums = [1,3,2]
输出:[2,1,3]

解题思路

💡 核心思想: 找到字典序中刚好比当前排列大的下一个排列。

步骤详解(✅ 标准解法)

  1. 从右向左找第一个「下降」的位置 i

    • 即找到最大的索引 i,使得 nums[i] < nums[i + 1]
    • 如果找不到(整个数组非递增),说明当前是最大排列,直接反转整个数组即可
  2. 从右向左找第一个大于 nums[i] 的元素 nums[j]

    • 因为 i 右侧是递减序列,所以从右往左第一个大于 nums[i] 的就是最小的可交换元素
  3. 交换 nums[i]nums[j]

  4. 反转 i+1 到末尾的子数组

    • 使右侧变为升序(字典序最小)

其他解法对比

解法 思路 时间复杂度 空间复杂度 是否满足题目要求
❌ 暴力生成全排列 生成所有排列后找下一个 O(n!) O(n!)
✅ 上述标准解法 贪心 + 双指针 O(n) O(1) ✅ 是
⚠️ 二分优化查找 在步骤2中用二分查找 O(n) O(1) ✅ 是(但常数更大)

💡 实际上由于步骤2的子数组是严格递减的,线性扫描已经是最优,二分并无优势。


代码实现

=== "Java"

java 复制代码
class Solution {
    public void nextPermutation(int[] nums) {
        int n = nums.length;
        
        // 步骤1: 从右向左找第一个下降位置 i
        int i = n - 2;
        while (i >= 0 && nums[i] >= nums[i + 1]) {
            i--;
        }
        
        // 如果找到了下降位置
        if (i >= 0) {
            // 步骤2: 从右向左找第一个大于 nums[i] 的位置 j
            int j = n - 1;
            while (j >= 0 && nums[j] <= nums[i]) {
                j--;
            }
            // 步骤3: 交换 nums[i] 和 nums[j]
            swap(nums, i, j);
        }
        
        // 步骤4: 反转 i+1 到末尾
        reverse(nums, i + 1, n - 1);
    }
    
    private void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
    
    private void reverse(int[] nums, int start, int end) {
        while (start < end) {
            swap(nums, start, end);
            start++;
            end--;
        }
    }
}

=== "Go"

go 复制代码
func nextPermutation(nums []int) {
    n := len(nums)
    
    // 步骤1: 从右向左找第一个下降位置 i
    i := n - 2
    for i >= 0 && nums[i] >= nums[i+1] {
        i--
    }
    
    // 如果找到了下降位置
    if i >= 0 {
        // 步骤2: 从右向左找第一个大于 nums[i] 的位置 j
        j := n - 1
        for j >= 0 && nums[j] <= nums[i] {
            j--
        }
        // 步骤3: 交换 nums[i] 和 nums[j]
        nums[i], nums[j] = nums[j], nums[i]
    }
    
    // 步骤4: 反转 i+1 到末尾
    reverse(nums, i+1, n-1)
}

func reverse(nums []int, start, end int) {
    for start < end {
        nums[start], nums[end] = nums[end], nums[start]
        start++
        end--
    }
}

示例演示

📌 [1,3,2] 为例:

步骤 操作 数组状态
初始 - [1,3,2]
1️⃣ 找下降位置:i=0 (1<3) [1,3,2]
2️⃣ 找大于 nums[0]=1 的最右元素:j=2 (2>1) [1,3,2]
3️⃣ 交换 nums[0]nums[2] [2,3,1]
4️⃣ 反转 i+1=1 到末尾 [2,1,3]

✅ 最终结果:[2,1,3]


答案有效性证明

💡 为什么这个算法正确?

  1. 必要性:要得到字典序下一个排列,必须修改尽可能靠右的位置(保持前缀不变)
  2. 充分性
    • 步骤1确保了 i 是最右的可增大位置
    • 步骤2确保了替换值是最小的可能增大值
    • 步骤4确保了后缀是最小字典序(升序)

反例验证 :对于最大排列 [3,2,1]

  • 步骤1找不到 ii=-1
  • 直接执行步骤4:反转整个数组 → [1,2,3]

复杂度分析

指标 分析
时间复杂度 O(n) - 最多三次线性扫描
空间复杂度 O(1) - 只使用常数额外空间
原地修改 ✅ 是

📌 关键点:虽然有多个循环,但每个元素最多被访问常数次,因此整体仍是线性时间。


问题总结

核心要点回顾:

  • 字典序下一个排列的本质是找到最右的可增大位置
  • 贪心策略 :在可增大位置选择最小的增大值
  • 后缀处理 :增大后必须让后缀变为最小字典序

💡 面试技巧:

  • 先理解字典序的定义
  • 从简单例子入手(如 [1,2,3,4] 的各种排列)
  • 强调算法的原地性线性时间特性

📌 易错点提醒:

  • 边界条件:i = -1 的情况(最大排列)
  • 比较操作符:步骤1用 >=,步骤2用 <=
  • 反转范围:从 i+1 开始,不是从 i 开始

github地址: https://github.com/swf2020/LeetCode-Hot100-Solutions

相关推荐
Cosmoshhhyyy1 小时前
《Effective Java》解读第40条:坚持使用Override注解
java·开发语言
ValhallaCoder1 小时前
hot100-二分查找
数据结构·python·算法·二分查找
0 0 01 小时前
【C++】矩阵翻转/n*n的矩阵旋转
c++·线性代数·算法·矩阵
m0_531237171 小时前
C语言-指针,结构体
c语言·数据结构·算法
癫狂的兔子2 小时前
【Python】【机器学习】十大算法简介与应用
python·算法·机器学习
丰海洋2 小时前
leetcode-hot100-1.两数之和
数据结构·算法·leetcode
苦藤新鸡2 小时前
58 单词搜索
数据结构·算法
_F_y2 小时前
背包问题动态规划
算法·动态规划
Frostnova丶2 小时前
LeetCode 401. 二进制手表
算法·leetcode