31. 下一个排列(Next Permutation)

频次 ★★★ · 难度 🟡 · 高频:阿里

题目

给定一个整数数组,表示一个排列,原地修改为字典序中的下一个更大的排列。如果不存在更大的排列,则修改为最小的排列(升序)。

示例

输入: [1, 2, 3]
输出: [1, 3, 2]

输入: [3, 2, 1]
输出: [1, 2, 3]

思路

三步法:

  1. 从右向左找第一个下降的位置 i(即 nums[i] < nums[i+1]
  2. 从右向左找第一个大于 nums[i] 的元素 j,交换 nums[i]nums[j]
  3. 翻转 i+1 到末尾的部分(使其变为最小排列)

如果找不到下降位置,说明当前是最大排列,直接翻转整个数组。

例如 [1,3,5,4,2]

  • 找到 i=1(3<5)
  • 找到 j=3(4 是从右第一个大于 3 的)
  • 交换 → [1,4,5,3,2]
  • 翻转后面 → [1,4,2,3,5]

代码

public void nextPermutation(int[] nums) {
    int n = nums.length;
    // 1. 找第一个下降位置
    int i = n - 2;
    while (i >= 0 && nums[i] >= nums[i + 1]) i--;
    
    if (i >= 0) {
        // 2. 找第一个大于 nums[i] 的元素并交换
        int j = n - 1;
        while (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 start, int end) {
    while (start < end) swap(nums, start++, end--);
}

复杂度

  • 时间:O(n) — 最多三次线性扫描
  • 空间:O(1) — 原地操作

边界条件

  • 数组已是最大排列(降序,如 [3,2,1]):第一步找不到下降位置(i 最终为 -1),跳过交换步骤,直接翻转整个数组得到最小排列 [1,2,3]
  • 数组只有一个元素:i = n - 2 = -1,同样跳过交换,翻转”整个数组”(其实只有一个元素,翻转无影响)。
  • 数组已是最小排列(升序):正常走三步得到字典序中紧邻的下一个排列。

变式

  • 上一个排列(Previous Permutation):思路完全对称——从右向左找第一个”上升”的位置,再从右向左找第一个小于它的元素交换,最后把后面部分翻转成降序(而不是升序)。
  • 求排列的字典序中第 k 个排列(LeetCode 60 题):用康托展开的思路,通过阶乘进制直接计算每一位,而不是重复调用”下一个排列” k 次(否则 k 很大时效率太低)。

易错点

  • 第 2 步”从右向左找第一个大于 nums[i] 的元素”,不能写成”找到第一个大于就停”从左往右找——因为 i+1 到末尾这一段是降序的,从右往左找到的第一个大于 nums[i] 的元素,才是”大于 nums[i] 的元素里最小的那个”,这样交换后能保证是紧邻的下一个排列而不是跳过了中间的排列。
  • 第 3 步的翻转不能省略:只交换不翻转,i+1 之后依然是降序,不是字典序最小的排列,导致得到的不是”紧邻的下一个”而是一个更大的排列。

面试追问

  • 为什么第 2 步一定能找到满足条件的元素? 因为第 1 步保证了 nums[i] < nums[i+1],而 i+1 到末尾是降序的,所以 nums[i+1] 本身就满足”大于 nums[i]”,第 2 步的 while 循环最坏情况下也会在 j = i+1 处停下,不会出现找不到的情况。
  • 能不能证明”交换 + 翻转”两步能得到字典序里紧邻的下一个排列,而不是跳过或重复? 交换让第 i 位变成”比原值大的最小候选”,是局部意义上最小的增量;翻转把 i 之后变成升序(该区间内字典序最小的排列),两者结合保证了整体是”比原排列大的所有排列中最小的一个”,也就是紧邻的下一个。

关联题

  • 同套路:189. 轮转数组 —— 复用”区间反转”子操作
  • 进阶:46. 全排列(回溯枚举全部)、60. 排列序列(数学法直接定位第 k 个)
  • 知识点:“从右找第一个升序对 → 交换 → 反转尾部”就是 C++ STL next_permutation 的实现