最全排列组合算法详解以及套路总结一文突破
项目github地址:bitcarmanlee easy-algorithm-interview-and-practice
经常有同学私信或留言询问相关问题,V号bitcarmanlee。github上star的同学,在我能力与时间允许范围内,尽可能帮大家解答相关问题,一起进步。
1.排列组合问题
排列组合是经典的算法问题,相关的内容中学阶段就学习过。在讲算法实现之前,我们先简单复习一下排列组合的相关定义。
排列,英文名称为Permutation,简称P。假设有一个数组{1, 2, 3, 4, 5},我们需要将数组中的所有元素进行排序,那么第一个位置,我们可以选择五个数字的任何一个,共有5种选择。第二个位置,可以选择剩余四个数字的任何一个,共有4种选择。第三个位置,可以选择剩余三个数字中的任何一个,共有3种选择。第四个位置,可以选择剩余两个数字中的任何一个,共有2种选择。最后一个位置,因为只剩一个数字,没得选择,所有只有一种选择。那么该数组总共的排列个数为5∗4∗3∗2∗1=1205*4*3*2*1=1205∗4∗3∗2∗1=120种。
如果数组的元素不重复,元素个数为N,按照上面的推导,容易得出该数组的全排列个数为N!N!N!,即P(N)=N!P(N) = N!P(N)=N!
很多时候我们不做全排列,比如5个元素,我们只需要取3个进行排序,按照前面的分析,很容易得知排列的个数为5∗4∗3=605*4*3=605∗4∗3=60种,后面的2∗12*12∗1两种情况被舍弃掉了。因此,从N个元素中选择k个做排列,公式也很容易写出来:P(N,k)=N!(N−k)!P(N, k) = \frac{N!}{(N-k)!}P(N,k)=(N−k)!N!
组合,英文名为Combination,简称C。假设同样是数组{1, 2, 3, 4, 5},我们需要从数组中选择任意3个元素,那么有多少种方式呢?
根据前面的推导,我们能够得知,如果从5个元素中选择3个元素,排列的方式有P(5,3)=5!(5−3)!=60P(5, 3) = \frac{5!}{(5-3)!} = 60P(5,3)=(5−3)!5!=60种。但是组合的时候,对顺序是不敏感的,比如我们选1,2,3与选1,3,2,虽然是两种排列方式,但是在组合里是一种情况。3个元素的全排列一共有3!=63!=63!=6种,所以组合的公式为C(N,K)=N!(N−k)!k!C(N,K) =\frac{N!}{(N-k)!k!}C(N,K)=(N−k)!k!N!
同时有二项式定理:
C(n,0)+C(n,1)+C(n,2)+⋯+C(n,n)=2nC(n, 0) + C(n, 1) + C(n, 2) + \cdots + C(n, n) = 2^nC(n,0)+C(n,1)+C(n,2)+⋯+C(n,n)=2n
2.所有子集
首先我们看看求所有子集的情况:假设现在数组有三个不重复的元素{1, 2, 3},求该数组所有的子集。
根据二项式定理,我们不难得出该数组所有的子集个数为C(3,0)+C(3,1)+C(3,2)+C(3,3)=23=8C(3, 0) + C(3, 1) + C(3, 2) + C(3, 3) = 2^3 = 8C(3,0)+C(3,1)+C(3,2)+C(3,3)=23=8
二话不说,先上代码,后面再分析具体思路。
import org.apache.commons.lang3.StringUtils;import java.util.ArrayList;public class SubSet {public static int[] nums = {1, 2, 3};public static ArrayList<ArrayList<Integer>> result = new ArrayList<>();public static void subset(ArrayList<Integer> inner, int start) {for(int i=start; i<nums.length; i++) {inner.add(nums[i]);result.add(new ArrayList<>(inner));subset(inner, i+1);inner.remove(inner.size()-1);}}public static void main(String[] args) {ArrayList<Integer> inner = new ArrayList<>();result.add(inner);subset(inner, 0);for(ArrayList<Integer> each: result) {System.out.println(StringUtils.join(each, ","));}}
}
上面代码的输出为
1
1,2
1,2,3
1,3
2
2,3
3
刚好8种情况,而且看输出结果,符合我们的预期。
上面的解法,是经典的回溯解法。分析一下具体的思路:
首先思考我们如何凑出{1}, {1,2}, {1,2,3}这三个子集?从index=0开始遍历,此时将元素1加入inner中,并将inner加入result中,这样就将{1}这个子集加入了结果。接下来递归调用subset方法,只是将index变成0+1=1,这个时候inner将2加上,变成{1,2},同时将inner加入result中,这样就将{1,2}这个子集加入结果。以此类推,下一次递归调用将{1,2,3}加入结果。
主要来分析一下怎么从{1,2,3}这个子集回溯得到{1,3}:
得到{1,2,3}这个子集以后,此时递归调用subset(inner, 3),不满足for循环中i<nums.length的条件,该次调用结束。此时返回start=2时的压栈现场,先执行inner.remove(inner.size()-1);这一句,会将此时inner的最后一个元素3删除,此时inner为{1, 2}。然后,会再返回start=1时的压栈现场,这时会删除inner中的最后一个元素2,此时inner只剩最后一个元素1。初始start=0时,for循环内调用subset(1)已经全部结束,开始执行for循环内subset(2),会添加上元素3,inner变成{1,3}。以此类推,最终会得到所有的子集。
上面的分析过程,实际上就是代码中函数不停压栈然后回溯调用的过程。建议同学们可以实际debug一下,看一看代码运行过程,会理解得更加深刻。
3.从n个元素中选择k个组合
第二部分是求所有的子集,如果我们限定子集元素的个数,即从n个元素中选择k个元素组合,就是常见的C(n,k)C(n, k)C(n,k)问题。
解法思路基本与上面相同,先看代码。
import org.apache.commons.lang3.StringUtils;import java.util.ArrayList;public class SelectK {public static int[] nums = {1, 2, 3};public static ArrayList<ArrayList<Integer>> result = new ArrayList<>();public static void select(ArrayList<Integer> inner, int start, int k) {for(int i=start; i<nums.length; i++) {inner.add(nums[i]);if (inner.size() == k) {result.add(new ArrayList<>(inner));inner.remove(inner.size()-1);continue;}select(inner, i+1, k);inner.remove(inner.size()-1);}}public static void main(String[] args) {ArrayList<Integer> inner = new ArrayList<>();int k = 2;select(inner, 0, k);for(ArrayList<Integer> each: result) {System.out.println(StringUtils.join(each, ","));}}
}
结果为:
1,2
1,3
2,3
与求所有子集不一样的地方在于,只有当inner中的元素个数为k时,才将inner添加到result中。同时,添加完毕以后,先将最后一个元素删除,然后就可以直接continue,结束本次循环。
4.n个元素的全排列
按照我们之前的分析,n个不重复的元素,全排列的情况总共为n!n!n!种。假设数组{1, 2, 3},那么全排列一共有6种情况。
还是先上代码
import org.apache.commons.lang3.StringUtils;import java.util.ArrayList;public class PermutationN {public static int[] nums = {1, 2, 3};public static ArrayList<ArrayList<Integer>> result = new ArrayList<>();public static void permuation(ArrayList<Integer> inner) {if (inner.size() == nums.length) {result.add(new ArrayList<>(inner));return;}for(int i=0; i<nums.length; i++) {if (inner.contains(nums[i])) {continue;}inner.add(nums[i]);permuation(inner);inner.remove(inner.size()-1);}}public static void main(String[] args) {ArrayList<Integer> inner = new ArrayList<>();permuation(inner);for(ArrayList<Integer> each: result) {System.out.println(StringUtils.join(each, ","));}}
}
同样来分析一下代码的思路:
1.如果inner的size满足条件,加入到result中,并返回。
2.从第一个元素开始循环:
2.1 如果该元素在inner中,表示该元素已经被访问,本次循环continue。
2.2 如果元素不在inner中,加入inner。
2.3 递归调用permuation方法。
2.4 本次permuation方法调用结束以后,删除掉inner中的最后一个元素。
思路是不是还算比较清晰。同样的,如果看上去有点晕,也建议大家去IDE中debug,观察一下函数递归调用的整个流程。
inner.contains(nums[i]) 这一行起的作用,是判断该元素有没有被访问,实际中更常见的另外一种写法是用一个visit数组来记录元素被访问的情况,下面我们采用visit数组的写法来表示一下。
import org.apache.commons.lang3.StringUtils;import java.util.ArrayList;public class PermutationN {public static int[] nums = {1, 2, 3};public static ArrayList<ArrayList<Integer>> result = new ArrayList<>();public static boolean[] visit = new boolean[nums.length];public static void permuation(ArrayList<Integer> inner, boolean[] visit) {if (inner.size() == nums.length) {result.add(new ArrayList<>(inner));return;}for(int i=0; i<nums.length; i++) {if (visit[i]) {continue;}visit[i] = true;inner.add(nums[i]);permuation(inner, visit);inner.remove(inner.size()-1);visit[i] = false;}}public static void main(String[] args) {ArrayList<Integer> inner = new ArrayList<>();permuation(inner, visit);for(ArrayList<Integer> each: result) {System.out.println(StringUtils.join(each, ","));}}
}
用visit数组标记该元素是否被访问,与之前的版本相比,多了两个步骤:
1.该元素被访问,visit数组该位置被置为true;
2.递归回溯的时候,visit数组该位置被置为false。
5.n个有重复元素的全排列
上面的全排列例子,数组中没有重复元素。如果某个数组中的元素有重复,比如该数组,{1, 1, 2},要求该数组的全排列,该怎么办?
话不多说,还是先上代码。
import org.apache.commons.lang3.StringUtils;import java.util.ArrayList;
import java.util.Arrays;public class PermutationDuplicate {public static int[] nums = {1, 2, 1};public static ArrayList<ArrayList<Integer>> result = new ArrayList<>();public static boolean[] visit = new boolean[nums.length];public static void permuation(ArrayList<Integer> inner, boolean[] visit) {if (inner.size() == nums.length) {result.add(new ArrayList<>(inner));return;}for(int i=0; i<nums.length; i++) {if (visit[i]) {continue;}if (i > 0 && nums[i] == nums[i-1] && !visit[i-1]) {continue;}inner.add(nums[i]);visit[i] = true;permuation(inner, visit);inner.remove(inner.size()-1);visit[i] = false;}}public static void main(String[] args) {Arrays.sort(nums);ArrayList<Integer> inner = new ArrayList<>();permuation(inner, visit);for(ArrayList<Integer> each: result) {System.out.println(StringUtils.join(each, ","));}}
}
重点看与上面不同的地方:
1.要先对数组进行排序,保证有序。
2.
if (i > 0 && nums[i] == nums[i-1] && !visit[i-1]) {continue;}
这个条件可以这么理解:
当第一个排列1,1,2记录完毕以后,后续会再产生一个1,1,2的排列。第二个1,1,2的排列,是第二个1先被访问,第一个1再被访问。此时第一个1的visit标志为false,所以这种情况下,本次循环也直接continue即可,不加入结果中!
6.套路总结
上面各个case挨个解完以后,下面我们来总结一下排列组合问题的套路。
先看排列问题:
result = []
def permutation(路径, 选择列表):if 满足结束条件:result.add(路径)returnfor 选择 in 选择列表:做选择permutation(路径, 选择列表)撤销选择
针对不同的情况,需要确认的就两点:结束条件与如何选择。
上面的流程,本质是个标准的回溯法。
再看看组合问题
result = []
def permutation(路径, 选择列表):for 选择 in 选择列表:做选择permutation(路径, 选择列表)撤销选择
组合的套路,本质也是回溯法的使用。与排列稍微不一样的地方在于,组合问题的结束条件可以不用写出来,等循环结果即可。
最全排列组合算法详解以及套路总结一文突破相关推荐
- 回溯算法详解之全排列、N皇后问题
回溯算法详解 回溯算法框架.解决一个回溯问题,实际上就是一个决策树的遍历过程.你只需要思考 3 个问题: 1.路径:也就是已经做出的选择. 2.选择列表:也就是你当前可以做的选择. 3.结束条件:也就 ...
- 【机器学习】集成学习及算法详解
集成学习及算法详解 前言 一.随机森林算法原理 二.随机森林的优势与特征重要性指标 1.随机森林的优势 2.特征重要性指标 三.提升算法概述 四.堆叠模型简述 五.硬投票和软投票 1.概念介绍 2.硬 ...
- 目标检测 RCNN算法详解
原文:http://blog.csdn.net/shenxiaolu1984/article/details/51066975 [目标检测]RCNN算法详解 Girshick, Ross, et al ...
- Twitter-Snowflake,64位自增ID算法详解
Twitter-Snowflake,64位自增ID算法详解 from: http://www.lanindex.com/twitter-snowflake%EF%BC%8C64%E4%BD%8D%E8 ...
- AdBoost算法详解
AdBoost算法详解 1 算法简介 1.2AdaBoost特点 1.3Bagging与AdaBoost区别 2AdaBoost算法步骤 3 AdaBoost的数学定义 4 推广到多分类 算法引入: ...
- YOLOv5算法详解
目录 1.需求解读 2.YOLOv5算法简介 3.YOLOv5算法详解 3.1 YOLOv5网络架构 3.2 YOLOv5实现细节详解 3.2.1 YOLOv5基础组件 3.2.2 输入端细节详解 3 ...
- [Network Architecture]DPN(Dual Path Network)算法详解(转)
https://blog.csdn.net/u014380165/article/details/75676216 论文:Dual Path Networks 论文链接:https://arxiv.o ...
- 多重背包O(N*V)算法详解(——使用单调队列)
多重背包O(N*V)算法详解(--使用单调队列) 多重背包问题: 有N种物品和容量为V的背包,若第i种物品,容量为v[i],价值为w[i],共有n[i]件.怎样装才能使背包内的物品总价值最大? 网上关 ...
- NEAT(NeuroEvolution of Augmenting Topologies)算法详解与实践(基于NEAT-Python)
NEAT(NeuroEvolution of Augmenting Topologies)算法详解与实践(基于NEAT-Python) NEAT算法详解 NEAT算法概述 NEAT编码方案 结构突变 ...
最新文章
- Oracle truncate、 delete、 drop区别
- 深入理解委托——为什么C#要引入委托
- 第2周项目1c++语言中函数参数传递的三种方式
- SAP S4HANA里关于生产订单的一些重要数据库表
- java nio doug_深入的聊聊 Java NIO
- 夜,思考——我想要的到底是什么?
- c语言数组指针题库,C语言 数组指针练习题.doc
- 藏文印刷体: 乌金体,又称有头体
- Acrobat专业版破解补丁AMTEmu+Win+v0.9.2
- plecs使用C-Script模块实现线性插值算法
- C/C++编码:无锁编程
- 端口渗透——21端口FTP
- python基础入门(变量)
- js删除网页中图片width 和 height
- 云南鲁甸县附近发生6.5级地震 震源深度12千米
- Mars3D平台介绍
- 兰州理工大学计算机与通信学院挑战杯课外学术科技作品,我校开展第十三届“挑战杯”甘肃省大学生课外学术科技作品竞赛校级成果展览暨省级推荐作品路演活动...
- GCN论文笔记——HopGAT: Hop-aware Supervision Graph Attention Networks for Sparsely Labeled Graphs
- 如何设置路由器连上电信宽带
- PHP Web实时消息后台服务器推送技术---GoEasy