编写程序,把一个n个元素的集合{1,2,3,…,n}中的所有m个元素的子集用字典顺序列出来。例如, {1,2,3,4,5}中3个元素的子集为:
{1,2,3},{1,2,4},{1,2,5},{1,3,4},{1,3,5},{1,4,5},{2,3,4},{2,3,5},{2,4,5},{3,4,5}
【第一次尝试:两层for循环】觉得应该是两层for循环,外层控制集合的生成次数,因为根据数学知识,可以算出来答案这样的集合有多少个;
在循环的内层,拿到一个子集所需的元素,封装成一个子集,由于一个子集有m个元素,于是就需要循环m次拿到m个元素。若现在把内层看成一个方法,分析它的输入输出情况如下:
输入——当前求的子集是第几个,当前正在确定该子集中的第几个元素
输出——使该子集符合字典顺序(所有子集中的元素按从小到大排列,子集之间排列时按“对位”比较进行排序)的一个元素。
书写这样一个输入信息量小、输出信息量大(相对输入而言)的方法是有难度的,这中间最大的问题就是输入信息只涉及到了序号,而不触及具体的元素,无疑,这样的“里程碑”对我们跑后续的路是支持不够的。于是,现在转而他求。
【第二次尝试:递归】
然后就感觉到可能要用递归行。
想想我们平时依靠纸和笔解决这个问题时的思维,先把给出的集合中的所有元素按照从小到大的顺序排列,然后,拿出最小的一个元素,把以该元素开头的所有子集列举完毕,再换一个次小的元素作开头。而怎样把以某元素开头的子集列举完毕呢?这个过程也是一个确定子集的过程,只不过是把给定的集合看成是原先的集合除去一个最小的元素,把所求的子集数目减一。依照这样的模式进行下去,直到所求的子集数目减为一。然后呢?是取这时候“给定的集合”中最小的那个吗,还是在“给定的集合”中依次将它们作为最后一个元素呢?
这样做,逻辑有些混乱,感觉到递归似乎更适合那种最后求一个结果的情况,像面试时我遇到的让用一行代码实现求阶乘。而递归好像不适合这样需要最后穷举多个结果的情况。可以说,递归是重结果的。(不知道这点看法是否正确,还有待深入学习和体验)
【第三次尝试:m层for循环】
那现在怎么做才能解决问题呢?后来想想,它更可能是m层循环,第一层代表第一个元素,第二层则代表第二个元素,同样,第m层代表了第m个元素,这样,就类似于我们解决此类问题一贯的思路——“固定前边,变化后边”。 那么现在的问题是确定各层for循环的范围以及 最内层for循环的要执行的方法。
可以先从上述简单的实例入手,然后再抽象化
public static void main(String[] args) { // 为了简化,直接给出一个从小到大排列的数组,它有n(此实例中n=5)个不重复的整数 int[] collection = new int[] { 1, 2, 3, 4, 5 }; //定义m个变量,对应于所求子集中的m(此实例中m=3)个元素 int r1, r2, r3; // 每层for循环控制所求子集中的一个元素的取值范围(数组的索引范围),第几层for循环就控制第几个元素的取值 for (int a = 0; a <= 2; a++) { // 共5个元素,求具有3个元素的子集,最后一个子集必定是由最后数组中最后三个值组成的 //经演绎,最外层for循环能进行n-m+1次有效执行,索引取值范围为0<= a <= (n-m) r1 = collection[a]; for (int b = a + 1; b <= 3; b++) { //第2个元素的索引要比第1个大, 并且索引小于等于3 //经演绎,第k个元素的索引要比第k-1个大, 并且最大索引小于等于n-m+k-1 r2 = collection[b]; for (int c = b + 1; c <= 4; c++) { //第3个元素的索引要比第2个元素大, 并且最大索引不能超过4 //第m个元素的索引要比第m-1个元素大, 并且最大索引不能超过n-m+(m-1),即n-1 r3 = collection[c]; //打印一组子集元素 System.out.println(r1 + "\t" + r2 + "\t" + r3); } } } }
m个变量可以用长度为m的整型数组表示,可for循环怎样抽象表示呢?想,for循环需要有m次,只要找出这m次循环的规律,就可以将这m次for循环用一个循环来控制了。
for循环的编号(从1到m) | 循环控制变量的左边界 | 循环控制变量的右边界 |
1 | 0 | n-m |
2 | 第一个循环控制变量所取的值加一 | n-m+1 |
'''''''''''''' | '''''''''''' | ' ' ' ' '' |
k(1<=k<=m) | 前一个for循环控制变量所取值加一 (如果k=1,则为0) |
n-m+(k-1) |
难点在于循环控制变量的左边界需要借助上一个for循环控制变量所取的值,
我终究没有想出来,看到仁兄提到的是用回溯法解决的,我还不太了解,有待学习呀。
【感悟: 编写算法时,做输入输出分析很有意义】
作输入、输出分析是很有意义的一件事情
每次取元素,输入什么,输出什么?当一系列操作的输入是同一类事物,输出也是同一类事物,那么,这一系列操作就可以抽象为一个方法。哈哈,这应该在思想认识上达到了一定的高度吧。
算法的代码能够编写出来,我认为必要的前提是人先得能想清楚,能通过纸和笔将问题解答出来,正所谓“程序是人思维的固化”。但也并不是凡是人能分析解答出的问题,都能代码实现,因为这中间还有一个语言转换的过程,它有可能成为一个瓶颈。
在编写算法时,对于简单问题,需要捋清我们用纸和笔去解答时的思维过程,之所以这么说,是因为人脑往往会跳跃式、综合式地去思考,需要把这个过程放慢,能形成“伪代码”,然后“翻译”成代码。如果不行,可以考虑换一种数学模型(更容易被计算机理解的)去考虑这个问题,毕竟问题往往有多种解法。
而对于复杂问题,恐怕困难就不仅在于“翻译”了。也许对于问题我们本身就没有思路,这里受限的并不仅是数学的问题, 还有边缘学科的问题。对事物认识清楚了,往往也就能用数学模型来解答,也就能“翻译”成代码了。譬如,数字图象处理在图像的分割方面的算法尚不能借鉴人眼的算法,因为“人眼处理图像的复杂机制”还没有被揭示,一旦揭示,数字图象处理学科必然会受到良好的影响而突飞猛进。