今天在进行数据排序时候用到递归,但是耗费内存太大,于是想找一找有没有既提升效率又节省内存的算法,然后发现尾递归确实不错,只可惜php并没有对此作优化支持.
虽然如此,但还是学习了,下面总结一下:
如果一个函数中所有递归形式的调用都出现在函数的末尾,我们称这个递归函数是尾递归的。当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。尾递归函数的特点是在回归过程中不用做任何操作,这个特性很重要,因为大多数现代的编译器会利用这种特点自动生成优化的代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
/*facttail.c*/
#include"facttail.h"
/*facttail*/
int
facttail(
int
n,
int
a)
{
/*Compute a factorialina tail - recursive manner.*/
if
(n < 0)
return
0;
else
if
(n == 0)
return
1;
else
if
(n == 1)
return
a;
else
return
facttail(n - 1, n * a);
}
|
1
2
3
4
5
|
long
Rescuvie(
long
n) {
return
(n == 1) ? 1 : n * Rescuvie(n - 1);
}
|
1
2
3
4
5
6
7
8
9
10
11
12
|
long
TailRescuvie(
long
n,
long
a) {
return
(n == 1) ? a : TailRescuvie(n - 1, a * n);
}
long
TailRescuvie(
long
n) {
//封装用的
return
(n == 0) ? 1 : TailRescuvie(n, 1);
}
|
Rescuvie(5)
{5 * Rescuvie(4)}
{5 * {4 * Rescuvie(3)}}
{5 * {4 * {3 * Rescuvie(2)}}}
{5 * {4 * {3 * {2 * Rescuvie(1)}}}}
{5 * {4 * {3 * {2 * 1}}}}
{5 * {4 * {3 * 2}}}
{5 * {4 * 6}}
{5 * 24}
120
TailRescuvie(5) // 所以在运算上和内存占用上节省了很多,直接传回结果
TailRescuvie(5, 1) return 120
↑
TailRescuvie(4, 5) return 120
↑
TailRescuvie(3, 20) return 120
↑
TailRescuvie(2, 60) return 120
↑
TailRescuvie(1, 120) return 120
↑
120 //当运行到最后时,return a => return 120 ,将120返回上一级
尾递归的效果就是去除了将下层的结果再次返回给上层,需要上层继续计算才得出结果的弊端,如果仔细观看例子就可以看出,其实每个递归的结果是存储在第二个参数a中的,到最后一次计算的时候,会只返回一个a的值,但是因为是递归的原理虽然仍然要返回给上层,依次到顶部才给出结果,但是不需要再做计算了,这点的好处就是每次分配的内存不会因为递归而扩大。
在效率上,两者的确差不多。
让我们先回顾一下函数调用的大概过程:
1)调用开始前,调用方(或函数本身)会往栈上压相关的数据,参数,返回地址,局部变量等。
2)执行函数。
3)清理栈上相关的数据,返回。
因此,在函数 A 执行的时候,如果在第二步中,它又调用了另一个函数 B,B 又调用 C.... 栈就会不断地增长不断地装入数据,当这个调用链很深的时候,栈很容易就满 了,这就是一般递归函数所容易面临的大问题。
一直在强调,尾递归的实现依赖于编译器的帮助(或者说语言的规定),为什么这样说呢?先看下面的程序:
1 #include
2
3 int tail_func(int n, int res)
4 {
5 if (n <= 1) return res;
6
7 return tail_func(n - 1, n * res);
8 }
9
10
11 int main()
12 {
13 int dummy[1024*1024]; // 尽可能占用栈。
14
15 tail_func(2048*2048, 1);
16
17 return 1;
18 }
上面这个程序在开了编译优化和没开编译优化的情况下编出来的结果是不一样的,如果不开启优化,直接 gcc -o tr func_tail.c 编译然后运行的话,程序会爆栈崩溃,但如果开优化的话:gcc -o tr -O2 func_tail.c,上面的程序最后就能正常运行。
这里面的原因就在于,尾递归的写法只是具备了使当前函数在调用下一个函数前把当前占有的栈销毁,但是会不会真的这样做,是要具体看编译器是否最终这样做,如果在语言层面上,没有规定要优化这种尾调用,那编译器就可以有自己的选择来做不同的实现,在这种情况下,尾递归就不一定能解决一般递归的问题。
我们可以先看看上面的例子在开优化与没开优化的情况下,编译出来的汇编代码有什么不同,首先是没开优化编译出来的汇编tail_func:
1 .LFB3:
2 pushq %rbp
3 .LCFI3:
4 movq %rsp, %rbp
5 .LCFI4:
6 subq $16, %rsp
7 .LCFI5:
8 movl %edi, -4(%rbp)
9 movl %esi, -8(%rbp)
10 cmpl $1, -4(%rbp)
11 jg .L4
12 movl -8(%rbp), %eax
13 movl %eax, -12(%rbp)
14 jmp .L3
15 .L4:
16 movl -8(%rbp), %eax
17 movl %eax, %esi
18 imull -4(%rbp), %esi
19 movl -4(%rbp), %edi
20 decl %edi
21 call tail_func
22 movl %eax, -12(%rbp)
23 .L3:
24 movl -12(%rbp), %eax
25 leave
26 ret
注意上面标红色的一条语句,call 指令就是直接进行了函数调用,它会先压栈,然后再 jmp 去 tail_func,而当前的栈还在用!就是说,尾递归的作用没有发挥。
再看看开了优化得到的汇编:
1 tail_func:
2 .LFB13:
3 cmpl $1, %edi
4 jle .L8
5 .p2align 4,,7
6 .L9:
7 imull %edi, %esi
8 decl %edi
9 cmpl $1, %edi
10 jg .L9
11 .L8:
12 movl %esi, %eax
13 ret
注意第7,第10行,尤其是第10行!tail_func() 里面没有函数调用!它只是把当前函数的第二个参数改了一下,直接就又跳到函数开始的地方。此处的实现本质其实就是:下一个函数调用继续延用了当前函数的栈!
这就是尾递归所能带来的效果: 控制栈的增长,且减少压栈,程序运行的效率也可能更高!
上面所写的是 c 的实现,正如前面所说的,这并不是所有语言都摆支持,有些语言,比如说 python, 尾递归的写法在 python 上就没有任何作用,该爆的时候还是会爆。
def func(n, res):
if (n <= 1):
return res
return func(n-1, n*res)
if __name__ =='__main__':
print func(4096, 1)
不仅仅是 python,据说 C# 也不支持,我在网上搜到了这个链接:https://connect.microsoft.com/VisualStudio/feedback/details/166013/c-compiler-should-optimize-tail-calls,微软的人在上面回答说,实现这个优化有些问题需要处理,并不是想像中那么容易,因此暂时没有实现,但是这个回答是在2007年的时候了,到现在岁月变迁,不知支持了没?我看老赵写的尾递归博客是在2009年,用 c# 作的例子,估计现在 c# 是支持这个优化的了(待考).
前面的讨论一直都集中在尾递归上,这其实有些狭隘,尾递归的优化属于尾调用优化这个大范畴,所谓尾调用,形式它与尾递归很像,都是一个函数内最后一个动作是调用下一个函数,不同的只是调用的是谁,显然尾递归只是尾调用的一个特例。
int func1(int a) { static int b = 3; return a + b; } int func2(int c) { static int b = 2; return func1(c+b); }
上面例子中,func2在调用func1之前显然也是可以完全丢掉自己占有的栈空间的,原因与尾递归一样,因此理论上也是可以进行优化的,而事实上这种优化也一直是程序编译优化里的一个常见选项,甚至很多的语言在标准里就直接要求要对尾调用进行优化,原因很明显,尾调用在程序里是经常出现的,优化它不仅能减少栈空间使用,通常也能给程序运行效率带来比较大的提升。
漫谈递归:尾递归与CPS
php可以采用函数自行优化,可以参考
http://www.nowamagic.net/librarys/veda/detail/2334