July 21st, 2013

本文的代码更关注算法思想,不关注边界条件

斐波那契数列与阶乘问题

考虑经典的斐波那契数列问题1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, …,我们很容易从数列本身的定义得到一个递推式:f(n)=f(n-1)+f(n-2),因此可以很容易的写出一个递归的函数来完成求该数列的第n项的值:

long fib(int n)
{
	if (n==0) return 0;
	if (n==1) return 1;
	if (n>1) return fib(n-1)+fib(n-2);
}

在考虑阶乘的问题,n的阶乘记为n!,按照定义n!=n(n-1)(n-2)…*1,我们很容易的根据定义设计一个循环来计算n的阶乘:

int factorial(int n)
{
        int result = 1;
        while (n > 0) {
                result = result * n;
                n = n - 1;
        }
        return result;
}

十分简单的两个问题,一个是递归实现,一个是循环实现。递归是一种函数式编程思想,而循环是传统过程式编程的思想。我想大部分的同仁在看到这两个问题的时候应该都首先想到的上述代码是实现方法。那么现在我们分别换一种实现方式。

斐波那契数列用循环来实现。思路,欲求n项的值,就要知道n-1和n-2项的值,这种依赖最终会到第0项和第1项,于是返回来迭代,根据第0项和第1项能算出第2项,同时将第1项和第2项再作为新的n-2和n-1计算第3项…直到迭代到第n项:

long fib(int n)
{
	if (n==0) return 0;
	if (n==1) return 1;
	int i=2;
	int n_1=1;//n-1的值
	int n_2=0;//n-2的值
	int tmp_n_1=0;
	while(i<n){
		tmp_n_1=n_1;//保存n-1的值
		n_1=n_2+n_1;//计算新的n-1的值
		n_2=tmp_n_1;//n-2的值为上次n-1的值
		i++;
	}
	return n_1+n_2;
}

下面用递归来实现阶乘。要用递归来做事情,那么必须先归纳出一个“递推关系”,阶乘的递推关系可以归纳为

n!=n*(n-1)! 当n=0时,结果为1

于是代码就非常直观了

int factorial(int n)
{
	if(n==0) return 1;
	return n*factorial(n-1);
}

到此,可以发现使用递归来实现程序,明显“简单”很多,而且符合人们直观的理解,而使用循环来实现程序很难一目了然的知道程序在做什么。这便是函数式编程的一大优势:是你在告诉程序要做什么,而不是告诉程序怎么去做。当然从内存占用和效率这个角度考虑,循环要比递归高效些。

那么为什么诸如阶乘这样的问题,人们的第一想法是用循环来考虑呢?这也许是最初学习编程的时候“先入为主”造成的,而且阶乘的递推关系不是十分直观,因为你要向一个不知道阶乘是何物的人解释什么是阶乘,恐怕不会使用递推关系来解释;返过来对斐波那契数列,就比较适合用递归关系来解释。

在实际的编程过程中我们很少用递归来解决问题,我想,一方面是受到传统过程式编程思维的影响,另一方面,大部分问题更容易用过程化的思维来思考。所以,我一直信奉“编程语言会影响人的思维方式,思维方式也会影响对语言的学习”。比如,我到现在都不是能很好的掌握javascript。于是我开始思考如何在两种思维间自如的切换呢?是否有通用的方法论呢?

通过例子归纳方法

现在尝试归纳上面的例子,并再试着举几个子:

1)斐波那契数列问题

  • 循环实现,从1开始迭代,计算n-1和n-2,并迭代替换直到n
  • 递归实现,归纳出递推式f(n)=f(n-1)+f(n-2), n=0时为0,n=1时为1

2)阶乘问题

  • 循环实现,从1开始迭代,并将迭代的结果乘法聚合直到n
  • 递归实现,归纳出递推式n!=n*(n-1)! n=0时,结果为1

3)求n以内正整数的和:1+2+3+…+n

  • 循环实现,从1开始迭代,并将迭代的结果加法聚合直到n
  • 递归实现,归纳出递推式sum(n)=n+sum(n-1), n=1时为1

4)求两个数的最大公约数(Euclid算法)

  • 循环实现,迭代计算a%b,将a替换为b,b替换为a%b,直到a%b=0
  • 递归实现,如果a能整除b,那么b既是最大公约数,否则递推式gcd(a,b)=gcd(b,a%b)

5)数组倒置(这里不讨论首尾互换的特殊算法),1234567变成7654321

  • 循环实现,从a[n]开始迭代,将结果聚合成新的数组,直到迭代到1
  • 递归实现,归纳出递推式r(a[n])=r(a[n-1]) + a[1] n=0时为空,n=1时为a[1]

上面的问题都是很简单的问题,但是从中我们可以看到,两种思路的关键点:

  • 循环的关键思路在于,进行迭代直到满足某个条件,在迭代的过程中进行迭代替换或者聚合
  • 递归的关键思路在于,归纳递推式,并且设置边界,以便递归结束

总结

到这里是本文想要阐述的所有内容。本文有感而发,随想而写,总结如下:递归和循环能够相互替代,递归代表的函数式编程倾向于”做什么”,而循环代表的过程式编程倾向于”怎么做”。两种方式对应着不同的程序设计思路,本文通过几个例子,简单总结了两种思路的不同之处以及关键点。希望对熟悉传统编程语言的人们掌握新兴的编程语言有所帮助。


1块2块也是钱,小额赞助