全排列问题一文详解
全排列问题是一个经典问题,不仅是组合数学的重要内容,在比如密码学的密钥生成、任务调度的方案枚举、数据的组合分析等场景下,也发挥着关键作用。本文我将主要探讨全排列问题的各种求解算法、优化技巧以及相关的变体问题,结合Java代码实现,带你全面掌握这一重要算法知识。
一、全排列基础问题
1.1 问题描述
给定一个不含重复数字的整数数组nums
,返回其所有可能的全排列。例如,输入nums = [1,2,3]
,则输出应为[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
,即数组元素所有不同顺序的排列组合。
1.2 回溯算法求解
1.2.1 解题思路
回溯算法是解决全排列问题的常用方法,其核心思想是通过深度优先搜索(DFS)的方式,系统地探索所有可能的排列组合。在搜索过程中,使用一个布尔数组used
来标记每个数字是否已经在当前排列中使用过,避免重复选择。每次选择一个未使用的数字加入当前排列,然后递归地继续生成下一个位置的数字,当当前排列的长度达到数组长度时,将该排列加入结果列表中。如果当前排列不符合要求(例如已经使用过某个数字),则回溯到上一步,尝试其他可能的选择。
1.2.2 Java代码实现
import java.util.ArrayList;
import java.util.List;
public class Permutations {
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
boolean[] used = new boolean[nums.length];
List<Integer> current = new ArrayList<>();
dfs(nums, used, current, result);
return result;
}
private void dfs(int[] nums, boolean[] used, List<Integer> current, List<List<Integer>> result) {
if (current.size() == nums.length) {
result.add(new ArrayList<>(current));
return;
}
for (int i = 0; i < nums.length; i++) {
if (!used[i]) {
used[i] = true;
current.add(nums[i]);
dfs(nums, used, current, result);
current.remove(current.size() - 1);
used[i] = false;
}
}
}
public static void main(String[] args) {
Permutations solution = new Permutations();
int[] nums = {1, 2, 3};
List<List<Integer>> permutations = solution.permute(nums);
for (List<Integer> permutation : permutations) {
System.out.println(permutation);
}
}
}
1.2.3 复杂度分析
- 时间复杂度:对于每个位置,都有
n
种选择(n
为数组长度),总共需要确定n
个位置的数字,所以时间复杂度为 O ( n × n ! ) O(n \times n!) O(n×n!)。其中, n ! n! n!是全排列的总数,每次生成一个排列需要 O ( n ) O(n) O(n)的时间来复制当前排列到结果列表中。 - 空间复杂度:除了存储结果的列表外,递归调用栈的最大深度为
n
,用于标记数字使用情况的布尔数组长度也为n
,所以空间复杂度为 O ( n ) O(n) O(n) 。
1.3 字典序算法求解
1.3.1 解题思路
字典序算法是一种基于数学规律生成全排列的方法。它的核心在于找到当前排列的下一个字典序更大的排列。具体步骤如下:
- 从后向前扫描数组,找到第一个顺序对
(i, i + 1)
,使得nums[i] < nums[i + 1]
,此时i
为需要调整的位置。 - 如果没有找到这样的顺序对,说明当前排列已经是最大的字典序排列,即全排列已经生成完毕。
- 从
i + 1
到数组末尾,找到大于nums[i]
的最小元素nums[j]
。 - 交换
nums[i]
和nums[j]
。 - 反转
i + 1
到数组末尾的子数组,使其变为字典序最小的排列。 - 重复上述步骤,直到生成所有的全排列。
1.3.2 Java代码实现
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class PermutationsLexicographic {
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
Arrays.sort(nums);
result.add(toList(nums));
while (nextPermutation(nums)) {
result.add(toList(nums));
}
return result;
}
private boolean nextPermutation(int[] nums) {
int i = nums.length - 2;
while (i >= 0 && nums[i] >= nums[i + 1]) {
i--;
}
if (i < 0) {
return false;
}
int j = nums.length - 1;
while (nums[j] <= nums[i]) {
j--;
}
swap(nums, i, j);
reverse(nums, i + 1, nums.length - 1);
return true;
}
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 end) {
while (start < end) {
swap(nums, start, end);
start++;
end--;
}
}
private List<Integer> toList(int[] nums) {
List<Integer> list = new ArrayList<>();
for (int num : nums) {
list.add(num);
}
return list;
}
public static void main(String[] args) {
PermutationsLexicographic solution = new PermutationsLexicographic();
int[] nums = {1, 2, 3};
List<List<Integer>> permutations = solution.permute(nums);
for (List<Integer> permutation : permutations) {
System.out.println(permutation);
}
}
}
1.3.3 复杂度分析
- 时间复杂度:生成一个全排列的时间复杂度为 O ( n ) O(n) O(n),总共需要生成 n ! n! n!个全排列,所以总时间复杂度为 O ( n × n ! ) O(n \times n!) O(n×n!)。
- 空间复杂度:除了存储结果的列表外,只使用了常数级别的额外空间,用于临时交换和反转操作,所以空间复杂度为 O ( 1 ) O(1) O(1)(不考虑结果列表占用的空间)。
二、全排列问题变体
2.1 含重复数字的全排列
2.1.1 问题描述
给定一个可包含重复数字的整数数组nums
,按任意顺序返回它所有不重复的全排列。例如,输入nums = [1,1,2]
,输出应为[[1,1,2],[1,2,1],[2,1,1]]
。
2.1.2 回溯算法改进
解题思路
在基础回溯算法的基础上,增加对重复数字的处理。首先对数组进行排序,使得相同的数字相邻。在回溯过程中,当遇到相同的数字时,如果该数字在当前排列中还未使用过,且与前一个数字相同且前一个数字还未使用过,那么跳过该数字,避免生成重复的排列。
Java代码实现
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class PermutationsWithDuplicates {
public List<List<Integer>> permuteUnique(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
boolean[] used = new boolean[nums.length];
List<Integer> current = new ArrayList<>();
Arrays.sort(nums);
dfs(nums, used, current, result);
return result;
}
private void dfs(int[] nums, boolean[] used, List<Integer> current, List<List<Integer>> result) {
if (current.size() == nums.length) {
result.add(new ArrayList<>(current));
return;
}
for (int i = 0; i < nums.length; i++) {
if (used[i] || (i > 0 && nums[i] == nums[i - 1] &&!used[i - 1])) {
continue;
}
used[i] = true;
current.add(nums[i]);
dfs(nums, used, current, result);
current.remove(current.size() - 1);
used[i] = false;
}
}
public static void main(String[] args) {
PermutationsWithDuplicates solution = new PermutationsWithDuplicates();
int[] nums = {1, 1, 2};
List<List<Integer>> permutations = solution.permuteUnique(nums);
for (List<Integer> permutation : permutations) {
System.out.println(permutation);
}
}
}
2.1.3 复杂度分析
- 时间复杂度:与不含重复数字的全排列类似,时间复杂度为 O ( n × n ! ) O(n \times n!) O(n×n!),但由于需要对数组进行排序(时间复杂度为 O ( n log n ) O(n \log n) O(nlogn)),并且在回溯过程中增加了对重复数字的判断,实际运行时间会稍长。
- 空间复杂度:同样为 O ( n ) O(n) O(n),主要来自递归调用栈和标记数组的空间占用。
2.2 字符串的全排列
2.2.1 问题描述
给定一个字符串s
,返回其所有可能的全排列。例如,输入s = "abc"
,输出应为["abc","acb","bac","bca","cab","cba"]
。
2.2.2 回溯算法实现
解题思路
与整数数组的全排列类似,将字符串转换为字符数组,然后使用回溯算法进行求解。在实现过程中,注意字符数组和字符串之间的转换。
Java代码实现
import java.util.ArrayList;
import java.util.List;
public class StringPermutations {
public List<String> permute(String s) {
List<String> result = new ArrayList<>();
char[] chars = s.toCharArray();
boolean[] used = new boolean[chars.length];
StringBuilder current = new StringBuilder();
dfs(chars, used, current, result);
return result;
}
private void dfs(char[] chars, boolean[] used, StringBuilder current, List<String> result) {
if (current.length() == chars.length) {
result.add(current.toString());
return;
}
for (int i = 0; i < chars.length; i++) {
if (!used[i]) {
used[i] = true;
current.append(chars[i]);
dfs(chars, used, current, result);
current.setLength(current.length() - 1);
used[i] = false;
}
}
}
public static void main(String[] args) {
StringPermutations solution = new StringPermutations();
String s = "abc";
List<String> permutations = solution.permute(s);
for (String permutation : permutations) {
System.out.println(permutation);
}
}
}
2.2.3 复杂度分析
- 时间复杂度:与整数数组全排列相同,为
O
(
n
×
n
!
)
O(n \times n!)
O(n×n!),其中
n
为字符串的长度。 - 空间复杂度:为 O ( n ) O(n) O(n),主要来自递归调用栈、标记数组和临时字符串构建的空间占用。
三、全排列问题的优化与拓展
3.1 剪枝优化
在回溯算法中,可以通过剪枝操作减少不必要的搜索。例如,在生成全排列时,如果已经知道当前部分排列不可能产生符合要求的结果,可以提前终止搜索。在含重复数字的全排列问题中,对重复数字的判断就是一种剪枝操作,避免了大量重复排列的生成,提高了算法效率。
3.2 并行计算
对于大规模数据的全排列问题,由于计算量巨大,可以考虑使用并行计算来加速。利用多线程或分布式计算框架,将全排列的计算任务分配到多个处理器或节点上,同时进行计算,最后将结果合并。这种方式可以显著减少计算时间,但需要注意线程安全和结果合并的正确性。
3.3 与其他算法结合
全排列问题常常与其他算法结合使用,以解决更复杂的实际问题。例如,在旅行商问题(TSP)中,需要找到经过所有城市的最短路径,其中一个解决思路就是生成所有城市的全排列,然后计算每个排列对应的路径长度,找到最短路径。在这种场景下,全排列算法与路径计算算法相结合,共同解决问题。
That’s all, thanks for reading!
觉得有用就点个赞
、收进收藏
夹吧!关注
我,获取更多干货~