当前位置: 首页 > news >正文

全排列问题一文详解

全排列问题一文详解

    • 一、全排列基础问题
      • 1.1 问题描述
      • 1.2 回溯算法求解
        • 1.2.1 解题思路
        • 1.2.2 Java代码实现
        • 1.2.3 复杂度分析
      • 1.3 字典序算法求解
        • 1.3.1 解题思路
        • 1.3.2 Java代码实现
        • 1.3.3 复杂度分析
    • 二、全排列问题变体
      • 2.1 含重复数字的全排列
        • 2.1.1 问题描述
        • 2.1.2 回溯算法改进
        • 解题思路
        • Java代码实现
        • 2.1.3 复杂度分析
      • 2.2 字符串的全排列
        • 2.2.1 问题描述
        • 2.2.2 回溯算法实现
        • 解题思路
        • Java代码实现
        • 2.2.3 复杂度分析
    • 三、全排列问题的优化与拓展
      • 3.1 剪枝优化
      • 3.2 并行计算
      • 3.3 与其他算法结合

全排列问题是一个经典问题,不仅是组合数学的重要内容,在比如密码学的密钥生成、任务调度的方案枚举、数据的组合分析等场景下,也发挥着关键作用。本文我将主要探讨全排列问题的各种求解算法、优化技巧以及相关的变体问题,结合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 解题思路

字典序算法是一种基于数学规律生成全排列的方法。它的核心在于找到当前排列的下一个字典序更大的排列。具体步骤如下:

  1. 从后向前扫描数组,找到第一个顺序对(i, i + 1),使得nums[i] < nums[i + 1],此时i为需要调整的位置。
  2. 如果没有找到这样的顺序对,说明当前排列已经是最大的字典序排列,即全排列已经生成完毕。
  3. i + 1到数组末尾,找到大于nums[i]的最小元素nums[j]
  4. 交换nums[i]nums[j]
  5. 反转i + 1到数组末尾的子数组,使其变为字典序最小的排列。
  6. 重复上述步骤,直到生成所有的全排列。
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!
觉得有用就点个赞、收进收藏夹吧!关注我,获取更多干货~

相关文章:

  • 几种大功率远距传输WiFI解决方案
  • 1、使用STM32CubeMX在项目中添加FreeRTOS源码
  • 9. TypeScript 泛型
  • 【CiteSpace】引文可视化分析软件CiteSpace下载与安装
  • FPGA基础 -- Verilog 锁存器简介
  • 模拟/思维
  • 新发布的一款使用ReactNative新架构加载Svga动画的开源插件[android/ios]
  • RA信号处理
  • 生成https免费证书并绑定到nginx
  • 嵌入式之硬件学习(三)通信方式、串口通信
  • RK3568笔记八十四:rtmp转rtmp直播流测试
  • 亚矩阵云手机+Whatnot:直播电商的自动化增长引擎
  • iOS多端兼容性调试:一次iOS性能优化分工具协作排查过程
  • 上线iOSApp前抓包工具协作保障接口行为一致性(iOS抓包)
  • spring-webmvc @InitBinder 典型用法
  • 《前端面试题:数组操作》
  • PID控制电机
  • springboot入门之路(二)
  • 技术赋能教师专业发展:从理论到实践的深度剖析
  • createInnerAudioContext播放不完整?
  • 收费网站建设视频教程免费下载/app有哪些推广方式
  • 响应式网站 图片处理/新闻热点大事件
  • 网站建设的功能定位/廊坊seo排名优化
  • 帮别人做设计图的网站/2021年新闻摘抄
  • 网站左侧导航代码/网站建设多少钱
  • 做网站有个名字叫小廖/我想学做互联网怎么入手