二项式定理(Java实现及代码重审)

I》中获得的一些启示。其中有非常重要的一条:代码重审和回顾。通过对以前写过的代码进行重新审视和改进(以现在的经验),使之更具实用性,从而学习新的东西。你敢于面对以前写过的代码吗?如果你都不敢面对,谁还能有这个勇气?

作为代码重审和回顾的一个例子,我对以前的一个粗糙的二项式定理实现进行了重审和改写。当时,主要是为了学习动态规划法技术,运用它来计算二项式系数。

简单回顾二项式定理的相关知识:

(a+b)^n= a^n + C(n,1)a^(n-1)b+…+C(n,k)a^(n-k)b^k +…+ C(n,n-1)ab^(n-1)+b^n

其中: C(n,0)= C(n,n) = 1; C(n,k) = C(n, n-k); C(n,k)= C(n-1, k-1) + C(n-1,k)

计算方法: 例如C(4,2)

S1:构造数组: a[0:4][0:4]= 0

S2:a[0][0] = 1;

S3:a[1][0] = 1; a[1][1] = 1;

S4:a[2][0] = 1; a[2][1] = 2; a[2][2] = 1

S5:a[3][0] = 1; a[3][1] = 3; a[3][2] = 3; a[3][3] = 1

S6:a[4][0] = 1; a[4][1] = 4; a[4][2] = 6

如上所示,自上向下运用公式计算即可得到a[4][2] = 6.动态规划法通过保存并重用已经计算的值,从而避免不必要的计算时间,提高运行时间效率;其代价是一定的空间效率。这里,空间效率是O(n^2);后面可以看到,通过重用空间,空间效率可以降低到O(n)(否则,很快就内存不足了)。

如何改写呢?首先要定义好类的接口和功能。对于该二项式类BinomialTheorem(命名要贴切,英文不知道如何拼写?搜索!):

1.提供唯一参数为 二项式的幂 ,通过公共构造器传入;

2.显示该二项式展开式的字符串表示。

这里,仅仅只留出两个公共接口。简单的接口可使类更易使用;此外,在改写的过程中,发现要提供一个计算组合系数C(n,k)的便利方法。

改进的几个方面:

[1] 将其改为值对象。值对象是状态不可变的对象;对于同样的值应当返回同一个实例。值对象需要覆写equals和hashCode方法;而如何使得对于同一个值参数返回同一个实例呢?参考Integer.ValueOf()实现,定义了一个内部嵌套类BinoTheoremCache,存放256个缓存BinomialTheorem对象。若取BinomialTheorem(i), 0<=i < 256 , 那么,直接从缓存中取;否则,使用 new来获取。这也说明一点:如果不太清楚某某怎么实现的,可以参考JDK源码来获取思路和启示。

[2]大整数: 计算到C(60,30)就发现整型不够用了。n= 60就无法获得正确结果,这个类是毫无用处的。必须使用大整数;这里直接使用了java提供的BigInteger.具体实现是如何呢?留待以后推敲。这里仅仅给出猜想:应该是用整型数组拼接而成。inti可以表示2147483647;那么 int[]arr = new int[2] ; arr可以表示到21474836472147483647这么大的数(转换为字符串形式)。每个整数用一个整型数组表示显然浪费空间;那么可以分段表示:若i<= 2147483647 ,则用含一个整数的数组表示; 若 2147483647< I <= 21474836472147483647;则用含两个整数的数组表示。依次类推。接下来就需要实现数组的加法、减法、乘法等。

[3]空间效率: 如果采用初始的a[0:n][0:n]那么,其空间效率是O(n^2);当n= 10000时;int[0:10000][0:10000]需要10000*10000*4/1024/1024=381.47MB 空间;对于n= 100000就无能为力了;因此,如果总是囿于小数据处理,那么,会有一种“取之不尽,用之不竭”的错觉;一旦深入到大数据集的处理领域,就不得不经常面对JavaOutOfMemoryError了。

如何改进其空间效率呢?思路是直观的:重用空间。可以发现:A.考虑到对称性,C(n,k)= C(n, n-k), 实际上只需要a[0:n][0:n/2];B. 当矩阵a[j-1][i]用来计算a[j][i] i = 0,1,…, n/2 ;1 <=j <= n之后,a[j-1][i]便不再起作用;因此,可以重用a[j-1][i],i= 0,1,…, n/2 的空间; 于是,只需要a[0:1][0:n/2]就可以了; 进一步地, 可以只需要a[0:n/2]空间 ,不过必须要倒着计算:

假设a[0: 2]存储着C(4,0) , C(4,1), C(4,2) ;即:

a[0]= C(4,0), a[1] = C(4,1) , a[2] = C(4,2) ; 则

S1:a[2] = C(5,2) = 2C(4,2)= 2 *a[2];

S2:a[1] = C(5,1) = C(4,0) + C(4,1)= a[0] + a[1];

S3:a[0] = C(5,0) =1;

如果顺着计算,会产生覆盖:

S1:a[0] = C(5,0) =1;

S2:a[1] = C(5,1) =C(4,0)+C(4,1)

S3: a[2] = C(5,2) = 2C(4,1) //! a[1]已被覆盖为C(5,1)

这里实际上说明了一个比较普遍的现象:虽然动态规划法通常牺牲一定的空间来换取时间效率;但空间效率通常是有一定的提升和优化的空间的。

[4] 一个简单的运行时间测量框架

测量方法运行时间是一个较为频繁的操作,尤其是当实现一定的算法,并期望知道其性能的时候。通过写一个比较简单的运行时间测量框架,可以方便以后的算法性能测量应用。目前还只能接受单个问题规模参数的测试。这与程序测试等其实都是一本万利的事情。最初,可能觉得很麻烦,一段时间熟悉后,当你掌握相关方法和技术后,开发速度就自然提上去了。熟能生巧。

[5]小结:

不得不说,在这个代码重审和改进的过程中,确实学到了不少东西,也体验到了不断精益求精的一些感受,很好很充实。一个类,要写好可不容易!当然,改写后的最终程序并不一定就有多完善,不过,实用性是有很大提升的。程序中有不足之处,还恳请读者指正。

Java 代码实现:

BinomialTheorem.java

一个简单的运行时间测试框架 RuntimeMeasurement.java

BinomialTheorem 测试类 : BinomialTest.java

影子依旧可以相亲相爱。哪一块骨骼最温暖,总能一击即中。

二项式定理(Java实现及代码重审)

相关文章:

你感兴趣的文章:

标签云: