文章目录
- [LeetCode 31. 下一个排列 - 最简算法实现](#LeetCode 31. 下一个排列 - 最简算法实现)
-
- [🎯 核心思路](#🎯 核心思路)
-
- [📝 算法步骤(4步法)](#📝 算法步骤(4步法))
- [💻 最简短代码](#💻 最简短代码)
- [🎭 最易懂代码(注释版,推荐!!)](#🎭 最易懂代码(注释版,推荐!!))
- [🎬 可视化演示过程](#🎬 可视化演示过程)
-
- [示例1:[1,2,3] → [1,3,2]](#示例1:[1,2,3] → [1,3,2])
- [示例2:[2,3,1] → [3,1,2]](#示例2:[2,3,1] → [3,1,2])
- [示例3:[3,2,1] → [1,2,3]](#示例3:[3,2,1] → [1,2,3])
- [复杂示例:[1,2,7,4,3,1] → [1,3,1,2,4,7]](#复杂示例:[1,2,7,4,3,1] → [1,3,1,2,4,7])
- [🎨 动画演示](#🎨 动画演示)
- [📊 算法流程图](#📊 算法流程图)
- [🎨 数组变化可视化](#🎨 数组变化可视化)
- [🧠 为什么这样做?](#🧠 为什么这样做?)
- [🔧 完整测试代码](#🔧 完整测试代码)
- [⚡ 性能分析](#⚡ 性能分析)
- [🎯 关键记忆点](#🎯 关键记忆点)
- [📈 排列变化轨迹图](#📈 排列变化轨迹图)
- [📋 数字操作追踪表(结合推荐的解法,仔细看这里,一定会懂的!!!)](#📋 数字操作追踪表(结合推荐的解法,仔细看这里,一定会懂的!!!))
-
- [示例:[1,2,7,4,3,1] → [1,3,1,2,4,7]](#示例:[1,2,7,4,3,1] → [1,3,1,2,4,7])
- 反转操作详解
- [🎯 记忆技巧](#🎯 记忆技巧)
LeetCode 31. 下一个排列 - 最简算法实现
🎯 核心思路
找下一个排列就是找刚好比当前排列大一点点的排列。
📝 算法步骤(4步法)
- 从右往左 找第一个小于右邻居的数(称为"突破点")
- 如果没找到,说明是最大排列,全部反转
- 如果找到了,从右往左找第一个大于突破点 的数,交换它们
- 把突破点右边的部分反转
💻 最简短代码
java
class Solution {
public void nextPermutation(int[] nums) {
int i = nums.length - 2;
// 步骤1:找突破点
while (i >= 0 && nums[i] >= nums[i + 1]) {
i--;
}
// 步骤2&3:如果找到突破点,找替换数并交换
if (i >= 0) {
int j = nums.length - 1;
while (nums[j] <= nums[i]) {
j--;
}
swap(nums, i, j);
}
// 步骤4:反转突破点右边部分
reverse(nums, i + 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 left = start, right = nums.length - 1;
while (left < right) {
swap(nums, left++, right--);
}
}
}
总共仅20行核心代码!
🎭 最易懂代码(注释版,推荐!!)
强烈建议把该代码和下面可视化演示部分结合看
java
class Solution {
public void nextPermutation(int[] nums) {
int n = nums.length;
int breakPoint = -1;
for (int i = n - 2; i >= 0; i--) {//从n-2开始,不能从n-1开始,n-1的话,i+1=n-1+1=n,就越界了
if (nums[i]<nums[i+1]) {
breakPoint=i;
break;
}
}
if (breakPoint!=-1) {
for(int i=n-1;i>breakPoint;--i){
if (nums[i]>nums[breakPoint]) {
swap(nums,i,breakPoint);
break;
}
}
}
reverse(nums,breakPoint+1);
}
void reverse(int[] nums,int start){
int left=start,right=nums.length-1;
while (left < right) {
swap(nums,left++,right--);
}
}
void swap(int[] arr,int i,int j){
int t = arr[i];
arr[i]=arr[j];
arr[j]=t;
}
}
🎬 可视化演示过程
示例1:[1,2,3] → [1,3,2]
原数组: [1, 2, 3]
↑ ↑ ↑
0 1 2
🔍 步骤1:从右往左找突破点
检查位置1: nums[1]=2 < nums[2]=3 ✓
找到突破点 i=1
🔄 步骤3:从右往左找替换数
检查位置2: nums[2]=3 > nums[1]=2 ✓
找到替换数 j=2
💱 交换 nums[1] 和 nums[2]
[1, 2, 3] → [1, 3, 2]
🔄 步骤4:反转突破点右边部分(位置2开始)
右边只有一个数,无需反转
✅ 最终结果: [1, 3, 2]
示例2:[2,3,1] → [3,1,2]
原数组: [2, 3, 1]
↑ ↑ ↑
0 1 2
🔍 步骤1:从右往左找突破点
检查位置1: nums[1]=3 > nums[2]=1 ❌
检查位置0: nums[0]=2 < nums[1]=3 ✓
找到突破点 i=0
🔄 步骤3:从右往左找替换数
检查位置2: nums[2]=1 < nums[0]=2 ❌
检查位置1: nums[1]=3 > nums[0]=2 ✓
找到替换数 j=1
💱 交换 nums[0] 和 nums[1]
[2, 3, 1] → [3, 2, 1]
🔄 步骤4:反转突破点右边部分(位置1开始)
反转 [2, 1] → [1, 2]
[3, 2, 1] → [3, 1, 2]
✅ 最终结果: [3, 1, 2]
示例3:[3,2,1] → [1,2,3]
原数组: [3, 2, 1]
↑ ↑ ↑
0 1 2
🔍 步骤1:从右往左找突破点
检查位置1: nums[1]=2 > nums[2]=1 ❌
检查位置0: nums[0]=3 > nums[1]=2 ❌
没有找到突破点 i=-1
🔄 步骤4:反转整个数组(从位置0开始)
[3, 2, 1] → [1, 2, 3]
✅ 最终结果: [1, 2, 3]
复杂示例:[1,2,7,4,3,1] → [1,3,1,2,4,7]
原数组: [1, 2, 7, 4, 3, 1]
↑ ↑ ↑ ↑ ↑ ↑
0 1 2 3 4 5
🔍 步骤1:从右往左找突破点
检查位置4: nums[4]=3 > nums[5]=1 ❌
检查位置3: nums[3]=4 > nums[4]=3 ❌
检查位置2: nums[2]=7 > nums[3]=4 ❌
检查位置1: nums[1]=2 < nums[2]=7 ✓
找到突破点 i=1
当前状态: [1, 2, 7, 4, 3, 1]
↑ 突破点
🔄 步骤3:从右往左找替换数(找第一个>2的数)
检查位置5: nums[5]=1 < 2 ❌
检查位置4: nums[4]=3 > 2 ✓
找到替换数 j=4
💱 交换 nums[1]=2 和 nums[4]=3
[1, 2, 7, 4, 3, 1] → [1, 3, 7, 4, 2, 1]
当前状态: [1, 3, 7, 4, 2, 1]
↑ 突破点右边需要反转
🔄 步骤4:反转突破点右边部分(位置2开始)
反转 [7, 4, 2, 1] → [1, 2, 4, 7]
[1, 3, 7, 4, 2, 1] → [1, 3, 1, 2, 4, 7]
✅ 最终结果: [1, 3, 1, 2, 4, 7]
🎨 动画演示
步骤动画图解
找突破点过程:
[1, 2, 7, 4, 3, 1]
←←←←←
从右往左检查每相邻对
位置4-5: 3 > 1 ❌ 继续
位置3-4: 4 > 3 ❌ 继续
位置2-3: 7 > 4 ❌ 继续
位置1-2: 2 < 7 ✅ 找到!突破点=1
找替换数过程:
[1, 2, 7, 4, 3, 1]
↑ ←←
突破点 从右往左找>2的数
位置5: 1 ≤ 2 ❌ 继续
位置4: 3 > 2 ✅ 找到!替换数=4
交换过程:
[1, 2, 7, 4, 3, 1]
↑ ↑
交换这两个数
[1, 3, 7, 4, 2, 1]
反转过程:
[1, 3, | 7, 4, 2, 1]
反转右边部分
[1, 3, | 1, 2, 4, 7]
最终结果:[1, 3, 1, 2, 4, 7]
📊 算法流程图
🎨 数组变化可视化
关键概念图解
🧠 为什么这样做?
核心洞察
-
为什么从右往左找?
- 我们要的是"刚好大一点点"的排列
- 改变越右边的数字,影响越小
-
为什么找"小于右邻居"的数?
- 这是第一个可以"增大"的位置
- 右边都是降序,无法再增大
-
为什么要反转右边部分?
- 交换后,右边仍然是降序(最大排列)
- 反转变成升序(最小排列),确保是"下一个"
直观理解
想象数字排列像一个"计数器":
[1, 2, 3] 计数器:123
[1, 3, 2] 计数器:132 (下一个)
[2, 1, 3] 计数器:213
...
我们的算法就是实现了这个"计数器+1"的操作!
🔧 完整测试代码
java
public class NextPermutationTest {
public static void main(String[] args) {
Solution solution = new Solution();
// 测试用例
int[][] testCases = {
{1, 2, 3}, // → [1, 3, 2]
{3, 2, 1}, // → [1, 2, 3]
{1, 1, 5}, // → [1, 5, 1]
{2, 3, 1}, // → [3, 1, 2]
{1, 2, 7, 4, 3, 1} // → [1, 3, 1, 2, 4, 7]
};
for (int[] nums : testCases) {
int[] original = nums.clone();
solution.nextPermutation(nums);
System.out.printf("输入: %s → 输出: %s%n",
Arrays.toString(original),
Arrays.toString(nums));
}
}
}
输出结果:
输入: [1, 2, 3] → 输出: [1, 3, 2]
输入: [3, 2, 1] → 输出: [1, 2, 3]
输入: [1, 1, 5] → 输出: [1, 5, 1]
输入: [2, 3, 1] → 输出: [3, 1, 2]
输入: [1, 2, 7, 4, 3, 1] → 输出: [1, 3, 1, 2, 4, 7]
⚡ 性能分析
- 时间复杂度:O(n) - 最多扫描数组3遍
- 空间复杂度:O(1) - 原地操作,只用几个变量
🎯 关键记忆点
- 四字口诀:找点、找数、交换、反转
- 两个查找:都是从右往左
- 一次反转:让右边变成最小排列
📈 排列变化轨迹图
以[1,2,3]的所有排列为例
📋 数字操作追踪表(结合推荐的解法,仔细看这里,一定会懂的!!!)
示例:[1,2,7,4,3,1] → [1,3,1,2,4,7]
步骤 | 操作类型 | 数组状态详情 | 关键信息 |
---|---|---|---|
初始 | 原始状态 | ---- | |
1 | 找突破点 | ---- | |
2 | 找替换数 | ---- | |
3 | 交换操作 | ---- | |
4 | 反转右边 | ---- |
反转操作详解
反转步骤 | 数组状态 | 指针位置 | 操作说明 |
---|---|---|---|
反转前 | ---- | --- | |
第1步 | ---- | --- | |
第2步 | ---- | --- |
🎯 记忆技巧
助记口诀
找点找数交换反转,
从右往左是关键,
突破点处做文章,
最小排列是终点。
三个关键位置
- 突破点:第一个可以"增大"的位置
- 替换数:刚好能让突破点增大的最小数
- 反转区:突破点右边需要变成最小排列
这个算法虽然只有20行代码,但蕴含了深刻的数学思想,是贪心算法的完美体现!