須要用到遞歸的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--;
}
}