算法背景
下一个排列问题是排列组合中的经典问题:给定一个数字序列,找出这些数字重新排列后的下一个更大的排列。如果不存在更大的排列(即当前已是降序),则返回最小排列(升序)。该问题在LeetCode 31题中非常著名,也是理解字典序排列生成算法的入门题目。
字典序排列在现实中有广泛应用:密码生成、组合测试、遗传算法中的基因编码等场景都需要按字典序遍历所有可能状态。
核心思路
下一个排列的生成遵循以下步骤:
- 从右向左找第一个升序对:找到最大的索引
i,使得nums[i] < nums[i+1]。如果找不到,说明数组已是降序,直接反转得到最小排列。 - 在右侧找最小的大于nums[i]的元素:从右向左找到最大的索引
j,使得nums[j] > nums[i]。 - 交换nums[i]和nums[j]:将较大的元素放到前面。
- 反转i+1到末尾:使后缀变为升序(即最小排列),从而得到下一个字典序排列。
完整Java实现
import java.util.Arrays;
/**
* 下一个排列算法 - 字典序排列生成
* 时间复杂度: O(n) 空间复杂度: O(1)
*/
public class NextPermutation {
public void nextPermutation(int[] nums) {
if (nums == null || nums.length <= 1) {
return;
}
int n = nums.length;
// 步骤1: 从右向左找第一个升序对 nums[i] < nums[i+1]
int i = n - 2;
while (i >= 0 && nums[i] >= nums[i + 1]) {
i--;
}
// 如果找到了这样的i
if (i >= 0) {
// 步骤2: 从右向左找第一个大于nums[i]的元素
int j = n - 1;
while (j >= 0 && nums[j] <= nums[i]) {
j--;
}
// 步骤3: 交换
swap(nums, i, j);
}
// 步骤4: 反转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--;
}
}
/**
* 生成从当前排列开始的所有后续排列
*/
public void printAllPermutations(int[] nums) {
int[] current = nums.clone();
int count = 0;
do {
System.out.println("排列 " + (++count) + ": " + Arrays.toString(current));
nextPermutation(current);
} while (!Arrays.equals(current, nums));
System.out.println("总排列数: " + count);
}
/**
* 第k个排列 - 结合阶乘分解直接定位
* 避免生成前面k-1个排列
*/
public String getPermutation(int n, int k) {
// 预计算阶乘
int[] factorial = new int[n + 1];
factorial[0] = 1;
for (int i = 1; i <= n; i++) {
factorial[i] = factorial[i - 1] * i;
}
// 可用数字列表
List<Integer> numbers = new ArrayList<>();
for (int i = 1; i <= n; i++) {
numbers.add(i);
}
k--; // 转换为0-based索引
StringBuilder result = new StringBuilder();
for (int i = n - 1; i >= 0; i--) {
int index = k / factorial[i];
k %= factorial[i];
result.append(numbers.get(index));
numbers.remove(index);
}
return result.toString();
}
/**
* 上一个排列 - 与下一个排列对称
*/
public void prevPermutation(int[] nums) {
int n = nums.length;
int i = n - 2;
while (i >= 0 && nums[i] <= nums[i + 1]) {
i--;
}
if (i >= 0) {
int j = n - 1;
while (j >= 0 && nums[j] >= nums[i]) {
j--;
}
swap(nums, i, j);
}
reverse(nums, i + 1, n - 1);
}
public static void main(String[] args) {
NextPermutation solution = new NextPermutation();
// 测试用例1: 基本下一个排列
int[] nums1 = {1, 2, 3};
System.out.println("原排列: " + Arrays.toString(nums1));
solution.nextPermutation(nums1);
System.out.println("下一个: " + Arrays.toString(nums1));
// 测试用例2: 需要多处调整
int[] nums2 = {1, 5, 8, 4, 7, 6, 5, 3, 1};
System.out.println("\n原排列: " + Arrays.toString(nums2));
solution.nextPermutation(nums2);
System.out.println("下一个: " + Arrays.toString(nums2));
// 测试用例3: 已是最大排列
int[] nums3 = {3, 2, 1};
System.out.println("\n原排列: " + Arrays.toString(nums3));
solution.nextPermutation(nums3);
System.out.println("下一个(最小): " + Arrays.toString(nums3));
// 测试用例4: 生成所有排列
System.out.println("\n生成 [1,2,3] 的所有排列:");
solution.printAllPermutations(new int[]{1, 2, 3});
// 测试用例5: 第k个排列
System.out.println("\n第9个排列(3! = 6个, 取第3个): " + solution.getPermutation(3, 3));
}
}
复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 下一个排列 | O(n) | O(1) | 最多两次扫描 + 一次反转 |
| 第k个排列 | O(n²) | O(n) | 需要维护可用数字列表 |
| 全排列生成 | O(n! × n) | O(1)额外 | n个元素共n!个排列 |
扩展思考
- 第k个排列优化:使用线段树或树状数组维护可用数字,可以将时间优化到O(n log n)。
- 有重复元素的下一个排列:上述算法在有重复元素时仍然正确,但全排列数量会变为n!/(k1!×k2!×…)。
- 循环排列:如果要求首尾相连的最小字典序排列,可以先找到最小元素作为起点,再应用下一个排列。