(几年前的几篇文章,重发于此。)
楔子
王二、张三和赵四一日无聊,决定玩击鼓传花讲冷笑话的游戏。王二和张三围成一圈传花,赵四负责击鼓。张三接连讲了几个诸如小菜、狐狸狡猾的笑话。花停在了王二的手中。
王二:这个笑话很短。你要保证听完后不生气我就说。
张三:你说吧。
王二:张三。
张三:怎么了?
王二:笑话说完了,就两个字。
张三欲发怒。
王二:欸,你刚才说好了不会生气的。
张三只好作罢。新一轮开始,花又停在王二的手中。
王二:张三不是个笑话。
张三再次欲发怒。
王二:别生气,我说的是冷笑话,就表示不好笑啊。
花又一次停在王二的手中。
王二:[张三不是个笑话]不是个笑话。
第四次花停在王二的手中。
王二:[[[张三不是个笑话]不是个笑话]不是个笑话]。
……
递归与一个例子程序设计中,递归指在一个函数中直接或间接地调用函数自身。所以以上冷笑话的无穷序列就可以看作是递归调用“不是个笑话”。递归在实现上没有什么特殊的困难,因为以堆栈模型来看函数调用,被调用函数与调用函数是否相同是无关紧要的。所以大部分语言都支持递归。函数作为Javascript的基石,也支持递归。
递归和循环常常可以用来解决同一个问题。但是两者的设计方法有很大不同。采用循环的代码需要指明解决问题的每一个步骤。而递归则更接近于数学上的形式定义,只需要写明问题给出的条件,剩下的就由计算机重复地迭代,思路上往往很直观。
很多书里都用计算斐波纳契数列来说明递归的应用。这里也把它作为第一个例子。
斐波纳契数列的定义为,当n=0和1时,A(n)=n;当n>=2时,A(n)=A(n-1)+A(n-2)。
用循环和递归的方法分别都可以写出计算斐波纳契某一项的函数。不过实际上,如果让一个熟练掌握数列通项公式又刚学习程序的高中生来写,结果可能会是这样。
//以下一系列的函数都假设n为非负整数
function fibonacci1(n){
var phi=(1+Math.sqrt(5))/2;//(1-Math.sqrt(5))/2
if (n<2) return n;
return (Math.pow(phi, n) + Math.pow(1-phi, n))/(phi*(3-phi));
}
(他的思路如下:根据A(n)=A(n-1)+A(n-2),得A(n)+x*A(n-1)=(1+x)*[A(n-1)+1/(1+x)*A(n-2)],令x=1/(1+x)可得x^2+x-1=0,解出x=[-1+sqrt(5)]/2或[-1-sqrt(5)]/2。A(n)+x*A(n-1)构成一个等比数列。最后得到程序中的解的公式。)
这样的解法会让数学老师高兴,计算机老师难过。计算机被当成计算器来用。另外,由于运算中涉及到小数,计算结果与本应是整数的精确值相比有微小的误差。
采用循环的的第二个版本如下:
function fibonacci2(n){
if (n<2) return n;
var a=0, b=1, c;
for (i=2; i<=n; i++){
c=a+b;
a=b;
b=c;
}
return c;
}
下面则是采用递归的第三个版本。
function fibonacci3(n){
if (n<2) return n;
return f3(n-1) + f3(n-2);
}
可以明显地看出,采用递归的版本程序最简短,它只是将斐波纳契数列的数学定义用程序的语言写出来。