LeetCode 31. 下一个排列
文章目录
- LeetCode 31. 下一个排列 - 最简算法实现
- 🎯 核心思路
- 📝 算法步骤(4步法)
- 💻 最简短代码
- 🎭 最易懂代码(注释版,推荐!!)
- 🎬 可视化演示过程
- 示例1:[1,2,3] → [1,3,2]
- 示例2:[2,3,1] → [3,1,2]
- 示例3:[3,2,1] → [1,2,3]
- 复杂示例:[1,2,7,4,3,1] → [1,3,1,2,4,7]
- 🎨 动画演示
- 步骤动画图解
- 📊 算法流程图
- 🎨 数组变化可视化
- 关键概念图解
- 🧠 为什么这样做?
- 核心洞察
- 直观理解
- 🔧 完整测试代码
- ⚡ 性能分析
- 🎯 关键记忆点
- 📈 排列变化轨迹图
- 以[1,2,3]的所有排列为例
- 📋 数字操作追踪表(结合推荐的解法,仔细看这里,一定会懂的!!!)
- 示例:[1,2,7,4,3,1] → [1,3,1,2,4,7]
- 反转操作详解
- 🎯 记忆技巧
- 助记口诀
- 三个关键位置
LeetCode 31. 下一个排列 - 最简算法实现
🎯 核心思路
找下一个排列就是找刚好比当前排列大一点点的排列。
📝 算法步骤(4步法)
- 从右往左找第一个小于右邻居的数(称为"突破点")
- 如果没找到,说明是最大排列,全部反转
- 如果找到了,从右往左找第一个大于突破点的数,交换它们
- 把突破点右边的部分反转
💻 最简短代码
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行核心代码!
🎭 最易懂代码(注释版,推荐!!)
强烈建议把该代码和下面可视化演示部分结合看
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"的操作!
🔧 完整测试代码
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 | 找突破点 |
| 🔍 nums[1]=2 < nums[2]=7 ✅ i=1 为突破点 | |||||||||||||||||||||
2 | 找替换数 |
| 🔍 从右往左找 > 2 的数 ✅ nums[4]=3 > nums[1]=2 | |||||||||||||||||||||
3 | 交换操作 |
| 🔄 交换突破点与替换数 💫 位置1和位置4的值互换 | |||||||||||||||||||||
4 | 反转右边 |
| 🔄 反转突破点右边部分 ✅ 最终结果生成完毕 |
反转操作详解
反转步骤 | 数组状态 | 指针位置 | 操作说明 | |||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
反转前 |
| left=2, right=5 | 🎯 准备反转位置2-5的部分 | |||||||||||||||||||||
第1步 |
| left=3, right=4 | 🔄 交换位置2和5:7↔1 | |||||||||||||||||||||
第2步 |
| left=4, right=3 | 🔄 交换位置3和4:4↔2 ✅ 反转完成 |
🎯 记忆技巧
助记口诀
找点找数交换反转,
从右往左是关键,
突破点处做文章,
最小排列是终点。
三个关键位置
- 突破点:第一个可以"增大"的位置
- 替换数:刚好能让突破点增大的最小数
- 反转区:突破点右边需要变成最小排列
这个算法虽然只有20行代码,但蕴含了深刻的数学思想,是贪心算法的完美体现!