一道小算法题的思考

编写程序,把一个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);
				}
			}
		}
	}

现在,可以把问题稍微抽象一些,要求输入的是一个按从小到大排列的整型数组,具有n(n>1)个元素,并且两两不等,要求按字典顺序输出具有m(1<=m<n)个元素的所有子集

m个变量可以用长度为m的整型数组表示,可for循环怎样抽象表示呢?想,for循环需要有m次,只要找出这m次循环的规律,就可以将这m次for循环用一个循环来控制了。


求解子集时的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循环控制变量所取的值,

我终究没有想出来,看到仁兄提到的是用回溯法解决的,我还不太了解,有待学习呀。


【感悟: 编写算法时,做输入输出分析很有意义】

作输入、输出分析是很有意义的一件事情

每次取元素,输入什么,输出什么?当一系列操作的输入是同一类事物,输出也是同一类事物,那么,这一系列操作就可以抽象为一个方法。哈哈,这应该在思想认识上达到了一定的高度吧。

 

算法的代码能够编写出来,我认为必要的前提是人先得能想清楚,能通过纸和笔将问题解答出来,正所谓“程序是人思维的固化”。但也并不是凡是人能分析解答出的问题,都能代码实现,因为这中间还有一个语言转换的过程,它有可能成为一个瓶颈。

 

在编写算法时,对于简单问题,需要捋清我们用纸和笔去解答时的思维过程,之所以这么说,是因为人脑往往会跳跃式、综合式地去思考,需要把这个过程放慢,能形成“伪代码”,然后“翻译”成代码。如果不行,可以考虑换一种数学模型(更容易被计算机理解的)去考虑这个问题,毕竟问题往往有多种解法。


而对于复杂问题,恐怕困难就不仅在于“翻译”了。也许对于问题我们本身就没有思路,这里受限的并不仅是数学的问题, 还有边缘学科的问题。对事物认识清楚了,往往也就能用数学模型来解答,也就能“翻译”成代码了。譬如,数字图象处理在图像的分割方面的算法尚不能借鉴人眼的算法,因为“人眼处理图像的复杂机制”还没有被揭示,一旦揭示,数字图象处理学科必然会受到良好的影响而突飞猛进。

你可能感兴趣的:(一道小算法题的思考)