1. 程式人生 > >【程式設計學習記錄】遞迴轉非遞迴

【程式設計學習記錄】遞迴轉非遞迴

想要知道怎麼遞迴轉非遞迴,就得先弄明白遞迴函式呼叫和返回的步驟(來源於網課):

呼叫

  • 儲存呼叫資訊(引數,返回地址)
  • 分配資料區(區域性變數)
  • 控制轉移給被調函式的入口

返回

  • 儲存返回資訊
  • 釋放資料區
  • 控制轉移到上級函式

因為遞迴滿足LIFO,所以用棧來實現

遞迴轉非遞迴方法

最機械的轉換方法:

設定t+2個語句標號,t為函式遞迴呼叫的次數,令第0個標號為遞迴入口,1到t標記各個遞迴規則,t+1標記遞迴出口;通過入棧出棧來模擬實際遞迴時系統壓棧彈棧的過程。最最機械的部分,則是真的用label在各處做標記,然後用goto語句模擬實現遞迴過程。

比如,拿求n的階乘來舉例子:

正常遞迴寫法:

int factorial(int n)
{
	if (n == 1){
		return 1;
	} else 
		return n*factorial(n-1);
}

可以看到其中只有一個遞迴規則,只有一處遞迴呼叫,我們稍微轉化一下可以得到:

void factorial(int n, int& f)
{
	int u;
	if (n == 1){
		f = 1;
	} else{
		factorial(n-1, u);
		f = n*u;
	}
}

即:

f(n)=\left\{\begin{matrix} 1 & n=1\\ n*f(n-1)&n>1 \end{matrix}\right.

開始設定標號:

  • label 0 :第一個可執行語句
  • label t+1 :設在函式體結束處
  • label i (1<=i<=t): 第i個遞迴返回處

程式碼如下:

void factorial(int n, int& f)
{
	fac x, tmp;
	x.rd = 2;
	x.pn = n;
	S.push(x);//壓入棧底作為監視哨
	label0: if ((x = S.top()).pn == 1){//遞迴入口
			S.pop();
			x.pf = 1;
			S.push(x);
			goto label2;
	}
	//第一處也是本程式碼唯一一處遞迴
	x.rd = 1;
	x.pn = x.pn-1;
	S.push(x);
	goto label0;
	label1: tmp = S.top(); S.pop();
			x = S.top(); S.pop();
			x.u = tmp.pf;
			x.pf = x.pn * x.u;
			S.push(x);
	//遞迴出口
	label2: x = S.top();
			switch (x.rd){
				case 0: goto label0; break;
				case 1: goto label1; break;
				case 2: tmp = S.top();
						S.pop();
						f = tmp.pf;
						break;
			}
}

但是會有一些冗餘的入棧操作,並且畢竟goto語句現在也不大推薦使用了,比較容易出問題,於是有了優化後的程式碼:

void factorial(int n, int& f)
{
	fac x, tmp;
	x.rd = 2;
	x.pn = n;
	S.push(x);//監視哨
	do{
		//入棧
		while ((x = S.top()).pn > 1){
			x.rd = 1;
			x.pn = x.pn-1;
			S.push(x);
		}
		x = S.top();
		S.pop();
		x.pf = 1;
		S.push(x);
		//開始上升
		while ((x = S.top()).rd == 1){
			tmp = S.top(); S.pop();
			x = S.top(); S.pop();
			x.pf = x.pn * tmp.pf;
			S.push(x);
		}
	}while ((x = S.top()).rd != 2);
	S.pop();
	f = x.pf;
}

有兩處遞迴呼叫的程式也類似,可以選擇從某一個遞迴語句開始入棧,從另一個上升,大家自己去摸索吧!溜了溜了