尾遞迴(tail recursion) 的簡單使用

YunShell發表於2014-04-04

摘要

       本文首先論述了尾遞迴的定義,然後通過例項進行講解尾遞迴的本質,最後給出了兩個例子完整程式碼。
引導:
         什麼是遞迴?通俗的說,就是在一個函式中不斷呼叫自己的這麼一種呼叫形式。遞迴函式的設計,就是截止條件+呼叫本身。大家都知道,遞迴的資源消耗是發生在棧中,在呼叫函式時候,通常需要儲存相應的資源,比如引數,返回地址,區域性變數等。那麼進行大量的遞迴,就會造成棧的溢位。windows下棧為 1M。那麼是什麼導致了溢位呢?就是在進行遞迴的時候,本身遞迴函式的狀態,沒有完全執行完畢,需要藉助於下一個狀態來完成,那麼上一個狀態必然要進行儲存。這樣就導致了大量的中間過程。才肯能在回溯遞迴的時候,一一使用,最後拼湊出原來的結果。
比如階乘函式:
int factorial(int n)
{
	if(n==0)
		return 1;
	else 
		return n*factorial(n-1);
}
        在上述的函式中,返回的值並不是一個完全獨立的狀態,而需要儲存n這樣一種額外需維護的變數,每層遞迴都需要這樣一個變數,那麼在下次呼叫的時候,就會重新建立新的棧幀來進行儲存。每次都是這樣。所以對於大數而言,就可能溢位了。那麼仔細觀察這種遞迴的本質,就是因為建立了很多中間過程需要儲存,才導致了這樣一種空間的浪費。那麼我們在設計遞迴函式的時候,能不能直接省略中間變數,直接返回結果呢?下面就是尾遞迴的設計思路了。
什麼是尾遞迴:
      本質就是不產生出中間過程,將中間過程作為函式的引數,傳入到下次呼叫中去。比如上面階乘函式,使用尾遞迴設計後。
int factorial_tailcursion(int n,int acc)
{
	if(n==0)
		return acc;
	else 
		return factorial_tailcursion(n-1,acc*n);
}
      在呼叫的時候 factorial_tailcursion(n,1) 需要一個初始累計器acc=1。可以看出,在每次呼叫完畢尾遞迴形式的階乘函式後,根本沒有產生人中間過程,中間變數都通過引數傳遞給下一次呼叫。從而節省了棧的開銷。因為對於某些編譯器而言,會自動識別到這一動作,並且認為每次呼叫過程都是獨立的,那麼就不需要每次都開闢新的 棧幀來儲存當前值,而是直接就地覆蓋調之前的棧。那麼其空間複雜度將為O(1)。這就是尾遞迴的本質。說白了,也就是在進行遞迴設計的時候,儘量省略中間過程。
下面是兩個例子的完整程式碼:
/***********************************************************************
two examples of tail recursion 
1. factorial 
2. fibonacci
*********************************************************************/
#include <iostream>
using namespace std;
int factorial(int n) // usual recursion call
{
	if(n==0)
		return 1;
	else 
		return n*factorial(n-1);
}

int factorial_tailrecursion(int n,int acc) // tail recursion call
{
	if(n==0)
		return acc;
	else 
		return factorial_tailrecursion(n-1,acc*n);
}

int fibinacci(int n) 
{
	if(n<2)
		return n;
	else
		return fibinacci(n-1)+fibinacci(n-2);
}
int  fibinacci_tailrecursion(int n,int acc1,int acc2)
{
	if(n==0)
		return acc1;
	else 
		return fibinacci_tailrecursion(n-1,acc1+acc2,acc1);
}
int main()
{	
	puts("usual factor recursion 4!");
	cout<<factorial(4)<<endl;
	puts("tail recursion 4!");
	cout<<factorial_tailrecursion(4,1)<<endl;
	puts("usual fibinacc ");
	for(int i=0;i<10;i++)
		cout<<fibinacci(i)<<" ";
	cout<<endl;
	puts("tail recursion ");
	for(int i=0;i<10;i++)
		cout<<fibinacci_tailrecursion(i,0,1)<<" ";
	return 0;
}


小結:

         遞迴函式的使用,無疑對一些複雜的問題,可以清晰的反應其本質,使得易於理解。everything is banlance!有利必有弊,我們必須儘量減少這中弊。

   更詳細的參考:老趙這篇文章

相關文章