使用组合改进软件测试用例的生成3
计算组合元素的个数
现在我已经确定了如何创建一个组合对象,让我们看看组合的三个基本操作的第二个——根据某个给定的条目总数 n 及子集大小 k 来计算组合元素的总数。举个例子,如果你处理一次从 n=5 条目中取 k=3,这里有10种可能的组合元素:
{ 0, 1, 2 } { 0, 3, 4 } { 0, 1, 3 } { 1, 2, 3 } { 0, 1, 4 } { 1, 2, 4 } { 0, 2, 3 } { 1, 3, 4 } { 0, 2, 4 } { 2, 3, 4 }
我想实现一个 Choose(n,k) 函数,它返回组合元素的个数;Choose(5,3) 返回10。查找现有的Choose 实现,我惊讶地发现 Internet 上的大多数算法都很不耐用。在我向你展示我的 Choose 实现之前,让我们简要地审视一下 Choose 函数的标准实现。
编写 Choose(n,k) 函数的标准方法是直接使用其定义公式。这是一个明显的但是拙劣解决方案。这里是一个用 C# 编码的典型 Choose(n,k) 函数 :
以下是引用片段:
// poor implementation of Choose(n,k)
static int Choose(int n, int k)
{
int numerator = Factorial(n);
int denominator = Factorial(k) * Factorial(n-k);
return ( numerator / denominator );
}
辅助函数 Factorial 的实现如下:
以下是引用片段:
static int Factorial(int m)
{
int ans = 1;
for (int i = m; i >= 1; —i)
{
ans = ans * i;
}
return ans;
}
这里的 Choose(n,k) 实现有几个问题:最严重的是它会因为当 n 和 k 的值十分小时而溢出。注意这个 Choose(n,k) 首先计算 Factorial(n), 即便 n 是一个很小的值,它也会迅速增大到一个非常大的数 ( 比如,21! 将溢出一个无符号的 64 位数)。其次 Choose(n,k) 的值由两个阶乘的乘积 来除,这也可能成为一个非常大的数,得出的结果可能非常小。关键是即使 Choose(n,k) 返回的结果很小,中间计算结果很容易溢出。
一个更好的 Choose(n,k) 实现使用一个不同的方法计算其答案。改版的 Choose(n,k) 使用以下不同的公式来计算:
Choose(n,k) = (n * (n-1) * (n-2) * ... * (n-k+1)) / ( 1 * 2 * ... * k)
这个算法看起来很丑,但用一个例子来说明就知道,它更容易理解:
Choose(7,3) = (7 * 6 * 5) / (1 * 2 * 3)
这个算法取代了原来计算分子(一个大数),然后计算分母(一个大数),然后相除,你可以计算部分乘积法并随意进行除法运算。对于 Choose(7,3) 例子,你 先计算 7 * 6 并除以 2,得到 21(译注:原文为“getting 24”显然不对,7 * 6/2=21)(跳过此分数下面部分的第1项,因为被1除是没有作用的)。这时用5乘以部分乘积并用3除,你可以得到答案35, 和前面的结果一样。
这里对 Choose(n,k) 的第二次优化是由以下特性产生的:
Choose(n,k) = Choose(n, n-k).
举个例子,Choose(10,8) = Choose(10,2)。这不是一个明显的关系,但是如果你用一些例子来试验的话,你将看到为什么这是对的。直接计算 Choose(10,8) 之间涉及到计算七个部分乘积和七个除法,但是计算等价的 Choose(10,2) 只要求一个乘法和一个除法操作。
综上所述,我实现的 Choose(n,k) 如 Figure 5 所示。在 Choose 函数中,我对 n 等于 k 的情况进行了快速检查,如果为真,则返回 1。而 Choose 算法中没有对之进行检查,但它改进了生成特定 Combination 对象元素的方法性能。Choose 实现的剩余部分用我刚刚解释的算法计算元素的总数。
现在我已经确定了如何创建一个组合对象,让我们看看组合的三个基本操作的第二个——根据某个给定的条目总数 n 及子集大小 k 来计算组合元素的总数。举个例子,如果你处理一次从 n=5 条目中取 k=3,这里有10种可能的组合元素:
{ 0, 1, 2 } { 0, 3, 4 } { 0, 1, 3 } { 1, 2, 3 } { 0, 1, 4 } { 1, 2, 4 } { 0, 2, 3 } { 1, 3, 4 } { 0, 2, 4 } { 2, 3, 4 }
我想实现一个 Choose(n,k) 函数,它返回组合元素的个数;Choose(5,3) 返回10。查找现有的Choose 实现,我惊讶地发现 Internet 上的大多数算法都很不耐用。在我向你展示我的 Choose 实现之前,让我们简要地审视一下 Choose 函数的标准实现。
编写 Choose(n,k) 函数的标准方法是直接使用其定义公式。这是一个明显的但是拙劣解决方案。这里是一个用 C# 编码的典型 Choose(n,k) 函数 :
以下是引用片段:
// poor implementation of Choose(n,k)
static int Choose(int n, int k)
{
int numerator = Factorial(n);
int denominator = Factorial(k) * Factorial(n-k);
return ( numerator / denominator );
}
辅助函数 Factorial 的实现如下:
以下是引用片段:
static int Factorial(int m)
{
int ans = 1;
for (int i = m; i >= 1; —i)
{
ans = ans * i;
}
return ans;
}
这里的 Choose(n,k) 实现有几个问题:最严重的是它会因为当 n 和 k 的值十分小时而溢出。注意这个 Choose(n,k) 首先计算 Factorial(n), 即便 n 是一个很小的值,它也会迅速增大到一个非常大的数 ( 比如,21! 将溢出一个无符号的 64 位数)。其次 Choose(n,k) 的值由两个阶乘的乘积 来除,这也可能成为一个非常大的数,得出的结果可能非常小。关键是即使 Choose(n,k) 返回的结果很小,中间计算结果很容易溢出。
一个更好的 Choose(n,k) 实现使用一个不同的方法计算其答案。改版的 Choose(n,k) 使用以下不同的公式来计算:
Choose(n,k) = (n * (n-1) * (n-2) * ... * (n-k+1)) / ( 1 * 2 * ... * k)
这个算法看起来很丑,但用一个例子来说明就知道,它更容易理解:
Choose(7,3) = (7 * 6 * 5) / (1 * 2 * 3)
这个算法取代了原来计算分子(一个大数),然后计算分母(一个大数),然后相除,你可以计算部分乘积法并随意进行除法运算。对于 Choose(7,3) 例子,你 先计算 7 * 6 并除以 2,得到 21(译注:原文为“getting 24”显然不对,7 * 6/2=21)(跳过此分数下面部分的第1项,因为被1除是没有作用的)。这时用5乘以部分乘积并用3除,你可以得到答案35, 和前面的结果一样。
这里对 Choose(n,k) 的第二次优化是由以下特性产生的:
Choose(n,k) = Choose(n, n-k).
举个例子,Choose(10,8) = Choose(10,2)。这不是一个明显的关系,但是如果你用一些例子来试验的话,你将看到为什么这是对的。直接计算 Choose(10,8) 之间涉及到计算七个部分乘积和七个除法,但是计算等价的 Choose(10,2) 只要求一个乘法和一个除法操作。
综上所述,我实现的 Choose(n,k) 如 Figure 5 所示。在 Choose 函数中,我对 n 等于 k 的情况进行了快速检查,如果为真,则返回 1。而 Choose 算法中没有对之进行检查,但它改进了生成特定 Combination 对象元素的方法性能。Choose 实现的剩余部分用我刚刚解释的算法计算元素的总数。