力扣(下一个排列)

解析 LeetCode 31. 下一个排列:找下降点 + 交换 + 翻转

一、题目分析

(一)问题定义

给定一个整数数组 nums,每个元素代表一位数字,整个数组表示一个整数的各位排列。要求把它变成字典序中紧挨着的下一个更大排列。如果当前已经是字典序最大的排列,就返回字典序最小的排列,也就是升序排列。

比如 1,2,3 的下一个排列是 1,3,2。3,2,1 没有下一个更大排列,所以返回 1,2,3。1,1,5 的下一个排列是 1,5,1。

(二)核心挑战

  1. 不能暴力生成所有排列再排序比较,必须在线性时间内完成
  2. 必须原地修改数组,不允许新建等长数组存储中间结果
  3. 字典序的增量要刚好一步,不能跳过或重复,比如 1,2,3 → 1,3,2 是对的,跳到 2,1,3 就错了

二、算法思路:找最长非递增后缀 + 交换 + 翻转

(一)为什么先从右往左找下降点

  • 字典序比较是从左到右逐位比大小,要让结果刚好变大一点,就得尽量保持左边不变,只动最靠右能改的位置
  • 从右往左扫描,找到第一个满足 numsi < numsi+1 的下标 i,说明 i 右侧是一段非递增序列(比如 5,4,3,2,1 或 5,4,3,3,1),这段内部已经排成该前缀下最大的字典序,没法再靠它自己变大,必须把 numsi 换掉
  • 这个 i 就是 pivot,是唯一需要被替换的位置

(二)为什么在右侧找最后一个大于 numsi 的数,而不是第一个

  • 因为右侧是非递增的,从右往左遍历,第一个遇到的大于 numsi 的数,就是右侧里最小的那个大于值
  • 比如 numsi = 3,右侧是 5,4,4,2,1,从右往左扫,先看到 1 不行,2 不行,4 符合,这就是最小的大于 3 的数,选它交换后整体增量最小
  • 如果选最左边那个 5,得到的排列就跳得太远了

(三)为什么交换后一定要翻转右侧子数组

  • 交换完成后,numsi+1...n-1 仍然保持非递增(因为原来非递增,只是把末尾一个较大数换到了前面,但没破坏整体趋势)
  • 而我们要的是该新前缀下的最小后缀,才能保证整体是下一个排列,不是下下个
  • 所以把 numsi+1...n-1 翻转成升序,就得到了字典序最小的合法后缀
  • 举例:1,2,3,6,5,4 → 找到 i=2(nums2=3),右侧 6,5,4 中最后一个大于 3 的是 4,交换得 1,2,4,6,5,3,再翻转 6,5,3 成 3,5,6,最终 1,2,4,3,5,6

三、代码实现与详细解析

java 复制代码
public void nextPermutation(int[] nums)
{
    int n = nums.length;
    // 初始化 i 为倒数第二个位置,准备从右往左找下降点
    int i = n - 2;
    // 向左扫描,跳过所有非下降位置:nums[i] >= nums[i+1] 就继续
    while (i >= 0 && nums[i] >= nums[i + 1])
    {
        i--;
    }
    // 如果 i >= 0,说明找到了下降点,需要交换
    if (i >= 0)
    {
        // j 从末尾开始往左找,找第一个大于 nums[i] 的数
        int j = n - 1;
        while (j > i && nums[j] <= nums[i])
        {
            j--;
        }
        // 找到了,交换 nums[i] 和 nums[j]
        swap(nums, i, j);
    }
    // 无论是否找到下降点,都要把 i+1 到末尾翻转成升序
    // 若没找到(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--;
    }
}

(一)代码流程拆解

  1. 初始化:设 n = nums.length,i = n-2,从倒数第二位开始向左扫描
  2. 找下降点:while 循环判断 numsi >= numsi+1,一直左移直到不满足或 i < 0。退出时 i 是 pivot 或 -1
  3. 判断分支:if (i >= 0) 决定是否执行交换逻辑,避免越界访问
  4. 查找交换对象:j 从 n-1 开始左移,跳过所有小于等于 numsi 的数,停在第一个更大的位置
  5. 执行交换:调用 swap 把 pivot 位和右侧选中的数互换
  6. 统一翻转:reverse 始终作用于 i+1 到末尾,这个范围恰好是原非递增后缀,翻转后变成升序,确保最小后缀

(二)关键逻辑解析

pivot 位置定义: numsi < numsi+1,且 i 右侧完全非递增。例如 1,3,2 中 i=0(nums0=1 < nums1=3),右侧 3,2 非递增

交换目标选择: 在右侧非递增段中找最后一个大于 numsi 的数,即从右往左第一个符合条件的。例如 numsi=2,右侧 5,4,3,2,1,选 3 而不是 5,避免跳步

翻转范围固定: 一定是 i+1 到 n-1,不会多也不会少。因为 pivot 已确定,右侧段长度就是 n-1-i,翻转后正好构成最小后缀

边界全覆盖: 当数组长度为 1,i 初始化为 -1,while 直接跳过,reverse(nums, 0, 0) 不执行任何交换,结果不变。当全降序如 3,2,1,i 最终为 -1,reverse 翻转整个数组得 1,2,3

四、复杂度分析

(一)时间复杂度

O(n)。最坏情况扫描三次:第一次找 pivot 最多 n-1 步,第二次找交换点最多 n-1 步,第三次翻转最多 n/2 步,总步数与 n 成正比

(二)空间复杂度

O(1)。只使用了 i、j、temp、n 四个整型变量,没有额外数组,也没有递归调用栈

相关推荐
Evand J几秒前
【电机滤波例程1】PMSM永磁同步电机转速与位置估计,MATLAB程序:标准无迹卡尔曼滤波、模型说明与仿真验证
算法·matlab·电机·ekf·负载·卡尔曼滤波
打码的老程是远篁18 分钟前
手把手教你玩转大模型——3. Transformer 的位置编码——模型怎么知道谁在前,谁在后?
人工智能·算法
嘎嘎风19 分钟前
MiniMind 学习笔记之 06 一张图、一百行:MiniMind 的骨架
算法·源码
用户73416810354822 分钟前
只盯被动响应做状态监测?论“控制信号”在变工况故障诊断中的重要作用
算法
Ivanqhz25 分钟前
MQA、GQA、稀疏/滑动窗口注意力及混合范式
人工智能·算法·机器学习
罗湖老棍子33 分钟前
荒岛野人(信息学奥赛一本通- P1637)(洛谷-P2421)
算法·数论·枚举·裴蜀定理·扩展欧几里得·扩展中国剩余定理
朝朝辞暮i38 分钟前
C++ 第 40 章:ROS2 Publisher + Timer + Subscriber + Callback + Executor 完整闭环
开发语言·c++·算法·ros2
400分1 小时前
具身智能系列之从零到「机械臂会放试管」:π0.5 VLA 真机部署完全指南【第一节】
算法
小宋10211 小时前
一套服务托管多个 LoRA:适配器加载、租户隔离与热切换实战
大数据·人工智能·算法
Rosanci1 小时前
Codex 下载与本地部署实战:从安装到运行全流程指南
开发语言·前端·算法·chatgpt·codex