31. 下一个排列

31. 下一个排列

中等

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

  • 例如,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:

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

示例 2:

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

示例 3:

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

提示:

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

题意描述:

给你一个数字数组,把它看作一个"数字组合",请你找出比它大、但又最接近它的下一个组合。如果已经是最大的了,就回到最小的组合(升序)。

📝 核心笔记:下一个排列 (Next Permutation)

1. 核心思想 (一句话总结)

"寻找拐点:从后往前找第一个'跌落'的数字(因为它还有变大的潜力),把它和后面比它稍大的数交换(变大一点点),再把剩下的尾巴翻转成升序(变小一点点),从而得到紧挨着的下一个排列。"

  • 找位置 ( i**)** :从右向左,找第一个 nums[i] < nums[i+1] 的位置。这表示从 i+1 到末尾都是降序的(已经是该部分的最大排列),无法通过内部调整变大了,必须动 i
  • 找替身 ( j**)** :在 i 的右边,找 最小的 **nums[i]** 的数。
  • 重置尾部 :交换后,i 后面的序列依然是降序的,必须翻转成升序,使其变成最小值。
2. 算法流程 (三个步骤)
  1. 寻找较小数 (First Loop)
    • in-2 开始倒着走。
    • while (nums[i] >= nums[i+1]) i--
    • 停下来的 i 就是我们要动的那个数(也就是"山峰"左侧的山脚)。
  1. 寻找较大数并交换 (Second Loop)
    • 如果 i >= 0 (找到了拐点):
      • jn-1 开始倒着走。
      • while (nums[j] <= nums[i]) j--
      • 找到第一个比 nums[i] 大的数 nums[j]
      • Swap(i, j)
  1. 翻转后缀 (Reverse)
    • 无论是否执行了步骤 2(如果 i=-1 说明全是降序,如 321),都要把 [i+1, end] 这一段翻转。
    • 对于 321,i=-1,翻转 [0, end] 变成 123。
🔍 代码回忆清单
复制代码
// 题目:LC 31. Next Permutation
class Solution {
    public void nextPermutation(int[] nums) {
        int n = nums.length;

        // 1. 从右向左找到第一个"非递增"元素 nums[i]
        // 也就是找到第一个"山峰"左边的数
        int i = n - 2;
        while (i >= 0 && nums[i] >= nums[i + 1]) {
            i--;
        }

        // 2. 如果找到了 (i >= 0),说明不是全降序(如 321)
        if (i >= 0) {
            // 从右向左找到 nums[i] 右边"第一个大于它"的数 nums[j]
            // 因为右边已经是降序了,从右边找的第一个肯定就是"最小的大于数"
            int j = n - 1;
            while (nums[j] <= nums[i]) {
                j--;
            }
            // 交换:让高位稍微变大一点
            swap(nums, i, j);
        }

        // 3. 翻转:将 i 后面的降序序列变成升序
        // 这一步让低位变得尽可能小
        reverse(nums, i + 1, n - 1);
    }

    private void swap(int[] nums, int i, int j) {
        int tmp = nums[i];
        nums[i] = nums[j];
        nums[j] = tmp;
    }

    private void reverse(int[] nums, int left, int right) {
        while (left < right) {
            swap(nums, left++, right--);
        }
    }
}
⚡ 快速复习 CheckList (易错点)
  • \] **第二个循环为什么是** **nums[j] <= nums[i]****?**

    • 我们需要找的是大于 nums[i] 的数。所以如果遇到小于或等于的,都要跳过 (j--)。
    • 一定要注意这里不能漏掉 =
  • \] **翻转的范围?**

    • i + 1n - 1
    • 如果是全降序 (i = -1),则翻转 0n - 1,逻辑统一。
  • \] **为什么交换后直接翻转是对的?**

    • 在交换前,[i+1...end] 是降序的。
    • 我们拿 nums[j] (稍微大的数) 换走了 nums[i]
    • 由于 nums[j] 原本就在这个降序序列里,交换后,[i+1...end]依然保持降序性质
    • 所以直接 reverse 就能得到升序。
🖼️ 数字演练

nums = [1, 2, 7, 4, 3, 1]

  1. Step 1: 找 i
    • nums[4]=3 < nums[5]=1? No (3 > 1). i--.
    • nums[3]=4 < nums[4]=3? No. i--.
    • nums[2]=7 < nums[3]=4? No. i--.
    • nums[1]=2 < nums[2]=7? Yes.
    • 停止,i = 1 (值是 2)。
  1. Step 2: 找 j**(在 i 的右边找比 2 大的)**
    • 从右往左扫:
    • nums[5]=1 > 2? No. j--.
    • nums[4]=3 > 2? Yes.
    • 停止,j = 4 (值是 3)。
    • Swap(1, 4) : 数组变为 [1, 3, 7, 4, 2, 1].
  1. Step 3: 翻转后缀 ( i+1****开始)
    • 当前后缀 [7, 4, 2, 1] (索引 2 到 5)。
    • 翻转后变为 [1, 2, 4, 7].
    • 拼接回去。
  1. 最终结果 : [1, 3, 1, 2, 4, 7].
相关推荐
Frostnova丶2 小时前
LeetCode 3212. 统计X和Y出现次数相等的子矩阵数量
算法·leetcode·矩阵
We་ct2 小时前
LeetCode 53. 最大子数组和:两种高效解法(动态规划+分治)
前端·算法·leetcode·typescript·动态规划·分治
sin°θ_陈2 小时前
CVPR 2026的3DGS卷到什么地步?工程语义上探:BrepGaussian如何打通图像到CAD的最后一公里?(Part III 1-3)
python·深度学习·算法·机器学习·3d·webgl
没头脑的男大2 小时前
环形链表很曼妙的一个做题思路
数据结构·链表
shehuiyuelaiyuehao2 小时前
算法9,滑动窗口,长度最小的子数组
数据结构·算法·leetcode
葳_人生_蕤2 小时前
hot100——动态规划
算法·动态规划
sheeta19982 小时前
LeetCode 每日一题笔记 日期:2025.03.18 题目:3070.元素和小于等于k的子矩阵的数目
笔记·leetcode·矩阵
量化炼金 (CodeAlchemy)2 小时前
【交易策略】基于决策树的机器学习策略:从预测价格到预测市场结构
算法·决策树·机器学习
计算机安禾2 小时前
【C语言程序设计】第33篇:二级指针与指针数组
c语言·开发语言·数据结构·c++·算法·visual studio code·visual studio