假设有一组由n个不同的元素。为说明得具体些,我们假设这些元素是英文字母。那么,它们有多少种不同的排列顺序?显然,结果取决于n,所以结果是n的函数。该函数称为阶乘函数(factorial function)。n的阶乘就是n个不同元素的不同排列顺序的数目。数学家通常用感叹号来表示,所以n的阶乘可以写为n!。不同的顺序也称为排列(permutation)。
下面来计算几个阶乘。显然,一个元素的排列方法只有一种,所以 1!=1。两个元素有两种排列方式:A-B和B-A,因此 2!=2。稍稍动一动笔,就会发现三个元素有六种排列方式:
C AB C BA A C B B C A AB C BA C
怎样才能保证没有漏掉任何情况?找出一个构建每一种可能的组合的方法并不难,在第4章(TODO)我们将介绍一个程序,它能列出所有排列。这里介绍另一种方法。在由两个元素组成的排列中添加一个新元素,就能组成含有三个元素的任意排列。两个元素的排列有两种情况:AB和BA。 每种排列下,都可以将C放在三个地方:开头,中间,或结尾。因此可作出的选择有 2·3 = 6 中,并且由于每种选择都会生成不同的排列,所以总共必然有六种排列。上述列表的左边那一列是将C插入到AB中的情况,右边那列是将C插入到BA的情况,所以上述结论是完整的。
类似地,如果想知道四个元素有多少种排列,可以使用同样的方法。三个元素有六种排列,而插入第四个元素的位置有四个,所以总共有 6·4 = 24 种顺序:
D ABC D ACB D BAC D BCA D CAB 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 - 1个元素的(n - 1)!种排列中取出一种,然后将第n个元素插入到排列中的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时,就不需要再递归调用了:
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,那么它就无法计算阶乘,而是永远返回零。
接下来花点时间看看如果不写my会怎样。下面这个factorial()跟前面那个几乎一模一样,只是在$n前面少了my声明:
sub factorial {
($n) = @_;
return 1 if $n == 0;
return factorial($n-1) * $n;
}
这样,由于任何没有声明成my的Perl变量都是全局的,所以$n是全局变量。这就是说,即使有多个factorial()在同时执行,它们都会使用同一个全局变量$n。这对函数的行为有什么影响呢?
来看看调用factorial(1)时的情况。一开始,$n设置成1,这样第二行的判断会失败,于是函数递归调用factorial(0)。而factorial(1)则会一直等到新调用的函数执行完毕之后,才会继续执行。进入factorial(0)之后,$n被设置为0。这次,第二行的判断为真,于是函数立即返回1。
factorial(0)的结果为1,这样factorial(1)可以继续执行了。factorial(1)使用刚才的结果1,将它乘以$n,并返回结果。但刚才factorial(0)已经把$n设置成了0,所以结果为1·0 = 0。最终我们得到了factorial(1)的错误结果——0。正确的值应该是1。
类似地,factorial(2)应该返回2,但却返回0;factorial(3)应该返回6,但却返回0;以此类推。
为了确保正确执行,每个factorial()的调用都要有自己的$n的副本,避免被其他的调用影响,而这正是my的作用。每次调用factorial(),都会为该次调用生成一个新的变量,作为$n使用。
其他所有支持递归函数的语言都有类似于Perl的my变量的某种机制,用于在每次函数调用时创建新的变量。例如C语言中,在函数内部创建的变量默认就是这样,除非用特别的方法定义。(在C语言中,这种变量的分配和释放是自动完成的,因此被称为自动变量。使用全局变量,或者不会在每次函数调用时重新分配的存储方式,会导致函数无法递归调用,这种函数称为不可重入的(non-reentrant)。在Fortran之类的语言风行的时代(Fortran直到1990年才支持递归),不可重入函数十分常见,但当支持私有变量的语言(如C语言)流行之后,这种函数就比较少见了。