Java 全排列的几种实现方法
全排列问题是一个经典的算法问题,指的是对一个序列中的所有元素生成不重复的排列组合。以下是全排列问题在 Java 中的详细实现和讲解。
1. 全排列问题定义
输入: 给定一个序列或数组,找出所有元素的排列。
输出: 返回所有可能的排列。
示例:
输入:[1, 2, 3]
输出:
[1, 2, 3]
[1, 3, 2]
[2, 1, 3]
[2, 3, 1]
[3, 1, 2]
[3, 2, 1]
2. 常用算法
全排列的常见实现方法:
- 回溯法(Backtracking)
- 字典序排列法(Lexicographic Order)
- 迭代法(非递归实现)
3. 使用回溯法解决全排列
回溯法是一种基于递归的搜索算法,它通过不断尝试并撤销之前的选择来生成所有可能的解。
3.1 回溯法实现(基础版)
适用于数组中无重复元素的全排列。
import java.util.ArrayList; import java.util.List; public class Permutations { public static List<List<Integer>> permute(int[] nums) { List<List<Integer>> result = new ArrayList<>(); boolean[] used = new boolean[nums.length]; // 标记是否使用过 List<Integer> current = new ArrayList<>(); backtrack(nums, used, current, result); return result; } private static void backtrack(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]) { // 跳过已使用的元素 continue; } // 做选择 current.add(nums[i]); used[i] = true; // 递归 backtrack(nums, used, current, result); // 撤销选择 current.remove(current.size() - 1); used[i] = false; } } public static void main(String[] args) { int[] nums = {1, 2, 3}; List<List<Integer>> permutations = permute(nums); System.out.println(permutations); } }
输出结果
对于输入 [1, 2, 3]
:
[[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]
3.2 回溯法(不使用标记数组)
通过交换数组中的元素,可以避免使用标记数组。
import java.util.ArrayList; import java.util.List; public class PermutationsSwap { public static List<List<Integer>> permute(int[] nums) { List<List<Integer>> result = new ArrayList<>(); backtrack(nums, 0, result); return result; } private static void backtrack(int[] nums, int start, List<List<Integer>> result) { if (start == nums.length) { List<Integer> permutation = new ArrayList<>(); for (int num : nums) { permutation.add(num); } result.add(permutation); return; } for (int i = start; i < nums.length; i++) { swap(nums, start, i); // 交换元素 backtrack(nums, start + 1, result); swap(nums, start, i); // 撤销交换 } } private static void swap(int[] nums, int i, int j) { int temp = nums[i]; nums[i] = nums[j]; nums[j] = temp; } public static void main(String[] args) { int[] nums = {1, 2, 3}; List<List<Integer>> permutations = permute(nums); System.out.println(permutations); } }
3.3 回溯法处理重复元素
如果数组中包含重复元素(例如 [1, 1, 2]
),我们需要对结果去重。可以通过对数组排序并在递归时跳过重复元素来实现。
import java.util.ArrayList; import java.util.Arrays; import java.util.List; public class PermutationsWithDuplicates { public static List<List<Integer>> permuteUnique(int[] nums) { List<List<Integer>> result = new ArrayList<>(); Arrays.sort(nums); // 排序以便跳过重复元素 boolean[] used = new boolean[nums.length]; backtrack(nums, used, new ArrayList<>(), result); return result; } private static void backtrack(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]) { continue; } // 跳过相邻重复的元素 if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) { continue; } current.add(nums[i]); used[i] = true; backtrack(nums, used, current, result); current.remove(current.size() - 1); used[i] = false; } } public static void main(String[] args) { int[] nums = {1, 1, 2}; List<List<Integer>> permutations = permuteUnique(nums); System.out.println(permutations); } }
输出结果
对于输入[1, 1, 2]
:
[[1, 1, 2], [1, 2, 1], [2, 1, 1]]
4. 字典序排列法
字典序法生成全排列的步骤:
- 找到当前排列的“最后一个升序对”。
- 从右侧找到比升序对中较小值大的最小值,交换这两个值。
- 将右侧部分反转。
适用于需要按字典序输出排列的情况。
import java.util.Arrays; public class PermutationsLexicographic { public static void nextPermutation(int[] nums) { int i = nums.length - 2; while (i >= 0 && nums[i] >= nums[i + 1]) { // 找到升序对 i--; } if (i >= 0) { int j = nums.length - 1; while (j >= 0 && nums[j] <= nums[i]) { j--; } swap(nums, i, j); // 交换 } reverse(nums, i + 1); // 反转后面的部分 } private static void reverse(int[] nums, int start) { int i = start, j = nums.length - 1; while (i < j) { swap(nums, i, j); i++; j--; } } private static void swap(int[] nums, int i, int j) { int temp = nums[i]; nums[i] = nums[j]; nums[j] = temp; } public static void main(String[] args) { int[] nums = {1, 2, 3}; System.out.println(Arrays.toString(nums)); for (int i = 0; i < 6; i++) { nextPermutation(nums); System.out.println(Arrays.toString(nums)); } } }
5. 总结
常见方法对比
方法 | 适用场景 | 特点 |
---|---|---|
回溯法 | 全排列(通用) | 最常用,适合生成所有排列,可以处理重复元素,通过递归和剪枝实现。 |
交换法 | 无重复全排列 | 不需要额外空间标记数组,通过交换实现排列,但对重复元素需要额外处理。 |
字典序排列法 | 字典序输出排列 | 按字典序生成排列,适合序列化输出;需要对输入序列进行排序作为初始状态。 |
迭代法 | 生成排列的特定场景 | 使用循环代替递归,但实现起来较为复杂,适合排列生成问题中的迭代优化。 |
性能分析
- 时间复杂度: O ( n ! ) O(n!) O(n!),每种方法
的时间复杂度都与排列数量成正比。 - 空间复杂度:
- 使用标记数组的回溯法: O ( n ) O(n) O(n)
- 使用交换法: O ( 1 ) O(1) O(1)(不包括递归栈)
全排列是算法中基础而重要的问题。回溯法是最常用的解决方式,而在实际开发中,根据不同的需求可以选择合适的方法来实现高效的排列生成。
到此这篇关于Java 全排列的几种实现方法的文章就介绍到这了,更多相关Java 全排列内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
Spring中ApplicationListener的使用解析
这篇文章主要介绍了Spring中ApplicationListener的使用解析,ApplicationContext事件机制是观察者设计模式的实现,通过ApplicationEvent类和ApplicationListener接口,需要的朋友可以参考下2023-12-12详解Java如何在CompletableFuture中实现日志记录
这篇文章主要为大家详细介绍了一种slf4j自带的MDC类,来记录完整的请求日志,和在CompletableFuture异步线程中如何保留链路id,需要的可以参考一下2023-04-04Mybatis如何使用@Mapper和@MapperScan注解实现映射关系
这篇文章主要介绍了Mybatis使用@Mapper和@MapperScan注解实现映射关系,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教2021-10-10
最新评论