力扣(下一个排列)

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

一、题目分析

(一)问题定义

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

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

(二)核心挑战

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

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

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

  • 字典序比较是从左到右逐位比大小,要让结果刚好变大一点,就得尽量保持左边不变,只动最靠右能改的位置
  • 从右往左扫描,找到第一个满足 numsi < numsi+1 的下标 i,说明 i 右侧是一段非递增序列(比如 5,4,3,2,15,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,33,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 四个整型变量,没有额外数组,也没有递归调用栈

相关推荐
疯狂打码的少年1 小时前
【数据结构】复习日:树 + 图 + 排序(整理对比表)
数据结构·笔记·算法
benchmark_cc1 小时前
Claude Code + MCP + QuantDash:打造全自动量化研究流水线的终极指南
人工智能·后端·爬虫·算法·claude·mcp·quantdash
星星.7222 小时前
2026河南萌新联赛第六场(郑州大学)补题B、D、L、J、I
数据结构·c++·算法
朝发如雪2 小时前
快速掌握Linux(2)(时间相关函数)(文件IO)
linux·python·算法
Forever Nore2 小时前
LeetCode 16 最接近的三数之和 - 双指针逼近
算法·leetcode·职场和发展
smj2302_796826522 小时前
解决leetcode第4033题有效K个不同元素子数组I
数据结构·python·算法·leetcode
学linux的QQ蛋3 小时前
Linux 文件 IO:系统调用 open/read/write 完整总结
linux·运维·算法
rannn_1113 小时前
【力扣hot100】回溯专题|全排列、子集、字母组合、组合总和、括号生成、单词搜索、分割回文串、N皇后
java·算法·leetcode·回溯
旖旎夜光3 小时前
LCR 173:在点名(二分查找) —— 题解
数据结构·c++·算法·leetcode·二分查找