文章目录
问题描述
实现获取下一个排列的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列。
如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。
必须原地修改,只允许使用额外常数空间。
示例:
- 输入:nums = [1,2,3],输出:[1,3,2]
- 输入:nums = [3,2,1],输出:[1,2,3]
- 输入:nums = [1,1,5],输出:[1,5,1]
核心思想
要找到下一个排列,我们需要理解什么是"字典序中下一个更大的排列"。关键在于:我们希望增加的幅度尽可能小。
从右向左找第一个升序对
找到较小数nums[i]
从右向左找第一个大于nums[i]的数
交换这两个数
将i+1到末尾的序列反转
得到下一个排列
算法原理详解
观察规律
让我们通过一个例子来理解:[1, 5, 8, 4, 7, 6, 5, 3, 1]
原数组
1
5
8
4
7
6
5
3
1
关键观察:
- 从右向左看,
[7, 6, 5, 3, 1]是降序的,无法通过重排得到更大的数 - 继续向左,发现
4 < 7,这是第一个"较小数" - 要让排列变大且增幅最小,需要用刚好大于4的数替换4
- 在右侧降序序列中,从右向左找第一个大于4的数,是5
- 交换4和5后,将右侧序列反转为升序
算法步骤可视化
步骤4: 反转 步骤3: 交换 步骤2: 找较大数 步骤1: 找较小数 步骤4: 反转 步骤3: 交换 步骤2: 找较大数 步骤1: 找较小数 [1,5,8,4,7,6,5,3,1] 找到i=3, nums[3]=4 找到j=6, nums[6]=5 [1,5,8,5,7,6,4,3,1] [1,5,8,5,1,3,4,6,7] 从右向左找第一个nums[i] < nums[i+1] 从右向左找第一个nums[j] > nums[i] 交换nums[i]和nums[j] 反转i+1到末尾的序列
为什么这样做是正确的?
下一个排列
目标
字典序更大
增幅最小
策略
尽量改动右侧
右侧影响小
找到突破点
第一个升序对
实现
交换
用刚好大的数替换
反转
保证右侧最小
核心逻辑:
- 从右向左找较小数:右侧降序部分已经是最大排列,无法调整,必须向左找到可以增大的位置
- 找刚好大于它的数:保证增幅最小
- 反转右侧:交换后右侧仍是降序,反转成升序使其最小
解法一:标准算法
代码实现
java
class Solution {
public void nextPermutation(int[] nums) {
int n = nums.length;
// 步骤1: 从右向左找第一个升序对,找到较小数的位置i
int i = n - 2;
while (i >= 0 && nums[i] >= nums[i + 1]) {
i--;
}
// 步骤2: 如果找到了较小数(不是完全降序)
if (i >= 0) {
// 从右向左找第一个大于nums[i]的数
int j = n - 1;
while (j >= 0 && nums[j] <= nums[i]) {
j--;
}
// 交换
swap(nums, i, j);
}
// 步骤3: 反转i+1到末尾的序列
reverse(nums, i + 1, n - 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 left, int right) {
while (left < right) {
swap(nums, left, right);
left++;
right--;
}
}
}
复杂度分析
时间复杂度: O(n)
- 找较小数:最坏O(n)
- 找较大数:最坏O(n)
- 反转操作:最坏O(n)
- 总体:O(n)
空间复杂度: O(1)
- 只使用了常数个额外变量
- 原地修改数组
解法二:优化的查找方式
对于找较大数的步骤,由于右侧是降序的,我们可以使用二分查找优化。
代码实现
java
class Solution {
public void nextPermutation(int[] nums) {
int n = nums.length;
// 找较小数
int i = n - 2;
while (i >= 0 && nums[i] >= nums[i + 1]) {
i--;
}
if (i >= 0) {
// 使用二分查找找较大数(右侧是降序的)
int j = binarySearchGreater(nums, i + 1, n - 1, nums[i]);
swap(nums, i, j);
}
// 反转
reverse(nums, i + 1, n - 1);
}
// 在降序数组中二分查找第一个大于target的元素
private int binarySearchGreater(int[] nums, int left, int right, int target) {
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] > target) {
left = mid + 1; // 降序数组,大的在左边
} else {
right = mid;
}
}
return left;
}
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 left, int right) {
while (left < right) {
swap(nums, left, right);
left++;
right--;
}
}
}
复杂度分析
时间复杂度: O(n)
- 虽然使用了二分查找O(log n),但整体仍是O(n)
- 因为找较小数和反转操作都是O(n)
空间复杂度: O(1)
特殊情况处理
找到 i >= 0
未找到 i = -1
开始
是否找到较小数?
正常流程
完全降序
找较大数并交换
反转i+1到末尾
结束
反转整个数组
特殊情况: 当数组完全降序时(如[3,2,1]),找不到较小数,此时i=-1,直接反转整个数组得到升序排列。
算法执行示例
以 [1, 3, 5, 4, 2] 为例:
初始状态: [1, 3, 5, 4, 2]
↑ ↑
i 降序开始
步骤1: 从右向左找,3 < 5,i = 1
步骤2: 从右向左找第一个大于3的数
[1, 3, 5, 4, 2]
↑
j = 2 (nums[2]=5 > 3)
步骤3: 交换 nums[1] 和 nums[2]
[1, 5, 3, 4, 2]
步骤4: 反转 i+1 到末尾
[1, 5, 2, 4, 3]
结果: [1, 5, 2, 4, 3]
总结
下一个排列问题的核心是理解字典序和贪心思想:
- 从右向左找突破点:右侧降序部分已是最大,需要向左找可增大的位置
- 最小增幅原则:用刚好大一点的数替换,保证增幅最小
- 右侧最小化:交换后将右侧反转为升序,使整体尽可能小
这个算法巧妙地利用了排列的性质,通过O(n)时间和O(1)空间完成了任务,是一个经典的原地算法设计范例。