力扣热题100实战 | 第31期:下一个排列——数组规律的极致探索

力扣热题100实战 | 第31期:下一个排列------数组规律的极致探索

在有序的世界里寻找下一个更大的排列,就像在数字的迷宫中寻找唯一正确的出口。这道题教会我们:当数字序列达到峰值时,如何通过最小的调整,让它重新焕发更大的生机。

前言

你好,我是@礼拜天没时间。

上一期我们攻克了"串联所有单词的子串"(第30题),掌握了滑动窗口在单词级别匹配中的应用。这一期,我们来解一道非常有意思的题目------下一个排列(LeetCode 第31题)。

这道题在力扣上被标记为"中等",但它的难度并不在于代码实现有多复杂,而在于发现规律的过程。很多同学第一次看到这道题时,要么完全不知道从何下手,要么虽然能写出代码但讲不清楚为什么这样写。

今天,我希望能带你从最朴素的"手工找下一个排列"出发,一步步推导出那个优雅的三步算法,并理解它背后的数学原理。


一、题目:找下一个更大的排列

先看题目描述(LeetCode 第31题):

实现获取 下一个排列 的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列 。

如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。

必须 原地 修改,只允许使用额外常数空间 。

示例 1:

复制代码
输入:nums = [1,2,3]
输出:[1,3,2]
解释:所有排列按字典序排序为:[1,2,3]、[1,3,2]、[2,1,3]、[2,3,1]、[3,1,2]、[3,2,1]
      [1,2,3] 的下一个更大排列是 [1,3,2]

示例 2:

复制代码
输入:nums = [3,2,1]
输出:[1,2,3]
解释:[3,2,1] 已经是最大排列,没有下一个更大排列,所以返回最小排列 [1,2,3]

示例 3:

复制代码
输入:nums = [1,1,5]
输出:[1,5,1]

关键点解读

  1. 字典序:把数组看作一个数字,按数字大小排序的顺序就是字典序 。

  2. 下一个更大 :要找的是刚好比当前排列大一点的那个排列,不是任意一个更大的排列 。

  3. 常数空间:不能用额外数组,必须在原数组上操作 。

  4. 处理最大情况:如果当前已经是最大排列,下一个就是最小排列(升序)。


二、第一反应:暴力枚举(理论上可行,实际上不可行)

当我第一次看到这道题,我的第一反应和第46题"全排列"一样:生成所有排列,排序,然后找到当前排列的下一个

核心思想

  1. 递归生成数组的所有排列
  2. 将所有排列按字典序排序
  3. 找到当前排列在列表中的位置
  4. 返回下一个排列(如果是最后一个则返回第一个)

复杂度分析

  • 时间复杂度:O(n!) ------ 生成所有排列是指数级复杂度
  • 空间复杂度:O(n!) ------ 需要存储所有排列

为什么不可行?

  1. 效率极低:对于长度为n的数组,全排列数量是n!,当n=10时已经是3628800种,完全不可接受 。

  2. 不符合题目要求:题目要求常数空间,而暴力法需要指数级空间。

  3. 没有利用规律:全排列虽然有序,但我们不需要生成所有排列就能找到下一个。


三、核心解法:经典三步走算法

3.1 规律发现

观察一个例子:[1,2,3,5,4,2]

我们希望找到下一个更大的排列。直觉上,我们希望变动尽量靠右的位置,因为越靠右变动,对整个数字的影响越小 。

让我们从右向左看:

  • 最后两位 [4,2] 是降序,无法通过交换这两位得到更大的数
  • 再看 [5,4,2] 也是降序
  • 再看 [3,5,4,2] ------ 这里 3 后面是降序,而且 3 < 5

这个 3 就是我们要找的下降点 。因为从它往右都是降序,说明这一部分已经是最大排列,无法通过只改动右边得到更大的排列。

3.2 算法步骤

第一步:找下降点

从右向左遍历,找到第一个满足 nums[i] < nums[i+1] 的位置 i 。这个 i 就是我们要调整的"小数"位置。

第二步:找交换点

如果找到了 i,再从右向左遍历,找到第一个大于 nums[i] 的数 nums[j] 。这个 j 就是我们要交换的"大数"位置。

第三步:交换并反转

  • 交换 nums[i]nums[j]
  • i+1 到末尾的部分反转(因为原来这部分是降序,反转后变成升序,即最小排列)

特殊情况:如果第一步没找到下降点,说明整个数组是降序排列,已经是最大排列,直接反转整个数组 。

3.3 图解流程

nums = [1,2,3,5,4,2] 为例:

第1步:找下降点

复制代码
[1, 2, 3, 5, 4, 2]
              ↑  ↑
              4 > 2? 是,继续
          ↑  ↑
          5 > 4? 是,继续
      ↑  ↑
      3 > 5? 否!找到了下降点 i = 2 (值为3)

第2步:找交换点

从右向左找第一个大于3的数:

复制代码
[1, 2, 3, 5, 4, 2]
                 ↑ 2 < 3? 是,跳过
              ↑ 4 > 3? 是!找到了 j = 4 (值为4)

第3步:交换

交换 nums[2]nums[4]

复制代码
交换前:[1, 2, 3, 5, 4, 2]
交换后:[1, 2, 4, 5, 3, 2]

第4步:反转 i+1 到末尾

原来 i+1=3 到末尾是 [5,3,2],反转后为 [2,3,5]

复制代码
最终结果:[1, 2, 4, 2, 3, 5]

验证:123542 的下一个确实是 124235


四、代码实现:三步走算法(面试终极答案)

java 复制代码
class Solution {
    public void nextPermutation(int[] nums) {
        int n = nums.length;
        if (n <= 1) return; // 长度小于等于1,直接返回 
        
        // 1. 从右向左找第一个下降点 i
        int i = n - 2;
        while (i >= 0 && nums[i] >= nums[i + 1]) {
            i--;
        }
        
        // 2. 如果找到了下降点
        if (i >= 0) {
            // 从右向左找第一个大于 nums[i] 的数 j
            int j = n - 1;
            while (j >= 0 && nums[j] <= nums[i]) {
                j--;
            }
            // 交换 i 和 j
            swap(nums, i, j);
        }
        
        // 3. 反转 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;
    }
    
    // 反转数组从 start 到 end 的部分
    private void reverse(int[] nums, int start, int end) {
        while (start < end) {
            swap(nums, start, end);
            start++;
            end--;
        }
    }
}

代码解析

  1. 找下降点while (i >= 0 && nums[i] >= nums[i + 1]) 跳过所有逆序对,找到第一个正序对 。

  2. 边界处理 :如果 i 最终为 -1,说明整个数组是降序,直接进入反转步骤 。

  3. 找交换点 :第二个 while 循环从右向左找第一个大于 nums[i] 的数。因为 [i+1, end] 是降序,所以找到的第一个大于 nums[i] 的数就是最小的那个大数 。

  4. 反转 :无论是否找到下降点,最后都要反转 i+1 到末尾的部分。如果没找到下降点,i = -1,反转整个数组 。

复杂度分析

  • 时间复杂度:O(n) ------ 最多遍历数组三次
  • 空间复杂度:O(1) ------ 只用了常数个变量

五、细节剖析:面试官真正关心的问题

Q1:为什么找下降点时要比较 nums[i] >= nums[i+1] 而不是 >

答案 :因为要跳过所有非严格递增的情况。如果遇到相等的情况,也不能作为下降点,因为交换相等的数不会让排列变大。例如 [1,2,2,3],如果错误地把第一个2当作下降点,交换后会得到更小的排列 。

Q2:为什么找交换点时要从右向左找第一个大于 nums[i] 的数?

答案 :因为 [i+1, end] 是降序排列,所以从右向左第一个大于 nums[i] 的数,就是所有大于 nums[i] 的数中最小的那个 。这保证了交换后,新排列的增幅尽可能小。

Q3:如果数组是 [3,2,1] 这种完全降序,算法怎么处理?

答案

  • 第一步找下降点:i 最终会变成 -1
  • 第二步 if (i >= 0) 不执行
  • 第三步反转:reverse(nums, 0, n-1) 将整个数组反转,得到 [1,2,3]

Q4:为什么交换后要反转 i+1 到末尾?

答案 :交换后,[i+1, end] 这部分仍然是降序(因为原来就是降序,而且换进来的数比原来的 nums[i] 大,不会破坏降序性质)。降序是这部分的最大排列,我们需要的是最小排列,所以反转成升序 。

Q5:这个算法能处理重复元素吗?

答案 :能。关键在于比较时用了 >=<=,跳过了相等的情况,确保找到的下降点是严格下降,找到的交换点是严格大于 。


六、面试官追问进阶版

追问1:如何实现上一个排列?

思路:对称操作即可

  • 找第一个上升点(nums[i] > nums[i+1]
  • 找第一个小于它的数
  • 交换并反转

追问2:如何实现第k个排列(LeetCode 60)?

思路:用阶乘数系统(Factorial Number System),通过数学计算直接定位,不需要逐个生成 。

追问3:如果要求返回所有排列,怎么实现?

思路:用回溯算法,每次找到下一个排列并记录,直到回到最小排列 。

java 复制代码
public List<List<Integer>> getAllPermutations(int[] nums) {
    List<List<Integer>> result = new ArrayList<>();
    // 先排序得到最小排列
    Arrays.sort(nums);
    do {
        result.add(arrayToList(nums));
        nextPermutation(nums);
    } while (!isSortedDesc(nums)); // 直到变成最大排列
    return result;
}

追问4:如何验证当前排列是否是最大排列?

思路 :检查是否整个数组是降序。即遍历一次,看是否有 nums[i] < nums[i+1]


七、实际开发:这道题到底有什么用?

很多读者会问:"下一个排列,实际工作中哪用得到?"

其实它的思想无处不在:

场景1:字典序迭代器

在生成组合、排列的算法中,经常需要按字典序迭代所有可能。这个算法就是迭代器的核心 。

场景2:密码破解中的穷举

在暴力破解密码时,需要按字典序生成所有可能的字符组合,这个算法可以高效实现。

场景3:数字游戏中的"下一关"

在有些数字游戏中,需要找到比当前数字大的下一个数字,比如"找出大于给定数的最小回文数"。

场景4:排列组合的数学问题

在数学竞赛或算法竞赛中,经常需要处理排列的顺序问题,这道题是基础。

场景5:面试中的思维题

这道题考察的是发现规律的能力,而不是死记硬背算法,所以经常作为面试题出现 。


八、总结:从一道题到一类题

回顾一下,我们从下一个排列学到了什么:

维度 收获
算法思维 暴力枚举 → 规律发现,理解字典序排列的数学性质
代码技巧 从右向左遍历、交换后反转、边界条件处理
复杂度分析 O(n) 时间、O(1) 空间,这是最优解
面试要点 为什么找下降点?为什么找最小的更大数?为什么要反转?
工程关联 字典序迭代器、穷举算法、数学游戏

力扣热题100的第三十一题,不是为了难住你,而是为了告诉你:有些问题看似复杂,但当你找到了规律,就能写出简洁而优雅的代码。这种发现规律的能力,比死记硬背一百道题都重要。

下一期预告:《最长有效括号》------栈与动态规划的巅峰对决


附录:思考题

看完这篇文章,你可以试着回答:

  1. 如果要求实现上一个排列,代码需要怎么改?
  2. 如何用这个算法实现一个函数,返回一个排列是第几个(按字典序)?
  3. 你能用这道题的思路,去解 LeetCode 60(排列序列)吗?

欢迎在评论区留下你的思考!

相关推荐
一直都在5721 小时前
新Java基础(二十五):异常类
java·开发语言
ws540d1 小时前
Ranking All UsersLast Updated: 2026-03-14(Sat) 19:46算法启发式活跃用户所有用户
算法
lcreek1 小时前
LeetCode LCR114.火星词典
leetcode··拓扑排序
xiaoye37082 小时前
java后端面试一般问什么?
java·面试
进击的小头2 小时前
第8篇:线性二次型调节器
python·算法·动态规划
Z9fish2 小时前
sse哈工大C语言编程练习42
c语言·开发语言·算法
badhope2 小时前
OpenClaw卸载命令全解析
java·linux·人工智能·python·sql·数据挖掘·策略模式
Hello.Reader2 小时前
Flink Task Lifecycle 一篇讲透 StreamTask 与 Operator 生命周期
java·大数据·flink
一个有毅力的吃货2 小时前
这个SKILL打通了AI写公众号文章的最后一公里
css·算法