基本遞迴
首先我們通過遞迴的方式去求解n!。函式可以定義成如下形式:
圖3-1展示了利用遞迴的方法計算的4!過程。它也勾畫出了遞迴過程中的兩個基本階段:遞推與回歸。在遞推階段,每乙個遞迴呼叫通過進一步呼叫自己來記住這次遞迴過程。當其中有呼叫滿足終止條件時,遞推結束。比如,在計算n的階乘時,終止條件是當n=1和n=0,此時函式只須簡單地返回1即可。每乙個遞迴函式都必須擁有至少乙個終止條件;否則,遞推階段就永遠不會結束了。一旦遞推階段結束,處理過程就進入回歸階段,在這之前的函式呼叫以逆序的方式回歸,直到最初呼叫的函式返回為止,此時遞迴過程結束。
圖3-1:以遞迴的方式計算4的階乘
示例3-1展示了乙個c函式fact,它接受乙個整數n作為引數,以遞迴的方式計算n的階乘。該函式按照如下的方式工作:如果n小於0,該函式直接返回0,這代表乙個錯誤。如果n等於0或者1,該函式返回1,這是因為o!和1!都等於1,以上就是終止遞迴的條件。否則,函式返回n-1的階乘的n倍。而n-1的階乘又會以遞迴的方式再次呼叫fact來計算,如此繼續。注意觀察遞迴實現與我們之前對遞迴的定義之間的相同點。
示例3-1:以遞迴方式計算階乘的函式實現
/* fact.c */
#include "fact.h"
/* fact */
int fact(int n)
回到示例3-1,考慮一下當計算4!時棧中都發生了些什麼。初始呼叫fact會在棧中產生乙個活躍記錄,輸入引數n=4(見圖3-3,第1步)。由於這個呼叫沒有滿足函式的終止條件,因此fact將繼續以n=3為引數遞迴呼叫。這將在棧上建立另乙個活躍記錄,但這次輸入引數(見圖3-3,第2步)。這裡,n=3也是第乙個活躍期中的輸出引數,因為正是在第乙個活躍期內呼叫fact產生了第二個活躍期。這個過程將一直繼續,直到n的值變為1,此時滿足終止條件,fact將返回1(見圖3-3,第4步)。
圖3-3:遞迴計算4!時的c程式的棧
尾遞迴如果乙個函式中所有遞迴形式的呼叫都出現在函式的末尾,我們稱這個遞迴函式是尾遞迴的。當遞迴呼叫是整個函式體中最後執行的語句且它的返回值不屬於表示式的一部分時,這個遞迴呼叫就是尾遞迴。尾遞迴函式的特點是在回歸過程中不用做任何操作,這個特性很重要,因為大多數現代的編譯器會利用這種特點自動生成優化的**。
當編譯器檢測到乙個函式呼叫是尾遞迴的時候,它就覆蓋當前的活躍記錄而不是在棧中去建立乙個新的。編譯器可以做到這點,因為遞迴呼叫是當前活躍期內最後一條待執行的語句,於是當這個呼叫返回時棧幀中並沒有其他事情可做,因此也就沒有儲存棧幀的必要了。通過覆蓋當前的棧幀而不是在其之上重新新增乙個,這樣所使用的棧空間就大大縮減了,這使得實際的執行效率會變得更高。因此,只要有可能我們就需要將遞迴函式寫成尾遞迴的形式。
為了理解尾遞迴是如何工作的,讓我們再次以遞迴的形式計算階乘。首先,這可以很容易讓我們理解為什麼之前所定義的遞迴不是尾遞迴。回憶之前對計算n!的定義:在每個活躍期計算n倍的(n-1)!的值,讓n=n-1並持續這個過程直到n=1為止。這種定義不是尾遞迴的,因為每個活躍期的返回值都依賴於用n乘以下乙個活躍期的返回值,因此每次呼叫產生的棧幀將不得不儲存在棧上直到下乙個子呼叫的返回值確定。現在讓我們考慮以尾遞迴的形式來定義計算n!的過程。函式可以定義成如下形式:
這種定義還需要接受第二個引數a,除此之外並沒有太大區別。a(初始化為1)維護遞迴層次的深度。這就讓我們避免了每次還需要將返回值再乘以n。然而,在每次遞迴呼叫中,令a=na並且n=n-1。繼續遞迴呼叫,直到n=1,這滿足結束條件,此時直接返回a即可。圖3-4說明了用尾遞迴計算4!的過程。注意在回歸的過程中不需要做任何操作,這是所有尾遞迴函式的標誌。
圖3-4:以尾遞迴的方式計算4!
**例項3-2給出了乙個c函式facttail,它接受乙個整數n並以尾遞迴的形式計算n的階乘。這個函式還接受乙個引數a,a的初始值為1。facttail使用a來維護遞迴層次的深度,除此之外它和fact很相似。讀者可以注意一下函式的具體實現和尾遞迴定義的相似之處。
示例3-2:以尾遞迴的形式計算階乘的乙個函式實現
/* facttail.c */
#include "facttail.h"
/* facttail */
int facttail(int n, int a)
示例3-2中的函式是尾遞迴的,因為對facttail的單次遞迴呼叫是函式返回前最後執行的一條語句。在facttail中碰巧最後一條語句也是對facttail的呼叫,但這並不是必需的。換句話說,在遞迴呼叫之後還可以有其他的語句執行,只是它們只能在遞迴呼叫沒有執行時才可以執行。圖3-5展示了當使用尾遞迴函式計算4!時棧使用的情況,讀者可以和圖3-3展示的棧使用情況作對比。
圖3-5:以尾遞迴形式計算4!時棧的情況
遞迴與尾遞迴
1 遞迴 關於遞迴的概念,我們都不陌生。簡單的來說遞迴就是乙個函式直接或間接地呼叫自身,是為直接或間接遞迴。一般來說,遞迴需要有邊界條件 遞迴前進段和遞迴返回段。當邊界條件不滿足時,遞迴前進 當邊界條件滿足時,遞迴返回。用遞迴需要注意以下兩點 1 遞迴就是在過程或函式裡呼叫自身。2 在使用遞迴策略時...
遞迴與尾遞迴
1 遞迴 簡單的來說遞迴就是乙個函式直接或間接地呼叫自身,是為直接或間接遞迴。一般來說,遞迴需要有邊界條件 遞迴前進段和遞迴返回段。當邊界條件不滿足時,遞迴前進 當邊界條件滿足時,遞迴返回。用遞迴需要注意以下兩點 1 遞迴就是在過程或函式裡呼叫自身。2 在使用遞迴策略時,必須有乙個明確的遞迴結束條件...
遞迴與尾遞迴
前言 今天上網看帖子的時候,看到關於尾遞迴的應用 大腦中感覺這個詞好像在 見過,但是又想不起來具體是怎麼回事。如是乎,在網上搜了一下,頓時豁然開朗,知道尾遞迴是怎麼回事了。下面就遞迴與尾遞迴進行總結,以方便日後在工作中使用。1 遞迴 關於遞迴的概念,我們都不陌生。簡單的來說遞迴就是乙個函式直接或間接...