最近程序里面需要一个组合的计算,于是写了一个组合类,没有实现排列,因为只要组合,所以只实现了组合,这里我们思想记录下来.
本文里约定 C(n,m)即从n个中选m个的组合,其中n>=m,m>=0,n>0
平时我们数据里学的组合一般都是问多少种情况比哪C(3,1)有几种情况,答案是3,不是我们排列出来的情况.而我这次写的主要是排列出组合情况,比如:C(3,1),输出结果
1,0,0
0,1,0
0,0,1
这样子的结果,当然结果我能输出来,有多少种情况我也能输出来,下面是思路
1.刚开始的时候,我会想到用动态规划,以前做过n个字符全排,就是变成n-1个字符全排列加上最后一个字符的各种位置排列.但是这是组合,我目前知道的组合公式就是
C(n,m)=n!/(m!(n-m)!)
我在这个公式中找不到一步一步递的算法.
2.在网上查询组合公式查到一个如下公式
C(n,m)=C(n-1,m-1)+C(n-1,m)
这个公式就有了递归关系了.
即F(n,m)=F(n-1,m-1)+F(n-1,m)
很大的一个问题算解决了,因为找到公式了.
3.但是又一个比较困难的问题来了,怎么存储我的组合的结.可能上面公式还是有倾向于计算数量,但是我现在要的是各种排列.
这个看对上面公式的理解了,如果只看到公式不理解,也能只编写出计算数量的方法,写不出组合的结果.
下面看一下对公式的理解,为什么C(n,m)就等于C(n-1,m-1)加C(n-1,m),先看一下他们分别的意思
C(n,m)从n个在取m个的组合情况
C(n-1,m-1)从n-1个中取m-1个的组合情况,也就是从n-1个中选m-1个,另一个确定
C(n-1,m)从n-1中取m个的组合情况,也就是都从n-1个中先
那么翻译一下就是
从n个中选m个的组合等于 某个选定,某个不选的情况,
某个选定即还要在n-1个中选m-1个
某个没有选即要在n-1个中选m个
所以
C(n-1,m-1)+C(n-1,m)
理解了上面公式,我们就好设计存储结果构了,这都是分两种情况,一种某个选定,一种某个没选,后面组给也这些继续分下去.比如:
C(5,2)=C(4,2)+C(4,1)
=C(3,2)+C(3,1)+C(4,1)
=C(2,2)+C(2,1)+C(3,1)+C(4,1) //其实也可以到这种结构算最后了因为C(x,1)也是很好计算的
=C(2,2)+C(1,1)+C(1,0)+C(2,1)+C(2,0)+C(3,1)+C(3,0)
=.... 最后全是C(x,x) C(x,0) 这种结构 我代码里是以这种结构计算的
我最后是以树的结构来构造这种结果的,比如从根结点分两种情况0,1,后面又继续分,因为每次都是两种情况,所以是二叉树,但是不是平衡的.比哪C(3,1)中分成 1,0,0和0,1,0和0,0,1左枝表示1,右枝表示0.
4.最后来看一下核心的方法
private void GetCnm(Node node, int n, int m) { if (n < 0 || m < 0) throw new Exception("n,m的值必须大于零"); if (n < m) throw new Exception("n的值必须大于等于m的值"); //最后分解后都是 Cn0 或 者Cnn if (n == m) { ReplicateNode(node, n, LEFT); } else if (m == 0) { ReplicateNode(node, n, RIGHT); } else { Node left = new Node() { Data = LEFT }; Node right = new Node() { Data = RIGHT }; node.Left = left; left.Parent = node; node.Right = right; right.Parent = node; GetCnm(node.Left, n - 1, m - 1); GetCnm(node.Right, n - 1, m); } }
递归当中比较重要的就是找到递归的出口条件,这个方法中就是n==m和m==0的情况,其实上面也说了可以用n==m和m=1的情况来做出口条件下面就是分解成两个方法递归调用
5.还有一块比较重要的就是树的遍历了,要打印出来就要遍历树,这次我是用深度优先遍历完成的.
最后说一下广度优先和深度优先遍历的关键东西,我以前也有代码写深度优先遍历,我拿出来发现完全没有参考价值了,以前是写的一个图的深度优先遍历,也全完不是复制过来就能用,而是完全从新写了一个,当然调试费了一些时间,但是我觉得这两个遍历的关键点我是抓住了的,所以即使不考代码也能写出来,可能调试的时候会费时,但是思想在问题不是很大.
考,说了这么多还是没说到我认为的关键点,其实我认为这两个关键点就是queue和stack,就是队列和堆栈,广度优先遍历就是队列的应用,把子层加入队列,再从队列头做相同的操作.而深度优先则是堆栈的应用,把自己访问到的结点压入堆栈,一直压,一直压,直到叶子再弹出,弹出后也做以前相同的操作.这里还有一个点小技巧,可以用两个栈,一个用来存储是否访问过了,也就是一个出栈,就压入另一个栈.这样方便后面的回退操作判断.
这次算写得长一点的文章了......加油.