一般说到递归,很多人第一时间可能都会想到斐波那契数列(Fibonacci),(注:斐波纳契,又称黄金分割数列,指的是这样一个数列:1、1、2、3、5、8、13、21、……数学上,斐波纳契数列以如下被以递归的方法定义:F0=0,F1=1,Fn=F(n-1)+F(n-2)(n>=2,n∈N*))。在C中用递归实现就很简单了,如下代码:
int Fbi_Rec(int n)
{
if (n < 2)
return n == 0 ? 0:1;
return Fbi_Rec(n-1)+Fbi_Rec(n-2);
}
接下来通过反编译来看看C内部具体是如何实现递归的:(贴出主要汇编代码,//后为个人加上的注释)
Fbi:
.LFB0:
.cfi_startproc
pushl %ebp //将ebp(堆栈数据指针)寄存器的值压入栈,esp=esp-ox10后面要用
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp //将ebp=esp
.cfi_def_cfa_register 5
pushl %ebx //将ebx入栈,esp=esp-ox10,ebx保存函数返回值
subl $20, %esp //esp=esp-ox20(ox20十六进制,因为前面pushl了两次
cmpl $1, 8(%ebp) //ss:[ebp+8]对应我们的变量n(int型8bit),这里即比较n与1,对应C中的n<2
jg .L2 //jg:如果n>1就跳转到.L2
.cfi_offset 3, -12
cmpl $0, 8(%ebp) //比较n与0
setne %al //接下来容易理解,就是n==0?0:1,这里编译器已经做了优化
movzbl %al, %eax //eax保存返回值
jmp .L3 //跳转.L3,主要是返回
.L2: //函数递归主要看这里
movl 8(%ebp), %eax //eax=n
subl $1, %eax //n-1
movl %eax, (%esp) //ss:[esp]=eax
call Fbi //调用Fbi段,最后在这里会跳出,整个函数返回,因为最后一步是F(3)=F(2)+F(1)
movl %eax, %ebx //ebx=eax,保存Fbi(n-1)的返回值到ebx
movl 8(%ebp), %eax //eax保存Fbi(n-1)的返回值
subl $2, %eax //n-2
movl %eax, (%esp) //n=n-2
call Fbi //调用Fbi
addl %ebx, %eax //Fbi(n-1)+Fbi(n-2)
.L3:
addl $20, %esp
popl %ebx
.cfi_restore 3
popl %ebp
.cfi_def_cfa 4, 4
.cfi_restore 5
ret
.cfi_endproc
通过上面的汇编代码我们可以更加深入的理解:
c实现递归是通过将每一层递归用到的函数局部变量,参数值以及返回地址压入栈中,退回时再送出。
因为递归用到了堆栈,就要考虑堆栈溢出的问题,X86 32位机堆栈最大能保存2^32bit.
递归的效率
接下来我将递归改成循环实现Fibonacci,代码如下:
int Fbi_Loop(int n)
{
int i, pre_one, pre_two, cur;
pre_two = 0;
pre_one = 1;
if (n < 2)
return n == 0?pre_two:pre_one;
i = 2;
while(i<=n)
{
cur = pre_two + pre_one;
pre_two = pre_one;
pre_one = cur;
i++;
}
return cur;
}
在我的机子上比较了下两种方式的效率:(取n=40,gprof统计结果)
% cumulative self self total
time seconds seconds calls s/call s/call name
100.00 2.58 2.58 1 2.58 2.58 Fbi_Rec
0.00 2.58 0.00 1 0.00 0.00 Fbi_Loop
可见递归的效率很低,递归写法的两个缺点就是
(1) 递归算法解题通常显得很简洁,但递归算法解题的运行效率较低。所以一般不提倡用递归算法设计程序。
(2) 在递归调用的过程当中系统为每一层的返回点、局部量等开辟了栈来存储。递归次数过多容易造成栈溢出等。所以一般不提倡用递归算法设计程序。