LeetCode Hot100(63/100)——31. 下一个排列

文章目录

问题描述

实现获取下一个排列的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列。

如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。

必须原地修改,只允许使用额外常数空间。

示例:

  • 输入:nums = 1,2,3,输出:1,3,2
  • 输入:nums = 3,2,1,输出:1,2,3
  • 输入:nums = 1,1,5,输出:1,5,1

核心思想

要找到下一个排列,我们需要理解什么是"字典序中下一个更大的排列"。关键在于:我们希望增加的幅度尽可能小。
从右向左找第一个升序对
找到较小数numsi
从右向左找第一个大于numsi的数
交换这两个数
将i+1到末尾的序列反转
得到下一个排列

算法原理详解

观察规律

让我们通过一个例子来理解:[1, 5, 8, 4, 7, 6, 5, 3, 1]
原数组
1
5
8
4
7
6
5
3
1

关键观察:

  1. 从右向左看,[7, 6, 5, 3, 1] 是降序的,无法通过重排得到更大的数
  2. 继续向左,发现 4 < 7,这是第一个"较小数"
  3. 要让排列变大且增幅最小,需要用刚好大于4的数替换4
  4. 在右侧降序序列中,从右向左找第一个大于4的数,是5
  5. 交换4和5后,将右侧序列反转为升序

算法步骤可视化

步骤4: 反转 步骤3: 交换 步骤2: 找较大数 步骤1: 找较小数 步骤4: 反转 步骤3: 交换 步骤2: 找较大数 步骤1: 找较小数 1,5,8,4,7,6,5,3,1 找到i=3, nums3=4 找到j=6, nums6=5 1,5,8,5,7,6,4,3,1 1,5,8,5,1,3,4,6,7 从右向左找第一个numsi < numsi+1 从右向左找第一个numsj > numsi 交换numsi和numsj 反转i+1到末尾的序列

为什么这样做是正确的?

下一个排列
目标
字典序更大
增幅最小
策略
尽量改动右侧
右侧影响小
找到突破点
第一个升序对
实现
交换
用刚好大的数替换
反转
保证右侧最小

核心逻辑:

  1. 从右向左找较小数:右侧降序部分已经是最大排列,无法调整,必须向左找到可以增大的位置
  2. 找刚好大于它的数:保证增幅最小
  3. 反转右侧:交换后右侧仍是降序,反转成升序使其最小

解法一:标准算法

代码实现

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--;
        }
        
        // 步骤2: 如果找到了较小数(不是完全降序)
        if (i >= 0) {
            // 从右向左找第一个大于nums[i]的数
            int j = n - 1;
            while (j >= 0 && nums[j] <= nums[i]) {
                j--;
            }
            // 交换
            swap(nums, i, j);
        }
        
        // 步骤3: 反转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 left, int right) {
        while (left < right) {
            swap(nums, left, right);
            left++;
            right--;
        }
    }
}

复杂度分析

时间复杂度: O(n)

  • 找较小数:最坏O(n)
  • 找较大数:最坏O(n)
  • 反转操作:最坏O(n)
  • 总体:O(n)

空间复杂度: O(1)

  • 只使用了常数个额外变量
  • 原地修改数组

解法二:优化的查找方式

对于找较大数的步骤,由于右侧是降序的,我们可以使用二分查找优化。

代码实现

java 复制代码
class Solution {
    public void nextPermutation(int[] nums) {
        int n = nums.length;
        
        // 找较小数
        int i = n - 2;
        while (i >= 0 && nums[i] >= nums[i + 1]) {
            i--;
        }
        
        if (i >= 0) {
            // 使用二分查找找较大数(右侧是降序的)
            int j = binarySearchGreater(nums, i + 1, n - 1, nums[i]);
            swap(nums, i, j);
        }
        
        // 反转
        reverse(nums, i + 1, n - 1);
    }
    
    // 在降序数组中二分查找第一个大于target的元素
    private int binarySearchGreater(int[] nums, int left, int right, int target) {
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] > target) {
                left = mid + 1;  // 降序数组,大的在左边
            } else {
                right = mid;
            }
        }
        return left;
    }
    
    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 left, int right) {
        while (left < right) {
            swap(nums, left, right);
            left++;
            right--;
        }
    }
}

复杂度分析

时间复杂度: O(n)

  • 虽然使用了二分查找O(log n),但整体仍是O(n)
  • 因为找较小数和反转操作都是O(n)

空间复杂度: O(1)

特殊情况处理

找到 i >= 0
未找到 i = -1
开始
是否找到较小数?
正常流程
完全降序
找较大数并交换
反转i+1到末尾
结束
反转整个数组

特殊情况: 当数组完全降序时(如3,2,1),找不到较小数,此时i=-1,直接反转整个数组得到升序排列。

算法执行示例

[1, 3, 5, 4, 2] 为例:

复制代码
初始状态: [1, 3, 5, 4, 2]
                  ↑   ↑
                  i   降序开始

步骤1: 从右向左找,3 < 5,i = 1

步骤2: 从右向左找第一个大于3的数
       [1, 3, 5, 4, 2]
              ↑     
              j = 2 (nums[2]=5 > 3)

步骤3: 交换 nums[1] 和 nums[2]
       [1, 5, 3, 4, 2]

步骤4: 反转 i+1 到末尾
       [1, 5, 2, 4, 3]

结果: [1, 5, 2, 4, 3]

总结

下一个排列问题的核心是理解字典序和贪心思想:

  1. 从右向左找突破点:右侧降序部分已是最大,需要向左找可增大的位置
  2. 最小增幅原则:用刚好大一点的数替换,保证增幅最小
  3. 右侧最小化:交换后将右侧反转为升序,使整体尽可能小

这个算法巧妙地利用了排列的性质,通过O(n)时间和O(1)空间完成了任务,是一个经典的原地算法设计范例。

相关推荐
生成论实验室12 小时前
自动驾驶:一个自主运动的系统
人工智能·算法·机器学习·语言模型·机器人·自动驾驶·安全架构
sheeta199812 小时前
LeetCode 每日一题笔记 日期:2026.06.16 题目:3612. 字符串特殊符号处理
笔记·算法·leetcode
CoderYanger12 小时前
A.每日一题:2095. 删除链表的中间节点
java·数据结构·程序人生·leetcode·链表·面试·职场和发展
Jasmine_llq12 小时前
《B4264 [GESP202503 四级] 二阶矩阵》
线性代数·算法·矩阵·二维矩阵遍历枚举所有2×2矩阵·交叉乘积等式条件判断·输入输出快读加速·长整型防溢出计数统计
不知名的老吴12 小时前
面经经验分享|算法和数据结构考察
数据结构·经验分享·算法
叫我:松哥13 小时前
基于Python flask的中学可控智能命题系统设计与实现,整合遗传算法、DeepSeek 大模型及数据库技术构建一体化应用
数据库·人工智能·python·算法·机器学习·flask·遗传算法
CoderYanger13 小时前
A.每日一题:3612. 用特殊操作处理字符串 I
java·程序人生·leetcode·面试·职场和发展·学习方法·改行学it
黎阳之光13 小时前
黎阳之光透明大楼:实景孪生重构智慧建筑全新范式
人工智能·物联网·算法·安全·数字孪生
承渊政道13 小时前
【MySQL数据库学习】(MySQL表的内外连接)
数据库·学习·mysql·leetcode·bash·数据库开发·数据库系统
旖-旎14 小时前
《LeetCode 130 被围绕的区域 FloodFill DFS 解法》
c++·算法·深度优先·力扣·floodfill