遞歸的實現原理

須要用到遞歸的3種狀況:
(1)定義是遞歸的
例如計算階乘的遞歸函數
longFactorial(longn){
    if(n==0) return1;
    elsereturnn*Factorial(n-1);
}
(2)數據結構是遞歸的
例如搜索單鏈表最後一個結點的算法
LinkNode *FindRear(LinkNode *f){
    if(f==NULL) returnNULL;
    elseif(f->link==NULL) returnf;
    elsereturnFindRear(f->link);
}
在單鏈表中搜索值等於x的結點
voidSearch(LinkNode *f,T& x){
    if(f==NULL) return;
    elseif(f->data==x) returnf;
    elsereturnSearch(f->link,x); 
}
(3)問題的解法是遞歸的
例如如漢諾塔問題:先將n-1個盤子移動到b柱子,再把最下面的盤子移動到c柱,再把n-1個盤子移動到c柱。T(n)=2T(n-1)+1=2 n-1。
又例如展轉求餘法求724和344的最大公約數:
int GCD(int m , int n){
    if(m<0) m=-m;
    if(n<0) n=-n;
    if(n==0) return m;
    return GCD(n , m%n);
}
GCD(724 , 344)=GCD(344 , 36)=GCD(36 , 20)=GCD(20 , 16)=GCD(16 ,4)=GCD(4 , 0)=4
能夠這麼遞歸的緣由:
假設a=qb+r,r=a%b
若a和b有公因子d(d|a且d|b),則d也是a-qb=r的因子,故d是b和r的公因子(d|b且d|r)
若b和r有公因子d(d|b且d|r),則d也是r+qb=a的因子,故d是a和b的公因子(d|a且d|b)
所以a和b的公因子集合、b和r的公因子集合是相同的
 
遞歸工做棧
IA-32使用棧來支持過程的嵌套調用。每一個過程都有本身的棧區,稱爲棧幀(stack frame) 。所以,一個棧由若干棧幀組成,每一個棧幀用專門的幀指針寄存器EBP指定起始位置, 當前棧幀的範圍在其和棧指針寄存器ESP指向區域之間。
IA-32規定,寄存器EAX、ECX和EDX是調用者保存寄存器。當過程P調用過程Q時,Q 能夠直接使用這三個寄存器,不用將它們的值保存到棧中,這也意味着,若是P在從Q返回後還要用這三個寄存器的話,P應在轉到Q以前先保存它們的值,並在從Q返回後先恢復它們的值再使用。寄存器EBX、ESl、EDI是被調用者保存寄存器,Q必須先將它們的值保存到棧中再使用它們,並在返回P以前先恢復 它們的值。
(1)每次遞歸調用前,先將 參數n~參數1按序複製到調用過程棧幀中
(2)執行call指令:首先將返回地址(call指令要執行時EIP的值,即call指令下一條指令的地址)壓入棧頂,而後將程序跳轉到當前調用的方法的起始地址,至關於執行了push和jump指令。
遞歸調用時,每一層調用過程棧幀中存放的返回地址都是相同的。
(3)每次遞歸,一定要先push %ebp(把原幀指針保存在棧頂)和mov %esp,%ebp(把存放原幀指針的棧頂,設置爲新棧底)
被調用者定義的非靜態局部變量僅存放於當前棧幀,調用結束後就被釋放了。
最後每每經過EAX寄存器將結果返回給調用者。
(4)執行leave指令:將棧指針指向幀指針,而後pop備份棧頂存放的原幀指針到EBP。
(5)最後執行ret指令:將棧頂的返回地址彈出到EIP,而後按照EIP此時指示的指令繼續執行程序。
如圖所示,Q的過程體執行時,入口參數1的地址老是R[ebp]+8,入口參數2的地址老是R[ebp]+12……(在棧中傳遞的參數如果基本類型,則都被分配4個字節)
與IA-32不一樣,x86-64最多可有6個整型或指針型參數經過寄存器傳遞,超過6個入口參數時,後面的經過棧來傳遞。在棧中傳遞的參數如果基本類型,則都被分配8個字節。棧中的地址也變爲了8個字節。
RAX、R10和R11爲調用者保存寄存器。RBX、RBP、R十二、R1三、R14和R15爲被調用者保存寄存器,須要將它們先保存在棧中再使用,最後返回前再恢復其值。
過程調用中使用的棧機制和寄存器使用約定,使得能夠進行過程的嵌套調用和遞歸調用。
理解了遞歸的實現原理後,對於遞歸過程,就能夠用棧將它改成非遞歸過程,如用棧幫助求解斐波那契函數的非遞歸算法
struct Node{   //棧結點的類定義
    longn;    //記憶走過的n
    inttag;   //區分左右遞歸的標誌
}
longFibnacci(longn){
    Stack<Node> S ;
    Node *w;
    longsum=0;
    do{
        while(n>1){
            w->n=n;
            w->tag=1;
            S.push(w);
            n--;
        }
        sum=sum+n;
        while(!S.IsEmpty()){
            S.Pop(w);
            if(w->tag==1){    //tag==1表示向左遞歸
                w->tag=2;     //tag==2表示向右遞歸
                S.push(w);
                n=w->n-2;
                break;
            }
        }
    }while(!S.IsEmpty());
}
直接用遞歸法、或是藉助棧求解斐波那契函數的時間複雜度是O(2 n),所以可改用迭代法
longFibIter(longn){
    if(n<=1) returnn;
    longtwoback=0,oneback=1,Current;
    for(i=2;i<=n;i++){
        Current=twoback+oneback;  //計算Fib(i-2)+Fib(i-1)的值
        twoback=oneback;     //把Fib(i-1)的值保存做爲下一次的Fib(i-2)
        oneback=Current;     //把Fib(i)的值保存做爲下一次的Fib(i-1)
    }
    returnCurrent;
}
 
若是以爲斐波那契函數的非遞歸算法很差理解,能夠舉一個更簡單的例子:
逆向打印數組A[]中數值的遞歸算法
voidrecfunc(intA[],intn){
    if(n>=0){
        cout<<A[n]<<",";
        n--;
        recfunc(A,n);
    }
}
改用迭代算法
voiditerfunc(intA[],intn){
    while(n>=0){
        count<<A[n]<<",";
        n--;
    }
}
相關文章
相關標籤/搜索