簡體   English   中英

為什么階乘遞歸函數的效率低於正常的階乘函數?

[英]Why is factorial recursive function less efficient than a normal factorial function?

我有兩個函數來計算數n的階乘。 我不明白為什么'正常'函數需要更少的時間來計算數n的階乘。 這是正常的功能:

double factorial(int n) {
    double s = 1;
    while (n > 1) {
        s *= n;
        --n;        
    }

    return s;
}

這是遞歸函數:

double factorial(int n) {
    if (n < 2) return 1;
    return n * factorial(n-1);
}

這應該不那么耗時,因為它不會創建新的變量,並且操作更少。 雖然普通函數確實使用了更多的內存,但速度更快。

我應該使用哪一個?為什么?

PS:我正在使用雙,因為我需要它來計算e ^ x的泰勒級數。

你寫的是遞歸函數“應該不那么耗時,因為它不會創建一個新的變量,並且它會減少操作”。 第一個斷言是毫無意義的。 局部變量的內存通常在進入函數時由單個減法操作分配,這需要不顯着的時間(這是人類已知的最快分配)。 第二個斷言對於C ++實現來說是完全錯誤的。 由於您已經測量過,使用編譯器的遞歸函數較慢,因此它會做得更多,而不是更少。

現在,為什么。

好吧,每次調用都必須復制一個返回地址和堆棧上的實際參數。 這需要時間。 此外,為了支持調試和異常,每個函數調用通常會做一些額外的工作來建立一個很好的堆棧幀 ,實質上存儲有關堆棧在調用之前的信息。

遞歸變種也不過沒有要慢一些。 但幾乎矛盾的是,實際上可以像迭代一樣快的變體看起來會做得更多......想法是編寫它以便編譯器可以將其轉換為迭代版本,也就是說,編譯器可以用簡單的循環替換遞歸調用(這需要時間)。

唯一的問題是,據我所知,如果有任何C ++編譯器進行這種優化,則很少。 :-(

但是,為了完整性,我們的想法是確保只有一個遞歸調用,並且它是最后發生的事情。 這叫做尾遞歸 您當前的遞歸代碼,

double factorial( int n )
{
    if( n < 2 ) { return 1; }
    return n*factorial( n-1 );
}

不是尾遞歸的,因為在遞歸調用之后有乘以n

為了使它具有尾遞歸性,你可以傳遞必要的信息來完成應該在最后完成的事情,這里是*n 所需的信息是n的值,加上它應該完成的事實。 這意味着引入一個帶有適當形式參數的輔助函數:

double factorialScaledBy( double m, int n )
{
    if( n < 2 ) { return m*1; }

    // Same as "n*factorialScaledBy( m, n-1 )", but tail-recursive:
    return factorialScaledBy( n*m, n-1 );  
}

double factorial( int n )
{
    return factorialScaledBy( 1, n );
}

現在一個足夠聰明的編譯器可以注意到在遞歸調用之后在函數執行中不再發生任何事情,因此不使用局部變量,因此它們可以僅用於遞歸調用,因此可以實現為模擬參數傳遞加上跳回函數的頂部,即循環。

干杯&hth。,

我想說這是因為函數調用的時間比while循環更昂貴。 我會使用第一個(沒有遞歸)好像N非常大,你將填滿你的堆棧並可能得到“堆棧溢出”:)

你最好的選擇是不要明確地計算階乘。 如果你正在計算一個exp(x)的泰勒(Maclaurin)系列:

   exp(x) = 1 + x/1! + x^2/2! + x^3/3! + x^4/4! + ...

您最好的選擇是執行以下操作:

   double y = 1.0;
   int i = 1;
   double curTerm = 1.0;
   double eps = 1e-10;  // whatever's desired
   while( fabs(curTerm) > eps)
   {
        curTerm *= x / (double)i;
        y += curTerm;
        ++i;
   }

通過這種方式,您永遠不必明確計算將會快速增長以對此問題有用的因子。

這當然與數據結構有關。 數據結構很有趣。 其中一些對於較小的數據大小表現良好,而一些對較大的數據大小表現更好。

在遞歸代碼中,有一個調用堆棧,當前遞歸的全部內容被推送到堆棧並在返回的路上被提取。 這是每次遞歸調用的函數調用的額外開銷。 這就是原因,表現緩慢。

有關詳細信息,請參閱此處http//publib.boulder.ibm.com/infocenter/iadthelp/v6r0/topic/com.ibm.etools.iseries.pgmgd.doc/c0925076137.htm

函數調用在時間和空間上花費更多,因為:

  • 需要將參數推送到堆棧並彈出返回值。 這需要時間。
  • 每次調用都會使用自己的堆棧“框架”。
    • 這不僅會阻止您進行非常深的遞歸(堆棧大小有限,通常為幾MB),
    • 它還會傷害你的緩存局部性(因為你在每次調用時都會遇到RAM的不同部分)並且最終也會花費時間。

順便說一句,當你說函數調用“做少操作”時 ,這實際上是不真實的。 函數調用可能看起來在源代碼中短,但之間的事情怎么在外面看起來實際在里面什么區別。

此外,雖然在這種情況下不相關,但“較少的操作”並不總是等於更好的性能。 有時,“更多操作” 但具有更好的局部性可以更好地利用所有現代CPU實現的緩存和預取來隱藏RAM延遲。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM