需要用到遞歸的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、R12、R13、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--; } }
