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]

核心思想

要找到下一个排列,我们需要理解什么是"字典序中下一个更大的排列"。关键在于:我们希望增加的幅度尽可能小。
从右向左找第一个升序对
找到较小数nums[i]
从右向左找第一个大于nums[i]的数
交换这两个数
将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, nums[3]=4 找到j=6, nums[6]=5 [1,5,8,5,7,6,4,3,1] [1,5,8,5,1,3,4,6,7] 从右向左找第一个nums[i] < nums[i+1] 从右向左找第一个nums[j] > nums[i] 交换nums[i]和nums[j] 反转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)空间完成了任务,是一个经典的原地算法设计范例。

相关推荐
222you1 小时前
Mysql的索引以及底层的数据结构(面试)
数据结构·数据库·mysql
智者知已应修善业2 小时前
【不用第三变量交换2个数】2024-10-18
c语言·数据结构·c++·经验分享·笔记·算法
XiaoHu02072 小时前
C/C++数据结构与算法(第三弹)
数据结构
会编程的土豆2 小时前
c语言时间戳从入门到精通
linux·c语言·算法
所谓伊人,在水一方3332 小时前
【机器学习精通】第2章 | 优化算法深度解析:从梯度下降到自适应优化器
人工智能·python·算法·机器学习·信息可视化
Storynone2 小时前
【Day24】LeetCode:122. 买卖股票的最佳时机 II,55. 跳跃游戏,45. 跳跃游戏II,1005. K次取反后最大化的数组和
python·算法·leetcode
滴滴答滴答答2 小时前
机考刷题之 17&18&19&20&21&22 LeetCode 1248&121&43&93&62&63
算法·leetcode·职场和发展
for_ever_love__2 小时前
Objective-C学习 类别和扩展
学习·算法·objective-c
Sakinol#2 小时前
Leetcode Hot 100 ——回溯part02
算法·leetcode