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)空间完成了任务,是一个经典的原地算法设计范例。

相关推荐
Darkwanderor24 分钟前
什么数据量适合用什么算法
c++·算法
zc.ovo1 小时前
河北师范大学2026校赛题解(A,E,I)
c++·算法
py有趣1 小时前
力扣热门100题之环形链表
算法·leetcode·链表
py有趣1 小时前
力扣热门100题之回文链表
算法·leetcode·链表
Kk.08022 小时前
数据结构|链表 刷题
数据结构·链表
月落归舟3 小时前
帮你从算法的角度来认识二叉树---(二)
算法·二叉树
清华都得不到的好学生4 小时前
数据结构->1.稀疏数组,2.数组队列(没有取模),3.环形队列
java·开发语言·数据结构
SilentSlot4 小时前
【数据结构】Hash
数据结构·算法·哈希算法
是娇娇公主~4 小时前
Lambda表达式详解
数据结构·c++