1. 程式人生 > >見識一下尾遞迴的強大!尾遞迴怎麼會比迭代還快!這不科學

見識一下尾遞迴的強大!尾遞迴怎麼會比迭代還快!這不科學

1.效能測試

    尾遞迴求Fibonaci數列,三種方法分別是:

(1)普通遞迴

(2)尾遞迴

(3)動態規劃

第一種重複計算很多,其他兩種都能避免重複計算

程式碼:

#include <iostream>
#include <sys/time.h>
//#include <boost/xpressive/xpressive.hpp>
using namespace std;
//using namespace boost;
//using namespace boost::xpressive;

int const N=30;
int const TIMES=1000;

//普通遞迴
int fib_r(int n)
{   if(n<=1)return 1;
    return fib_r(n-1)+fib_r(n-2); 
}

//尾遞迴
int fib_rw(int a, int b, int n)
{
    if(n<=1)return b;    
    return fib_rw(b, a+b, n-1);    
}

//動態規劃
int fib_dp(int n)
{
    int re;
    int *p=new int[n+1];
    int i;
    p[0]=p[1]=1;
    for(i=2;i<=n;i++)
        p[i]=p[i-1] + p[i-2];
    re=p[n];
    delete []p;
    return re;
}


////// main
int main()
{
     
    struct timeval begin,end;
    int re;
    
    ////////////////////////
    gettimeofday(&begin,0); //尾遞迴
    {
        int i=TIMES;
        while(--i)
        {
            re=fib_rw(1,1,N);
        }
    }
    
    gettimeofday(&end,0); 
    if(begin.tv_usec>end.tv_usec)  
    {
        end.tv_sec--;
        end.tv_usec+=1000000;
    }
    cout<<"尾遞迴 "<<re<<" time: "<<end.tv_sec - begin.tv_sec<<" s "<<end.tv_usec - begin.tv_usec<<" us"<<endl;
    
    
    ////////////////////////
    gettimeofday(&begin,0); //遞迴
    {
        int i=TIMES;
        while(--i)
        {
            re=fib_r(N);
        }
    }
    
    gettimeofday(&end,0); 
    if(begin.tv_usec>end.tv_usec)  
    {
        end.tv_sec--;
        end.tv_usec+=1000000;
    }
    cout<<"遞迴 "<<re<<" time: "<<end.tv_sec - begin.tv_sec<<" s "<<end.tv_usec - begin.tv_usec<<" us"<<endl;
  
    ////////////////////////
    gettimeofday(&begin,0); //動規
    {
        int i=TIMES;
        while(--i)
        {
            re=fib_dp(N);
        }
    }
    
    gettimeofday(&end,0); 
    if(begin.tv_usec>end.tv_usec)  
    {
        end.tv_sec--;
        end.tv_usec+=1000000;
    }
    cout<<"動規 "<<re<<" time: "<<end.tv_sec - begin.tv_sec<<" s "<<end.tv_usec - begin.tv_usec<<" us"<<endl;
 
    return 0;
}


執行一下,計算第30個元素:

[email protected]:~$ g++ a.cpp -o a
[email protected]:~$ ./a
尾遞迴 1346269 time: 0 s 271 us
遞迴 1346269 time: 23 s 961794 us
動規 1346269 time: 0 s 424 us

尾遞迴和動規的曲線:

可見,這兩個是線性增長~下面的是尾遞迴的。

普通遞迴的曲線:


看到是直的,有沒有覺得很高興?可惜。。縱座標是對數座標!標準的指數式增長。。。過了20以後簡直要等半天啊。

那麼,動規為什麼比尾遞迴慢?把new/delete換成靜態陣列:

將陣列宣告為全域性:

#include <iostream>
#include <sys/time.h>
#include <stdlib.h>
//#include <boost/xpressive/xpressive.hpp>
using namespace std;
//using namespace boost;
//using namespace boost::xpressive;

int const N=20;
int const TIMES=1000;
int p[50]={1,1,};

//普通遞迴
int fib_r(int n)
{   if(n<=1)return 1;
    return fib_r(n-1)+fib_r(n-2); 
}

//尾遞迴
int fib_rw(int a, int b, int n)
{
    if(n<=1)return b;    
    return fib_rw(b, a+b, n-1);    
}

//動態規劃
int fib_dp(int n)
{
    p[0]=p[1]=1;
    int i;
    for(i=2;i<=n;i++)
        p[i]=p[i-1] + p[i-2];
    return p[n];
}


////// main
int main(int argc, char*argv[])
{

    int N=20;
    if(argc>=2)
        N=atoi(argv[1]);
     
    struct timeval begin,end;
    int re;
    
    ////////////////////////
    gettimeofday(&begin,0); //尾遞迴
    {
        int i=TIMES;
        while(--i)
        {
            re=fib_rw(1,1,N);
        }
    }
    
    gettimeofday(&end,0); 
    if(begin.tv_usec>end.tv_usec)  
    {
        end.tv_sec--;
        end.tv_usec+=1000000;
    }
    cout<<"尾遞迴 "<<re<<" time: "<<end.tv_sec - begin.tv_sec<<" s "<<end.tv_usec - begin.tv_usec<<" us"<<endl;
    
    
   /* ////////////////////////
    gettimeofday(&begin,0); //遞迴
    {
        int i=TIMES;
        while(--i)
        {
            re=fib_r(N);
        }
    }
    
    gettimeofday(&end,0); 
    if(begin.tv_usec>end.tv_usec)  
    {
        end.tv_sec--;
        end.tv_usec+=1000000;
    }
    cout<<"遞迴 "<<re<<" time: "<<end.tv_sec - begin.tv_sec<<" s "<<end.tv_usec - begin.tv_usec<<" us"<<endl;
  */
    ////////////////////////
    gettimeofday(&begin,0); //動規
    {
        int i=TIMES;
        while(--i)
        {
            re=fib_dp(N);
        }
    }
    
    gettimeofday(&end,0); 
    if(begin.tv_usec>end.tv_usec)  
    {
        end.tv_sec--;
        end.tv_usec+=1000000;
    }
    cout<<"動規 "<<re<<" time: "<<end.tv_sec - begin.tv_sec<<" s "<<end.tv_usec - begin.tv_usec<<" us"<<endl;
 
    return 0;
}



編譯執行:

[email protected]:~$ g++ a.cpp -o a
[email protected]:~$ ./a 30
尾遞迴 1346269 time: 0 s 285 us
動規 1346269 time: 0 s 232 us

線性性那是槓槓的!這回正常了,動規的迭代比尾遞迴稍快一點點,但是不多。看來,區域性變數的定義也是需要時間的。。。

PS:當然,所謂的動規。。其實是不必要的,完全可以寫為:

int fib_dp(int n)
{
    int a=1;
    int b=1;
    int i;
    for(i=2;i<=n;i++)
    {
        b=a+b;
        a=b-a;
    }
    return b;
}

對於沒有引進中間變數這件事。。我表示乾的很漂亮!。。。當然,本來迴圈中需要執行一次計算,現在變成兩次,時間會上漲那麼一點點。。。不過這樣空間複雜度就下來了。

2.彙編分析

接下來,要做的事情是。。。分析彙編!

1.先是尾遞迴的:

(gdb) disas fib_rw
Dump of assembler code for function fib_rw(int, int, int):
   0x0804871e <+0>:	push   ebp
   0x0804871f <+1>:	mov    ebp,esp
   0x08048721 <+3>:	sub    esp,0x18
   0x08048724 <+6>:	cmp    DWORD PTR [ebp+0x10],0x1                    n和1比較
   0x08048728 <+10>:	jg     0x804872f <fib_rw(int, int, int)+17>            大於1的話,就jmp到下下下行--->
   0x0804872a <+12>:	mov    eax,DWORD PTR [ebp+0xc]                        返回b:eax=b
   0x0804872d <+15>:	jmp    0x8048750 <fib_rw(int, int, int)+50>          跳到leave那裡
   0x0804872f <+17>:	mov    eax,DWORD PTR [ebp+0x10]                      -->  jmp到這裡。n賦值給eax
   0x08048732 <+20>:	lea    edx,[eax-0x1]                                                    edx=eax-1
   0x08048735 <+23>:	mov    eax,DWORD PTR [ebp+0xc]                         eax=b
   0x08048738 <+26>:	mov    ecx,DWORD PTR [ebp+0x8]                         ecx=a
   0x0804873b <+29>:	add    eax,ecx                                                              eax=eax+ecx
   0x0804873d <+31>:	mov    DWORD PTR [esp+0x8],edx                         esp+8  <-- edx     n-1
   0x08048741 <+35>:	mov    DWORD PTR [esp+0x4],eax                         esp+4  <--a+b            
   0x08048745 <+39>:	mov    eax,DWORD PTR [ebp+0xc]                         eax <--   b                    
   0x08048748 <+42>:	mov    DWORD PTR [esp],eax                                  
   0x0804874b <+45>:	call   0x804871e <fib_rw(int, int, int)>                  遞迴呼叫
   0x08048750 <+50>:	leave  
   0x08048751 <+51>:	ret    
End of assembler dump.
程式碼貼上來對比下:

int fib_rw(int a, int b, int n)
{
    if(n<=1)return b;    
    return fib_rw(b, a+b, n-1);    
}

由於引數是 int fib_rw(int a, int b, int n),所以呼叫的時候:

n入棧                                              ebp+10

b入棧                                              ebp+c

a入棧                                              ebp+8

call的時候,eip入棧                    ebp+4

push ebp的時候,ebp入棧,<-----隨後,ebp指向這裡。所以,[ebp+0x10]指向的是n。從彙編程式碼來看,每一次呼叫,棧幀都會sub 0x18,就是二十幾個位元組。

2.是迭代的

(gdb) disas fib_dp
Dump of assembler code for function fib_dp(int):
   0x08048752 <+0>:	push   ebp
   0x08048753 <+1>:	mov    ebp,esp
   0x08048755 <+3>:	sub    esp,0x10
   0x08048758 <+6>:	mov    DWORD PTR [ebp-0xc],0x1                    a
   0x0804875f <+13>:	mov    DWORD PTR [ebp-0x8],0x1                    b
   0x08048766 <+20>:	mov    DWORD PTR [ebp-0x4],0x2                    i
   0x0804876d <+27>:	jmp    0x8048788 <fib_dp(int)+54>                -->jmp to 
   0x0804876f <+29>:	mov    eax,DWORD PTR [ebp-0xc]       eax=a                                                      -->here
   0x08048772 <+32>:	add    DWORD PTR [ebp-0x8],eax       b+=eax     b+=a
   0x08048775 <+35>:	mov    eax,DWORD PTR [ebp-0xc]      eax=a
   0x08048778 <+38>:	mov    edx,DWORD PTR [ebp-0x8]     edx=b
   0x0804877b <+41>:	mov    ecx,edx                                         exc=edx
   0x0804877d <+43>:	sub    ecx,eax                                        ecx - = eax
   0x0804877f <+45>:	mov    eax,ecx                                       eax=ecx
   0x08048781 <+47>:	mov    DWORD PTR [ebp-0xc],eax        a=eax
   0x08048784 <+50>:	add    DWORD PTR [ebp-0x4],0x1       i++
   0x08048788 <+54>:	mov    eax,DWORD PTR [ebp-0x4]                     -->here
   0x0804878b <+57>:	cmp    eax,DWORD PTR [ebp+0x8]              ebp+8是輸入的n
   0x0804878e <+60>:	setle  al                                                            setle是小於等於的比較
   0x08048791 <+63>:	test   al,al
   0x08048793 <+65>:	jne    0x804876f <fib_dp(int)+29>                                                                 -->jmp
   0x08048795 <+67>:	mov    eax,DWORD PTR [ebp-0x8]
---Type <return> to continue, or q <return> to quit---
   0x08048798 <+70>:	leave  
   0x08048799 <+71>:	ret    
End of assembler dump.

sub esp,0x10:只用了這麼點空間。記憶體是:

ebp

i=2     ebp-0x4

b=1    ebp-0x8

a=1    ebp-0xc

反正,棧幀是沒有發生生長。尾遞迴比起來,還是具有O(n)的空間複雜度的。迭代則可以避免(如果不用陣列的話)