Skip to content

31. 下一个排列

LeetCode 官方题目链接

题目描述

整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。

  • 例如,arr = [1,2,3] ,以下这些都可以视作 arr 的排列:[1,2,3][1,3,2][3,1,2][2,3,1]

整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。

  • 例如,arr = [1,2,3] 的下一个排列是 [1,3,2]
  • 类似地,arr = [2,3,1] 的下一个排列是 [3,1,2]
  • arr = [3,2,1] 的下一个排列是 [1,2,3] ,因为 [3,2,1] 不存在一个字典序更大的排列。

给你一个整数数组 nums ,找出 nums 的下一个排列。

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

示例 1:

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

示例 2:

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

示例 3:

text
输入:nums = [1,1,5]
输出:[1,5,1]

提示:

  • 1 <= nums.length <= 100
  • 0 <= nums[i] <= 100

思路拆解

我们要找到一个大于当前序列的新序列,且变大的幅度尽可能小。

  1. 从后向前查找第一个升序对

    • 我们需要找到两个相邻元素 nums[i]nums[i+1],满足 nums[i] < nums[i+1]
    • 这样,nums[i] 就是需要被替换成更大数字的位置(称为“较小数”),因为交换 nums[i] 后,序列变大的幅度主要由高位决定,越靠右的高位变动幅度越小。
    • 如果在整个数组中都找不到这样的相邻元素,说明数组已经是降序排列(最大排列),直接将其反转为升序(最小排列)即可。
  2. 从后向前查找第一个比 nums[i] 大的数

    • i 之后的部分(即 nums[i+1]nums[n-1]),由于步骤 1 中 i 是第一个满足升序的位置,所以 nums[i+1]nums[n-1] 必然是降序的。
    • 我们需要在这个降序区间中,找到第一个比 nums[i] 大的数 nums[j](称为“较大数”)。
    • 因为 nums[i+1]nums[n-1] 是降序的,所以从后往前找,第一个满足 nums[j] > nums[i]j 就是我们要找的最小的“较大数”。
  3. 交换 nums[i]nums[j]

    • 交换后,i 位置变大了,整个序列的字典序变大了。
    • 但是,i 之后的部分可能还不是最小的排列。
  4. 反转 i 之后的部分

    • 由于交换前 nums[i+1]nums[n-1] 是降序的,交换后(nums[j] 换到 inums[i] 换到 j),这一段依然保持降序性质(因为 nums[j] 是第一个比 nums[i] 大的,交换后该位置变小了,但对于降序序列来说不影响整体趋势,严谨证明略,但直观上成立)。
    • 为了让变大的幅度尽可能小,我们需要让 i 之后的部分变为升序(最小排列)。
    • 因为这部分目前是降序的,直接 反转 即可变为升序。

代码实现

javascript
/**
 * @param {number[]} nums
 * @return {void} Do not return anything, modify nums in-place instead.
 */
var nextPermutation = function(nums) {
    const n = nums.length;
    let i = n - 2;

    // 1. 从后向前找到第一个升序对 (nums[i] < nums[i+1])
    while (i >= 0 && nums[i] >= nums[i + 1]) {
        i--;
    }

    if (i >= 0) {
        // 2. 如果找到了 i,则在 i 后面找到第一个比 nums[i] 大的数
        let j = n - 1;
        while (j > i && nums[j] <= nums[i]) {
            j--;
        }
        // 3. 交换 nums[i] 和 nums[j]
        [nums[i], nums[j]] = [nums[j], nums[i]];
    }

    // 4. 反转 i + 1 之后的所有元素,使其变为升序
    let left = i + 1;
    let right = n - 1;
    while (left < right) {
        [nums[left], nums[right]] = [nums[right], nums[left]];
        left++;
        right--;
    }
};

运行演示

假设 nums = [1, 2, 3]

  1. n = 3, i = 1
  2. nums[1] (2) < nums[2] (3),循环不执行,i 停在 1
  3. i >= 0,进入 if 块。
  4. j = 2
  5. nums[2] (3) > nums[1] (2),循环不执行,j 停在 2
  6. 交换 nums[1]nums[2] -> nums 变为 [1, 3, 2]
  7. 反转 i + 1 (即 2) 之后的元素:
    • left = 2, right = 2
    • left < right 不成立,循环结束。
  8. 结果:[1, 3, 2]

假设 nums = [3, 2, 1]

  1. i = 1nums[1] (2) >= nums[2] (1)i-- -> i = 0
  2. nums[0] (3) >= nums[1] (2)i-- -> i = -1
  3. i < 0,不执行 if 块。
  4. 反转 0 + 1 (即 0) 之后的元素(整个数组):
    • left = 0, right = 2。交换 31 -> [1, 2, 3]
    • left++, right-- -> left = 1, right = 1
    • 结束。
  5. 结果:[1, 2, 3]

复杂度分析

  • 时间复杂度:$O(n)$,其中 n 是数组的长度。最坏情况下需要扫描数组两次(一次找 i,一次找 j)并反转一次。
  • 空间复杂度:$O(1)$,只使用了常数个额外空间。

Power by VitePress