C-遞歸

遞歸這個詞,生活中應該比較少用到,你可能對它比較陌生,而本文的主題就是它。舉個從小就聽過的例子:從前有座山,山里有座廟,廟里有個和尚,和尚在講故事,從前有座山,山里有座廟...

再舉個例子,下圖就可以看做近似的遞歸,

![在這里插入圖片描述](https://img-blog.csdnimg.cn/2019092211405895.png)

再來看一下百度對遞歸詞匯的解釋,

![在這里插入圖片描述](https://img-blog.csdnimg.cn/20190922114411872.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0JhZEF5YXNl,size_16,color_FFFFFF,t_70)

相信通過上面的解釋,大家對遞歸有一定的認識了。接下來,我們正式來講講程序設計中的遞歸函數(shù)。

遞歸函數(shù)就是函數(shù)直接或者間接調(diào)用自身的函數(shù)。許多教科書把階乘運算用來說明遞歸,這是非常不幸的。因為這個時候使用,它的效率非常之低。

這里有一個簡單的程序,可用于說明遞歸。程序的目的是把一個整數(shù)轉換為可打印的字符形式。例如,給出一個值4267,我們需要依次產(chǎn)生字符'4'、'2'、'6'、'7'。

我們采用的策略是把這個值反復對10取余得到余數(shù)處理打印后,并除以10。例如,4267除10的余數(shù)是7,但是我們不能直接打印這個余數(shù)。我們需要打印的是機器字符集中表示數(shù)字'7'的值。在ASCII碼中,字符'7'的值是55,所以我們需要在余數(shù)上加上48來獲得正確的字符。但是,使用字符常量而不是整型常量可以提高程序的可移植性??紤]下面的關系:

'0' + 0 = '0'

'0' + 1 = '1'

'0' + 2 = '2'

從這些關系中,我們很容易看出在余數(shù)上機上'0'就可以產(chǎn)生對應字符的代碼。接著就打印出余數(shù)。下一步是取得商,4267/10等于426(整型取整)。然后用這個得到的值重復上述步驟。

這種處理方法存在的問題是它產(chǎn)生的數(shù)字次序正好相反,它們是逆向打印的。在下面的程序中,我們就用遞歸來修正這個問題。

void binary_to_ascii(unsigned int value)

{

? unsigned int quotient = value / 10;

? if (quotient !=0)

? ? binary_to_ascii(quotient);

? ? putchar(value % 10 + '0');

}

上面程序中的函數(shù)是遞歸性質(zhì)的,因為它包含了一個對自身的調(diào)用。乍一看,函數(shù)似乎永遠也不會終止,當函數(shù)調(diào)用時,它將調(diào)用自身,第二次調(diào)用還是調(diào)用自身,以此類推,似乎永遠調(diào)用下去。但是,事實上并不會出現(xiàn)這種情況。

這個程序的遞歸實現(xiàn)了某種類型的螺旋狀while循環(huán)。while循環(huán)在循環(huán)體每次執(zhí)行時必須取得某種進展,逐步迫近循環(huán)終止條件。遞歸函數(shù)也是如此,它在每次遞歸調(diào)用后必須越來越接近某種限制條件。當遞歸函數(shù)符合這個限制條件時,它便不再調(diào)用自身。

該程序中,遞歸函數(shù)的限制條件就是變量quotient為0。在每次遞歸調(diào)用之前,我們都把quotient除以10,所以每遞歸一次,它的值就更接近0。當它最終變成0時,遞歸便告終了。

那么我們再來分析下,遞歸是如何幫我們以正確的順序打印這些字符的呢?下面是這個函數(shù)的工作流程。

1.將參數(shù)值除以10

2.如果quotient的值為非零,調(diào)用binary_to_ascii打印quotient當前值的各位數(shù)字

3.接著,打印步驟1中取余運算的余數(shù)

注意在第二個步驟中,我們要打印的是quotient當前值的各位數(shù)字。我們所面臨的問題和最初的問題完全相同,只是變量quotient的值變小了。我們用剛剛編寫的函數(shù)(把整數(shù)轉換為各個數(shù)字字符并打印出來)來解決這個問題。由于quotient的值越來越小,所以遞歸最終會終止。

一旦你理解了遞歸,閱讀遞歸函數(shù)最容易的方法不是糾纏于它的執(zhí)行過程,而是相信遞歸函數(shù)會順利完成它的任務。如果你的每個步驟正確無誤,你的限制條件設置正確,并且每次調(diào)用之后更接近限制條件,遞歸函數(shù)總是能夠正確地完成任務。

追蹤遞歸函數(shù)

為了理解遞歸的工作原理,你需要追蹤遞歸調(diào)用的執(zhí)行過程,所以讓我們來進行這項工作。追蹤一個遞歸函數(shù)執(zhí)行過程的關鍵是理解函數(shù)中所聲明的變量是如何存儲的。當函數(shù)被調(diào)用時,它的變量的空間是創(chuàng)建于運行的堆棧上的。以前調(diào)用的函數(shù)的變量仍保留在堆棧上,但它們被新函數(shù)的變量所掩蓋,因此是不能訪問的。

當遞歸函數(shù)調(diào)用自身時,情況也是如此。每進行一次新的調(diào)用,都將創(chuàng)建一批變量,它們掩蓋遞歸函數(shù)前一次調(diào)用所創(chuàng)建的變量。當我們追蹤一個遞歸函數(shù)的執(zhí)行過程時,必須把分屬不同次調(diào)用的變量區(qū)分開來,避免混淆。

我們上面寫的函數(shù)中有兩個變量:參數(shù)value和局部變量quotient。下面的一些圖顯示了堆棧的狀態(tài),當前可以訪問的變量位于棧頂。所有其他調(diào)用的變量飾以灰色陰影,表示它們不能被當前正在執(zhí)行的函數(shù)訪問。

假定我們以4267這個值調(diào)用遞歸函數(shù)。當函數(shù)開始執(zhí)行時,堆棧的內(nèi)容如下圖所示。

![在這里插入圖片描述](https://img-blog.csdnimg.cn/20190922114628795.png)

執(zhí)行除法運算后,堆棧的內(nèi)容如下:

![在這里插入圖片描述](https://img-blog.csdnimg.cn/2019092211463820.png)

接著,if語句判斷出quotient的值非零,所以對該函數(shù)進行遞歸調(diào)用。當這個函數(shù)第二次被調(diào)用之初,堆棧的內(nèi)容如下:

![在這里插入圖片描述](https://img-blog.csdnimg.cn/20190922114646882.png)

堆棧上創(chuàng)建了一批新的變量,隱藏了前面的那些變量,除非當前這次遞歸調(diào)用返回,否則它們是不能被訪問的。再次執(zhí)行除法運算之后堆棧的內(nèi)容如下:

![在這里插入圖片描述](https://img-blog.csdnimg.cn/2019092211465570.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0JhZEF5YXNl,size_16,color_FFFFFF,t_70)

quotient的值現(xiàn)在為42,仍然非零,所以需要繼續(xù)執(zhí)行遞歸調(diào)用,并再創(chuàng)建一批變量。在執(zhí)行完這次調(diào)用的除法運算之后,堆棧的內(nèi)容如下:

![在這里插入圖片描述](https://img-blog.csdnimg.cn/20190922114705290.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0JhZEF5YXNl,size_16,color_FFFFFF,t_70)

此時,quotient的值還是非零,仍然需要執(zhí)行遞歸調(diào)用。在執(zhí)行除法運算之后,堆棧的內(nèi)容如下:

![在這里插入圖片描述](https://img-blog.csdnimg.cn/20190922114712627.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0JhZEF5YXNl,size_16,color_FFFFFF,t_70)

不算遞歸調(diào)用語句本身,到目前為止所執(zhí)行的語句只是除法運算以及對quotient的值進行測試。由于遞歸調(diào)用使這些語句重復執(zhí)行,所以它的效果類似循環(huán):當quotient的值非零實時,把它的值作為初始值重新開始循環(huán)。但是,遞歸調(diào)用將會保存一些信息(這點與循環(huán)不同),也就是保存在堆棧中的變量值。這些信息很快就會變得非常重要。

現(xiàn)在quotient的值變成了0,遞歸函數(shù)便不再調(diào)用自身,而是開始打印輸出。然后函數(shù)返回,并開始銷毀堆棧上的變量值。

每次調(diào)用putchar得到變量value的最后一個數(shù)字,方法是對value進行模10取余運算,其結果是一個0到9之間的整數(shù)。當它與字符常量'0'相加,其結果便是對應于這個數(shù)字的ASCII字符,然后把這個字符打印出來。

![在這里插入圖片描述](https://img-blog.csdnimg.cn/20190922114740169.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0JhZEF5YXNl,size_16,color_FFFFFF,t_70)

接著函數(shù)返回,它的變量從堆棧中銷毀。接著,遞歸函數(shù)的前一次調(diào)用重新繼續(xù)執(zhí)行,它所使用的是自己的變量,它們現(xiàn)在位于堆棧的頂部。因為它的value值是42,所以調(diào)用putchar后打印出來的數(shù)字是2。

![在這里插入圖片描述](https://img-blog.csdnimg.cn/20190922114757831.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0JhZEF5YXNl,size_16,color_FFFFFF,t_70)

接著遞歸函數(shù)的這次調(diào)用也返回,它的變量也被銷毀,此時位于堆棧頂部的是遞歸函數(shù)再前一次調(diào)用的變量。遞歸調(diào)用從這個位置繼續(xù)執(zhí)行,這次打印的數(shù)字是6。在這次調(diào)用返回之前,堆棧的內(nèi)容如下:

![在這里插入圖片描述](https://img-blog.csdnimg.cn/20190922114808598.png)

現(xiàn)在我們已經(jīng)展開了整個遞歸過程,并回到該函數(shù)最初的調(diào)用。這次調(diào)用打印出的數(shù)字7,也就是它的value參數(shù)除10的余數(shù)。

![在這里插入圖片描述](https://img-blog.csdnimg.cn/20190922114821339.png)

然后,整個遞歸函數(shù)就徹底返回到一開始調(diào)用它的地點。

如果你把打印出來的字符一個接一個排在一起,出現(xiàn)在打印機或屏幕上,你將看到正確的值:4267。

遞歸與迭代

遞歸是一種強有力的技巧,但也和其他技巧一樣,它可能被誤用。這里就有一個例子,如下圖所示:

![在這里插入圖片描述](https://img-blog.csdnimg.cn/20190922114829422.png)

這個定義同時具備了我們開始討論遞歸所需要的兩個特性:存在限制條件,當符合這個條件時遞歸便不再繼續(xù):每次遞歸調(diào)用之后越來越接近這個限制條件。

用這種方式定義階乘往往會引導人們使用遞歸來實現(xiàn)階乘函數(shù),如下程序:

? ? long fun(int n)

? ? {

? ? ? if (n <= 0)

? ? ? ? return 1;

? ? ? else

? ? ? ? return n * fun(n-1);

? ? }

這個函數(shù)能夠產(chǎn)生正確的結果,但它并不是遞歸的良好用法。遞歸調(diào)用將涉及一些運行時開銷------參數(shù)必須壓倒堆棧中,為局部變量分配內(nèi)存空間(所有遞歸均是如此,并非特指這個例子),寄存器的值必須保存等。當遞歸函數(shù)的每次調(diào)用返回時,上述這些操作必須還原,恢復成原來的樣子。所以,基于這些開銷,對于這個程序而言,它并沒有簡化問題的解決方案。

下面使用循環(huán)計算相同的結果:

? ? long fun(int n)

? ? {

? ? ? int result = -1;

? ? ? while(n > 1)

? ? ? {

? ? ? ? result *= n;

? ? ? ? n -= 1;

? ? ? }

? ? ? return result;

? ? }

盡管這個使用簡單循環(huán)的程序不甚符合前面階乘的數(shù)學定義,但它卻能更為有效地計算出相同的結果。如果你仔細觀察遞歸函數(shù),你會發(fā)現(xiàn)遞歸調(diào)用是函數(shù)所執(zhí)行的最后一項任務。這個函數(shù)是尾部遞歸(tail recursion)的一個例子。由于函數(shù)在遞歸調(diào)用返回之后不再執(zhí)行任何任務,所以尾部遞歸可以很方便地轉換成一個簡單循環(huán),完成相同的任務。

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容