31. 下一个排列

难度:中等 | 主题:数组 + 双指针

题目

整数数组的一个排列就是将其所有成员以序列或线性顺序排列。下一个排列是指下一个字典序更大的排列。给你一个整数数组 nums,找出 nums 的下一个排列。如果不存在下一个更大的排列,则将数组重新排列为最小的排列(即升序排列)。必须原地修改。

示例

输入:nums = [1,2,3]

输出:[1,3,2]

解释:123 的下一个排列是 132


思路

先讲个故事:电子密码锁

你有一个电子密码锁,密码是四位数。你随手拨到 21543,想看看”下一个有效密码”是什么。

你盯着数字想了想:

  1. 从右往左找”下降点”——5→4→3 都在增大(降序),直到 1→5 这里变小了。1 就是进位点。

  2. 从右往左找比 1 大的最小的数——3(因为 3 > 1 且比 5 和 4 更接近 1)。交换 1 和 3,得到 23541。

  3. 进位点右边的部分(541),现在是降序,反转成升序(145)→ 得到 23145。

21543 的下一个密码是 23145。

这就是”下一个排列”算法的本质——找进位点 → 进位 → 后面归零最小化,和数字进位的思维一模一样。


引导式推导:三步标准算法

理解”字典序下一个”

全排列按字典序排列:[1,2,3] → [1,3,2] → [2,1,3] → [2,3,1] → [3,1,2] → [3,2,1] → 回到 [1,2,3]

要找到”刚好比当前大的下一个”,不能跳(排序后不一定比当前大 1),也不能回到开头。

核心洞察:类比数字进位

拿 21543 举例:

  • 最后四位 1543 是递减的——这是”尾部最大”的状态(降序是最大的排列方式)

  • 要想变大,必须动到倒数第 5 位(1),因为 1543 已经最大,无法再增

  • 找到”进位点”(从右往左第一个下降的点)


graph TD

    subgraph SGlirsj["21543 -> 23145"]

        step1["① 找下降点: 1 < 5 -> 进位点在 1"]

        step2["② 找交换对象: 从右找第一个 > 1 -> 3"]

        step3["③ 交换 1 和 3: 23541"]

        step4["④ 反转 5 4 1 -> 1 4 5: 23145"]

    end

    step1 --> step2 --> step3 --> step4

为什么反转?

交换后,原进位点右边的部分仍然是降序(因为交换没有破坏降序性质——被交换的元素本来就在降序序列中最接近)。降序是”这段能组成的最大排列”,我们需要的是”最小的下一个”→ 反转成升序。

如果没有下降点?

整个数组是降序(如 [3,2,1])→ 这是最大排列→ 下一个回到最小→ 反转整个数组。


代码

 
def nextPermutation(self, nums):
 
    n = len(nums)
 
    i = n - 2
 
    # ① 从右往左找第一个下降点:nums[i] < nums[i+1]
 
    while i >= 0 and nums[i] >= nums[i + 1]:
 
        i -= 1
 
    if i >= 0:  # 找到了下降点(不是最大排列)
 
        # ② 从右往左找第一个严格大于 nums[i] 的数
 
        j = n - 1
 
        while nums[j] <= nums[i]:
 
            j -= 1
 
        nums[i], nums[j] = nums[j], nums[i]  # ③ 交换
 
    # ④ 反转 i+1 到末尾(无条件执行:最大排列时反转整个数组)
 
    left, right = i + 1, n - 1
 
    while left < right:
 
        nums[left], nums[right] = nums[right], nums[left]
 
        left += 1
 
        right -= 1
 

复杂度

指标解释
时间O(n)最坏扫描两遍 + 反转一遍,三个循环是加法关系
空间O(1)原地修改

实战考量

频率分析

出现在:字节/美团/阿里 二面,考察数学规律和步骤拆分能力。难在”你想不到这三步”而不是”这三步写不出来”。很多人知道思路但写不对边界条件。

延伸思考

Q:上一个排列怎么做?

A:反过来:找上升点(nums[i] > nums[i+1])→ 从右找更小的交换 → 反转后面变降序。

Q:第 k 个排列怎么求?

A:用阶乘确定每一位的数字。不需要逐个求到第 k 个。n=4,k=9,第一位是 9/3! = 1…3,索引 1 的元素(2),然后在剩下的数字中继续。详见 60 题。

Q:为什么反转是”无条件执行”的?

A:如果 i = -1(整个数组降序,没有找到下降点),反转整个数组就是”最大排列变最小排列”。统一处理。

Q:找交换对象时为什么用 <= 而不是 <

A:相等时交换无意义(得到同样的排列),要找严格大于 nums[i] 的数。

易错点

  • 反转无条件执行——不管有没有找到下降点都要反转

  • while nums[j] <= nums[i]<= 而非 <

  • 三个循环依次执行,时间相加不是相乘

  • 下降点的索引范围是 n-2 到 0


生活类比

下一个排列 → 找进位点 → 进位 → 归零最小化

像你手机上的数字时钟从 23:59:59 跳到 00:00:00:

秒数到了 59(最大)→ 归零,分钟进一 → 分钟也到了 59 → 归零,小时进一。

下一个排列也是一样的逻辑:从右往左找到”还能变大的那一位”(下降点),把它变大一点点(交换),后面所有位”归零”到最小(降序变升序)。

如果所有位都不能变大了 → 全部归零,回到起点。


相关题目

题目关系
46全排列全排列生成基础,回溯法
60. 排列序列第 k 个排列,阶乘数系统化
31下一个排列本题,三步标准算法入门

→ 返回题单:LeetCode学习路线图 > 一、数组与哈希(含双指针、矩阵)

速记卡(面试闪卡)

Q1:一句话讲清「31. 下一个排列」到底是什么?

A:下一个排列是找字典序刚好比当前大的排列:找下降点→交换→反转尾部,原地 O(n) 完成。

Q2:像什么生活场景 —— 怎么理解?

A:像电子密码锁拨到 21543,想看「下一个有效密码」:从右找下降点 1(进位点),从右找比它大的最小数 3 交换,右边降序反转成升序,得 23145。

Q3:三步标准算法 —— 怎么理解?

A:像数字进位:①从右找第一个下降点 nums[i]<nums[i+1];②从右找第一个>nums[i] 交换;③反转 i+1 到末尾。反转无条件执行,最大排列时反转整组即回到最小。

Q4:复杂度与易错点 —— 怎么理解?

A:像扫地扫两遍:时间 O(n)(扫描两遍+反转,加法关系),空间 O(1) 原地。易错:反转无条件、找对象用 <= 而非 <、下降点索引范围 n-2 到 0。

Q5:延伸与变体 —— 怎么理解?

A:像同一套锁芯换方向:上一个排列反过来(找上升点→找更小交换→反转变降序);第 k 个排列用阶乘定位每一位,不必逐个求。字节 / 美团二面常考步骤拆分。

Q6:核心速记主线有哪些?

  • 字典序下一个:下降点→交换→反转

  • 反转无条件,最大排列即归零

  • 时间 O(n) 空间 O(1) 原地

  • 找对象用 <=,下降点 n-2 到 0

口诀

A:下一个排列找下降,进位交换反转尾

反转无条件莫忘,最大归零回起点

时间 O n 空间一,原地改动不费地

上一个反过来求,阶乘定位第 k 位

相关链接