关键词:C,语言的科学和艺术(The art and science of c), 阶乘,排列组合
今天早上因为拿错了书,没有想到也有了新的收获,原来是第五章的习题11
要求绘制 一个PASCAL三角形,上次是因为没有作Combination函数所以没作,
今天没有新作业,只好把这个老作业给解决了,第一步想到的是把Combination
函数复制到新文件中(因在进行第七章的学习,估计以后会把用到的通用
工具都整理到多个库中,就方便调用了,这是后话)。
因为在第六章中也学到了算法的问题,以作者的观点同一个问题一定要多找出
几种算法,然后进行优选,但是我学得每个人都有特定的思维习惯,通常在短
时间内只会想到一种算法,即使不同也是大同小异,所以要找到一个优选的方
案,我感觉有两种操作性比较强的方法,但一个共同的前提就是一定要自己尽
可能的独立完成自己可以完成的算法部分,然后呢,
方法一:时间反省法: 过一段时间再来
审查自己的算法,可能你会发现,原来自己觉得高明的算法是多么的幼稚(但
那永远是难能可贵的,因为它毕竟是一个开端,审视自己的历史,但永远不要
嘲笑它,因为今天的你已经掌握了更多的方法了)。你也许已经想到一个更好
的方法了,改进吧?今天这个例子就是一个时间出真知的例子。一个程序员是
优秀,也许这是一个检查标准:越优秀的人越是可以在越短的时间内进行否定
之否定。一个程序员的成长历程也就是将这个时间间隔缩短的过程吧!
方法二:对比检查法:
俗话说:三个臭皮匠赛过诸葛亮,读读别人的算法,永远是大有益处的,读别人
的算法时最重要的是要对比检查一下自己的实现方法和别人的有什么差别,一定
不要放过一个细小的地方,可能那个地方就有很大的学问。这在对级数的运算中
是一个例子
还是说一个这个组合函数的例子吧?
它是基于这个阶乘函数的:
int Factorial(int n)
{ int product=1,i;
if (n<0) {printf("n can not less than 0");exit(1);}
for (i=2;i<=n;i++) product*=i;
return (product);
}
这个我的组合函数的第一个版本:
int Combinations(int n,int k)
{ return (Factorial(n)/Factorial(k)/Factorial(n-k));
}
它直接实现了组合的概念,是抽象的最直接
的实现,优点:和数学公式一一对应,可读性很强,易于维护,缺点:效率不
高,重复计算,很容易数据溢出。
今天早上到公司时,我读到这个版本时,可能是因为刚学了第六章的算法的原因,
自己不由自主地想到,这个算法是最优的吗,还是其他更好的算法吗?
这几天的我感觉到是一个优秀的程序员首先是要实现一个算法,然后要不断地
怀疑它,进而进行优化,而不是要找到了优化再动手,因为永远没有最优的方案
,但是其中有一个重要的前提就是要保持接口的稳定性,优化只在内部进行,
而对外的接口还要要保持不变,或以扩展的方式出现,我在后面要谈一下自己的
体会。
版本一的缺点是效率不高,因为它对三个数进行了独立的阶乘运算
而大家知道这三个阶乘是相互连的,从其中一个可以得到其它的三个:只要
计算出Factorial(k)(if k<=n/2)或Factorial(n-k)(if k>n/2)然后在其
基础上算出Factorial(n-k)或Factorial(k),
然后再在其基础上算出Factorial(n)可,减少了两次重复的计算。
这是我的第二个版本:
int Combinations(int n,int k)
{ int i,fn=1,fk=1,fnk=1;
if (k<=n/2)
{ fnk=fk=Factorial(k);
for (i=k+1;i<=n-k;i++) fnk*=i;
fn=fnk;
for (i=n-k+1;i<=n;i++) fn*=i;
}
else
{ fk=fnk=Factorial(n-k);
for (i=n-k+1;i<=k;i++) fk*=i;
fn=fk;
for (i=k+1;i<=n;i++) fn*=i;
}
return (fn/fk/fnk);
}
刚完成这个改进,主管说话了,今天没有什么事,周一例会后大家灵活安排,
真是个难得的机会,回家吧,走在路上,想着这事,突然意识到自己被那个
公式的表面形式给套住,还没有跳出三界之外,其实,还可以进一步减少计
算量啊,为fn/fnk,或fn/fk,可以简化为一个从n-k+1到n或k+1到n一个乘法。
这样第二个版本中的两个循环乘法减少到一个,又减少了计算量,而且将
阶乘时数据的溢出危险降到了最低。
这是我的第三个版本:
int Combinations(int n,int k)
{ int i,fmin=1,fnnk=1;
if (k<=n/2)
{ fmin=Factorial(k);
for (i=n-k+1;i<=n;i++) fnnk*=i;
}
else
{ fmin=Factorial(n-k);
for (i=k+1;i<=n;i++) fnnk*=i;
}
return (fnnk/fmin);
}
到此,我感觉到这个组合函数的通用算法已经进化到今天的我感觉还满意的
地步了, 是时候加入一些特例检查的时候了,比如,n
这样第四个版本就出炉了:
int Combinations(int n,int k)
{ int i,fmin=1,fnnk=1;
if (n<0||k<0||n
if (n==1||k==1||n-k==1) return (n);
if (k<=n/2)
{ fmin=Factorial(k);
for (i=n-k+1;i<=n;i++) fnnk*=i;
}
else
{ fmin=Factorial(n-k);
for (i=k+1;i<=n;i++) fnnk*=i;
}
return (fnnk/fmin);
}
通过以上提到的两种方法,我相信还可以找到更优(当然这个最优是各种因素
在特定的条件下平衡的结果),昨天在网上见到一个程序是对阶乘进行不断优
化,提高其运算效率的,可见算法的动人之处,在于对完美的不断追求:用一
句广告词说:没有最好,只有更好。
对于算法实现,先找到一个通用的实现,然后排除非法数据,然后进行合法数
据的特殊进行优先处理,最近是通用数据的通用算法实现。
在这里,我不太确定的是对于n<0,k<0的检查是应交给Factorial()还是保留在
Combinations(n,int)内,想来想去,如果一个人完全透明地了解了整个库的
实现,做为最优的选择,这个检查还是应由Factorial()来完成,但是现实是
一个人很少有机会完全去实现所有的库功能,在不了解函数的内部实现时,还
是谨慎地引用他人实现的函数,一个安全的方法,还是实现必要的检查条件,
这样不会因为函数实现者的疏忽而导致高层建筑的崩溃了,这是很必要的代码
重复,因为你无法确认你的引用函数的实现者重复了它,我们只是不要重复
那些可以明确确定为重复的重复代码,不知道这是不是《代码大全》中的防御式
编程可能要表达的其中一个意思。以我现在的学习进度,不知何时才可以BD
这高山流水之作了。
所以,短时间内或是下一次使用时,我会考虑是不是有好的算法,现在是讨论
一些其他问题的时候了:
第一、数据和数据的表达形式:可以说在读到《Write Great Code》之前,我
也象作者所说那样是把数据和数据的表达形式混为一谈的人,数据是一
抽象的实体,可以至大至小,至刚至柔,可以说它是一个相对稳定或进化
缓慢的实体, 但是表达一个概念需要一个载体,就象人际交流需要一个
载体一样,这个载体受限于当时报技术条件而有限有形,容易被当时的人们
所接受。
我们知道阶乘的结果是整数,排列组合的结果也是整数,但是这个整数和
C语言中int想要表达的整数概念是完全不一样,前者表达的是数据概念,
而int表达的是一种整数的载体(数据的表达形式)概念。
int要表达的意义应该是:如果一个整数小到可以用int来表示就尽量用
int来表达它。这就好比是婴儿和婴儿床的关系,婴儿床是来放置婴儿的
它是按我们的常规设定来提前做好的,并针对婴儿的特点进行相应的布置,
(优化)很适合给婴儿用,但是如果一个呱呱落地的婴儿,个头特大,
虽然他/她也是婴儿但已经不适合放在婴儿床了,也只能先把它放到大床
,即使是太浪费了,我们也只能是满足最低要求的前提下来讨论其他问题
所以简单地说,在给函数的返回值选择数据类型时不能形而上学地认为
数学结果是整数返回值类型就是int或long,而应该考虑返回的最大值和
最小值是否在int这个载体的范围内,如果不能确定就应选用表达范围
更大的载休了,如float或double,虽然尽量用数据范围小的数据类型可
以极大地提高硬件的计算效率,但是前提是这种数据类型不会小到会溢
出数据。
所以,要记住,bool,char,string,int,float,double,它们要表达的不
数据的概念,而只是一个数据的载体,一个容器。对数据选择一个最合适
且最小的容器时,不要被int,float,char的表面意义。
同时要了解到,计算机对整数最拿手,对于象0,1这个两个数就更拿手了
但也记住在保证需求的前提下,考虑效率。
所以在这个阶乘的问题中,因为阶乘的值很容易就跑出的int的范围,
所以还是用double来表示比较好一些(更通用,除非你可明确限定)
第二。函数原型的稳定性,其实第一个问题只是个引子,为的就是为了说明第
二个问题,如果返回值的选择欠妥,那么一个最直接的问题就是会破坏
函数原型的稳定性,继而破坏库的稳定性。
所以在设计库时,对接口的反复权衡永远是第一步要完成的,只要在对接
口设计好了,才可以算法进行考虑,这样在一个稳定的函数原型或接口下
无论实现发生了什么变化,对用户来说都百利而无一害了。