中等
整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。
- 例如,
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 <= 1000 <= nums[i] <= 100
题意描述:
给你一个数字数组,把它看作一个"数字组合",请你找出比它大、但又最接近它的下一个组合。如果已经是最大的了,就回到最小的组合(升序)。
📝 核心笔记:下一个排列 (Next Permutation)
1. 核心思想 (一句话总结)
"寻找拐点:从后往前找第一个'跌落'的数字(因为它还有变大的潜力),把它和后面比它稍大的数交换(变大一点点),再把剩下的尾巴翻转成升序(变小一点点),从而得到紧挨着的下一个排列。"
- 找位置 ( i**)** :从右向左,找第一个
nums[i] < nums[i+1]的位置。这表示从i+1到末尾都是降序的(已经是该部分的最大排列),无法通过内部调整变大了,必须动i。 - 找替身 ( j**)** :在
i的右边,找 最小的 且 比 **nums[i]**大 的数。 - 重置尾部 :交换后,
i后面的序列依然是降序的,必须翻转成升序,使其变成最小值。
2. 算法流程 (三个步骤)
- 寻找较小数 (First Loop):
-
i从n-2开始倒着走。while (nums[i] >= nums[i+1]) i--。- 停下来的
i就是我们要动的那个数(也就是"山峰"左侧的山脚)。
- 寻找较大数并交换 (Second Loop):
-
- 如果
i >= 0(找到了拐点):
- 如果
-
-
j从n-1开始倒着走。while (nums[j] <= nums[i]) j--。- 找到第一个比
nums[i]大的数nums[j]。 - Swap(i, j)。
-
- 翻转后缀 (Reverse):
-
- 无论是否执行了步骤 2(如果
i=-1说明全是降序,如 321),都要把[i+1, end]这一段翻转。 - 对于 321,
i=-1,翻转[0, end]变成 123。
- 无论是否执行了步骤 2(如果
🔍 代码回忆清单
// 题目: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 + 1到n - 1。 - 如果是全降序 (
i = -1),则翻转0到n - 1,逻辑统一。
- 是
-
\] **为什么交换后直接翻转是对的?**
-
- 在交换前,
[i+1...end]是降序的。 - 我们拿
nums[j](稍微大的数) 换走了nums[i]。 - 由于
nums[j]原本就在这个降序序列里,交换后,[i+1...end]依然保持降序性质。 - 所以直接
reverse就能得到升序。
- 在交换前,
🖼️ 数字演练
nums = [1, 2, 7, 4, 3, 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)。
- 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].
- Step 3: 翻转后缀 ( i+1****开始)
-
- 当前后缀
[7, 4, 2, 1](索引 2 到 5)。 - 翻转后变为
[1, 2, 4, 7]. - 拼接回去。
- 当前后缀
- 最终结果 :
[1, 3, 1, 2, 4, 7].