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

相关推荐
Jerry4 小时前
LeetCode 189. 轮转数组
算法
Jerry4 小时前
LeetCode 739. 每日温度
算法
2601_954526759 小时前
【工业传感与算法实战】温漂补偿与零点抗漂破局:基于二阶多项式拟合的 C/C++ 边缘校准算法,深度拆解“压力变送器什么牌子好”的技术硬指标
c语言·c++·算法
叩码以求索10 小时前
浅谈:算法萌新如何高效刷题应对面试(一)
算法·面试·职场和发展
c2385611 小时前
把 C++ 内存分配拆透:new 与 malloc 的三层血缘
开发语言·c++·算法
aaaameliaaa12 小时前
指针之总结
c语言·笔记·算法
宵时待雨12 小时前
优选算法专题9:哈希表
数据结构·算法·散列表
txzrxz12 小时前
最短路问题——Dijkstra 算法
数据结构·c++·算法·最短路·优先队列
gwf21612 小时前
磨损均衡算法(Wear Leveling)——SSD如何让每块闪存“公平退休“?
运维·数据库·人工智能·python·嵌入式硬件·算法·智能硬件
noipp13 小时前
推荐题目:洛谷 P6231 [JSOI2013] 公交系统
c语言·数据结构·c++·算法·游戏·洛谷·luogu