一文搞懂全排列、组合、子集问题 (2)

回溯的测试是试探性填充,是对每个位置进行单独考虑赋值。而邻里互换的方法虽然是也是递归实现的,但是他是一种基于交换的策略和思路。而理解起来也是非常简单,邻里互换的思路是从左向右进行考虑。

因为序列是没有重复的,我们开始将数组分成两个部分:暂时确定部分未确定部分。开始的时候均是未确定部分,我们需要妥善处理的就是未确定部分。在未确定部分的序列中,我们需要让后面未确定的每一位都有机会处在未确定的首位,所以未确定部分的第一个元素就要和每一个依次进行交换(包括自己),交换完成之后再向下进行递归求解其他的可能性,求解完毕之后要交换回来(还原)再和后面的进行交换。这样当递归进行到最后一层的时候就将数组的值添加到结果集中。如果不理解可以参考下图进行理解:

邻里互换部分过程

实现代码为:

class Solution { public List<List<Integer>> permute(int[] nums) { List<List<Integer>>list=new ArrayList<List<Integer>>(); arrange(nums,0,nums.length-1,list); return list; } private void arrange(int[] nums, int start, int end, List<List<Integer>> list) { if(start==end)//到最后一个 添加到结果中 { List<Integer>list2=new ArrayList<Integer>(); for(int a:nums) { list2.add(a); } list.add(list2); } for(int i=start;i<=end;i++)//未确定部分开始交换 { swap(nums,i,start); arrange(nums, start+1, end, list); swap(nums, i, start);//还原 } } private void swap(int[] nums, int i, int j) { int team=nums[i]; nums[i]=nums[j]; nums[j]=team; } }

那么邻里互换和回溯求解的全排列有什么区别呢?首先回溯法求得的全排列如果这个序列有序得到的结果是字典序的,因为其策略是填充,先小后大有序,而邻里互换没有这个特征。其次邻里互换在这种情况下的效率要高于回溯算法的,虽然量级差不多但是回溯算法需要维护一个集合频繁增删等占用一定的资源。

有重复序列的全排列

有重复对应的是力扣第47题 ,题目描述为:

给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。

示例 1:

输入:nums = [1,1,2] 输出: [[1,1,2], [1,2,1], [2,1,1]]

示例 2:

输入:nums = [1,2,3] 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

提示:

1 <= nums.length <= 8
-10 <= nums[i] <= 10

这个和上面不重复的全排列略有不同,这个输入数组中可能包含重复的序列,我们怎么样采取合适的策略去重复才是至关重要的。我们同样针对回溯和邻里互换两种方法进行分析。

回溯剪枝法
因为回溯完整的比直接递归慢,所以刚开始并没有考虑使用回溯算法,但是这里用回溯剪枝相比递归邻里互换方法更好一些,对于不使用哈希去重的方法,首先进行排序预处理是没有悬念的,而回溯法去重的关键就是避免相同的数字因为相对次序问题造成重复,所以在这里相同数字在使用上相对位置必须不变,而具体剪枝条的规则如下:

先对序列进行排序

试探性将数据放到当前位置

如果当前位置数字已经被使用,那么不可使用

如果当前数字和前一个相等但是前一个没有被使用,那么当前不能使用,需要使用前一个数字。

回溯选取策略

思路很简单,实现起来也很简单:

List<List<Integer>> list; public List<List<Integer>> permuteUnique(int[] nums) { list=new ArrayList<List<Integer>>(); List<Integer> team=new ArrayList<Integer>(); boolean jud[]=new boolean[nums.length]; Arrays.sort(nums); dfs(jud, nums, team, 0); return list; } private void dfs(boolean[] jud, int[] nums, List<Integer> team, int index) { // TODO Auto-generated method stub int len = nums.length; if (index == len)// 停止 { list.add(new ArrayList<Integer>(team)); } else for (int i = 0; i < len; i++) { if (jud[i]||(i>0&&nums[i]==nums[i-1]&&!jud[i-1])) //当前数字被用过 或者前一个相等的还没用,当前即不可用 continue; team.add(nums[i]); jud[i]=true; dfs(jud, nums, team, index + 1); jud[i] = false;// 还原 team.remove(index); } }

邻里互换法

我们在执行递归全排列的时候,主要考的是要把重复的情况搞下去,邻里互换又要怎么去重呢?

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpwzwy.html