遞歸爲何那麼慢?遞歸的改進算法

不知道你們發現沒有,執行遞歸算法,特別是遞歸執行層數多的時候,結果極其的慢,並且遞歸層數達到必定的值,還可能出現內存溢出的狀況。本文就要將爲你解釋緣由和對應的解決方案。算法

1、遞歸與循環

1.1 所謂的遞歸慢究竟是什麼緣由呢?

你們都知道遞歸的實現是經過調用函數自己,函數調用的時候,每次調用時要作地址保存,參數傳遞等,這是經過一個遞歸工做棧實現的。具體是每次調用函數自己要保存的內容包括:局部變量、形參、調用函數地址、返回值。那麼,若是遞歸調用N次,就要分配N局部變量、N形參、N調用函數地址、N返回值,這勢必是影響效率的,同時,這也是內存溢出的緣由,由於積累了大量的中間變量沒法釋放。函數

1.2 用循環效率會比遞歸效率高嗎?

遞歸與循環是兩種不一樣的解決問題的典型思路。固然也並非說循環效率就必定比遞歸高,遞歸和循環是兩碼事,遞歸帶有棧操做,循環則不必定,兩個概念不是一個層次,不一樣場景作不一樣的嘗試。性能

1.3 那麼遞歸使用的棧是什麼樣的一個棧呢?

首先,看一下系統棧和用戶棧的用途。優化

2.1 遞歸算法:

優勢:代碼簡潔、清晰,而且容易驗證正確性。(若是你真的理解了算法的話,不然你更暈)ui

缺點:它的運行須要較屢次數的函數調用,若是調用層數比較深,須要增長額外的堆棧處理(還有可能出現堆棧溢出的狀況),好比參數傳遞須要壓棧等操做,會對執行效率有必定影響。可是,對於某些問題,若是不使用遞歸,那將是極端難看的代碼。操作系統

2.2 循環算法:

優勢:速度快,結構簡單。code

缺點:並不能解決全部的問題。有的問題適合使用遞歸而不是循環。若是使用循環並不困難的話,最好使用循環。排序

2.3 遞歸算法和循環算法總結:

1) 通常遞歸調用能夠處理的算法,也能夠經過循環去解決,常須要額外的低效處理。遞歸

2)如今的編譯器在優化後,對於屢次調用的函數處理會有很是好的效率優化,效率未必低於循環。進程

3) 遞歸和循環二者徹底能夠互換。若是用到遞歸的地方能夠很方便使用循環替換,而不影響程序的閱讀,那麼替換成遞歸每每是好的。(例如:求階乘的遞歸實現與循環實現。)

3.1 系統棧(也叫核心棧、內核棧)

是內存中屬於操做系統空間的一塊區域,其主要用途爲:
1)保存中斷現場,對於嵌套中斷,被中斷程序的現場信息依次壓入系統棧,中斷返回時逆序彈出;
2)保存操做系統子程序間相互調用的參數、返回值、返回點以及子程序(函數)的局部變量。

3.2 用戶棧

是用戶進程空間中的一塊區域,用於保存用戶進程的子程序間相互調用的參數、返回值、返回點以及子程序(函數)的局部變量。

咱們編寫的遞歸程序屬於用戶程序,所以使用的是用戶棧。

2、遞歸與尾遞歸

以上初略介紹了遞歸與循環的實現機理,彷佛代碼簡潔和效率不能共存。那麼有沒有一種方法能擁有遞歸代碼簡潔的好處,同時給咱們帶來更快的速率麼?算法的世界會告訴你,一切皆有可能。它的名字叫作尾遞歸。

讓遞歸和尾遞歸來作一個對比吧。

2.1 遞歸

用線性遞歸實現Fibonacci函數,程序以下所示:

int FibonacciRecursive(int n)
{
     if( n < 2)
         return n;
     return (FibonacciRecursive(n-1)+FibonacciRecursive(n-2));
}

遞歸寫的代碼很是容易懂,徹底是根據函數的條件進行選擇計算機步驟。例如如今要計算n=5時的值,遞歸調用過程以下圖所示,能夠看出,程序向下遞歸,向上返回,因此每一步都須要存儲中間變量和過程。

2.2 尾遞歸

顧名思義,尾遞歸就是從最後開始計算, 每遞歸一次就算出相應的結果, 也就是說, 函數調用出如今調用者函數的尾部, 由於是尾部, 因此根本沒有必要去保存任何局部變量。直接讓被調用的函數返回時越過調用者, 返回到調用者的調用者去。尾遞歸就是把當前的運算結果(或路徑)放在參數裏傳給下層函數,深層函數所面對的不是愈來愈簡單的問題,而是愈來愈複雜的問題,由於參數裏帶有前面若干步的運算路徑。

尾遞歸是極其重要的,不用尾遞歸,函數的堆棧耗用難以估量,須要保存不少中間函數的堆棧。好比f(n, sum) = f(n-1) + value(n) + sum,會保存n個函數調用堆棧,而使用尾遞歸f(n, sum) = f(n-1, sum+value(n)),這樣則只保留後一個函數堆棧便可。

採用尾遞歸實現Fibonacci函數,程序以下所示:

int FibonacciTailRecursive(int n,int ret1,int ret2)
{
   if(n==0)
      return ret1; 
    return FibonacciTailRecursive(n-1,ret2,ret1+ret2);
}

例如如今要計算n=5時的值,尾遞歸調用過程以下圖所示:

從圖能夠看出,尾遞歸不須要向上返回了,可是須要引入額外的兩個空間來保持當前的結果,這樣減小了中間變量的存儲和返回,大大提高了效率,並且避免了內存溢出。

3、觸類旁通

相信不少讀者對於快速排序都耳熟能詳,不知道各位還記得快速排序的實現就是基於遞歸實現的麼,因而這裏就提供了一種優化快速排序的方案,固然尾遞歸不能改變快速排序的時間複雜度,可是提高性能仍是沒問題的。筆者再也不作詳細介紹,只貼上實現代碼,留給各位獨立思考的空間。

int Partition(int *p,int len,int start,int last)  
{  
    int flag=*(p+start);  
    int i=start;  
    int j=last;  
    while(i<j)  
    {  
        while(i<j && *(p+j)>flag) --j;  
        *(p+i)=*(p+j);  
        while(i<j  && *(p+i)<=flag) ++i;  
        *(p+j)=*(p+i);  
    }  
    *(p+i)=flag;  
    return i;     
}  
  
void QuickSort(int *p,int len,int start,int last)  
{  
   if(NULL=p) return;  
   int index;  
   while(start<last)  
   {  
     index=Partition(p,len,start,last);  
 
     QuickSort(p,len,start,index-1);  
     //QuickSort(p,len,index+1,last);   /**遞歸調用*/
     start=index+1;   /**尾遞歸調用*/
   }          
}
相關文章
相關標籤/搜索