1.2 阶乘
假设有一个有n个不同条目的列表。为了具体点,假设这些条目是字母表的字母。这样一个列表有多少种不同的排列次序呢?显然,因为答案依赖于n,所以它是n的函数。这个函数称为阶乘函数(factorial function)。n的阶乘就是n个不同条目的不同的排列次序的数量。通常以一个后缀!标记,这样n的阶乘就是n!。一般不同的次序称为排列(permutation)。
下面计算一些阶乘。显然,只有一个条目的列表只有一种排列,所以1!=1。两个条目的列表有两种排列:A-B和B-A。少许笔算可以发现3个条目有6种排列:
C AB C BA
A C B B C A
AB C BA C
如何确定没有遗漏呢?不难想出一种构建所有可能排列的方法,第4章将介绍一个把它们都列出来的程序。这里有种方法可以做到。可以给一个两条目的列表添加一个新条目制造任何3条目的列表。从两条目列表开始时有两种选择:AB和BA。每种情况下,在何处放C都有三种选择:在开始处,在中间,或者在结尾处。一共有2×3=6种选择,由于每种选择产生一个不同的3条目的列表,因此就必有六个这样的列表。上面的左边一列显示了把C插入AB得到的所有列表,右边一列显示了把C插入BA得到的所有列表,所以上面的展示是完整的。
类似地,如果想知道4个条目有多少排列,就可以用同样的方法计算。3个条目有6个不同的列表,每个列表有4个位置可插入第4个条目,一共是6×4=24种排列:
D ABC D ACB D BAC D BCA D CAA D CBA
A D BC A D CB B D AC B D CA C D AB C D BA
AB D C AC D B BA D C BC D A CA D B CB D A
ABC D ACB D BAC D BCA D CAB D CBA D
现在写个函数计算,给定任意n,一个n元素列表有多少种排列。
刚才看到了如果知道了n-1个东西的可能的排列的数量,那就能计算n个东西的排列的数量了。要得到一个n项的列表,取(n-1)项列表的(n-1)!个列表中的一个,并把第n项插入列表中n个可能的位置之一。因此,n个条目的排列总数是(n-1)!·n:
sub factorial {
my ($n) = @_;
return factorial($n-1) * $n;
}
这个函数错了,因为遗漏了终止条件,所以对任何输入它永不产生结果。要计算factorial(2),它先尝试计算factorial(1)。要计算factorial(1),它先尝试计算factorial(0)。要计算factorial(0),它先尝试计算factorial(-1)。这个过程永远进行下去。可以修正它,明确地告诉函数0!是什么,那么当它遇到0时就不必再递归调用了:
### Code Library: factorial
sub factorial {
my ($n) = @_;
return 1 if $n == 0;
return factorial($n-1) * $n;
}
现在函数可以正确运行了。
0的阶乘是1的原因也许不那么明显。回到定义。factorial($n)是给定的$n个元素的列表的排列。factorial(2)是2,因为有两种方法排列一个两元素列表:('A', 'B')和('B', 'A')。factorial(1)是1,因为只有一种方法排列一个单元素列表:('A')。factorial(0)是1,因为只有一种方法排列一个零元素列表:()。有时人们想要争论0!应该是0,但是()的例子清楚地显示不是那样的。
在递归函数中得到正确的基本型是极其重要的,因为如果弄错了,函数将返回一堆别的结果。如果错误地把上面函数中的return 1换成return 0,它将不再是一个计算阶乘的函数,相反,它会是一个计算零的函数。
1.2.1 为什么私有变量是重要的
接下来介绍如果遗漏my将会发生什么。除了没有对$n的my声明,下面的函数factorial()和前一个版本一样:
### Code Library: factorial-broken
sub factorial {
($n) = @_;
return 1 if $n == 0;
return factorial($n-1) * $n;
}
现在$n是一个全局变量,因为所有的Perl变量都是全局的除非用my声明它们。这就意味着尽管几个factorial()的副本可以同时运行,但它们都使用同一个全局变量$n。这对函数的行为有什么影响呢?
考虑一下当调用factorial(1)时会发生什么。最初,把$n设置为1,第二行的测试失败,所以函数递归调用factorial(0)。factorial(1)的执行体在等待新的函数调用结束。当factorial(0)运行时,把$n设置为0。这次第二行的测试为真,函数立即返回,产生1。
在等待factorial(0)结果的factorial(1)的执行体现在可以继续了,来自factorial(0)的结果是1。factorial(1)得到这个1,乘以$n的值,然后返回结果。但是$n现在是0,因为factorial(0)设它为0,所以结果是1×0=0。这就是factorial(1)最终的错误返回值。它应该是1,而不是0。
类似地,factorial(2)返回0而不是2,factorial(3)返回0而不是6,以此类推。
为了运行正确,每个factorial()的执行体需要有它自己私有的$n副本,这样其他执行体就不会冲突了,这正是my所做的。每次factorial()执行时,都产生一个新的变量专供那次执行用做它的$n。
其他支持递归功能的语言都有某些变量与Perl的my变量功能类似,每次函数执行时就产生一个新的变量。例如,在C语言里,在函数内声明的变量默认有这种表现,除非以别的方式声明。(在C语言里,这样的变量称为自动(auto)变量,因为它们会自动分配和释放。)使用全局变量或者某种不在每次执行时会分配存储的函数通常无法递归地调用该函数,这类函数称为不可重入的(non-reentrant)。不可重入的函数在还使用Fortran(1990年以前不支持递归)语言时是非常普遍的,当使用有私有变量的语言(如C)流行时,它就不那么普遍了。