對C語言中遞歸算法的深入解析
許多教科書都把計算機(jī)階乘和菲波那契數(shù)列用來說明遞歸,非常不幸我們可愛的著名的老潭老師的《C語言程序設(shè)計》一書中就是從階乘的計算開始的函數(shù)遞歸。導(dǎo)致讀過這本經(jīng)書的同學(xué)們,看到階乘計算第一個想法就是遞歸。但是在階乘的計算里,遞歸并沒有提供任何優(yōu)越之處。在菲波那契數(shù)列中,它的效率更是低的非??植?。
這里有一個簡單的程序,可用于說明遞歸。程序的目的是把一個整數(shù)從二進(jìn)制形式轉(zhuǎn)換為可打印的字符形式。例如:給出一個值4267,我們需要依次產(chǎn)生字符‘4',‘2',‘6',和‘7'。就如在printf函數(shù)中使用了%d格式碼,它就會執(zhí)行類似處理。
我們采用的策略是把這個值反復(fù)除以10,并打印各個余數(shù)。例如,4267除10的余數(shù)是7,但是我們不能直接打印這個余數(shù)。我們需要打印的是機(jī)器字符集中表示數(shù)字‘7'的值。在ASCII碼中,字符‘7'的值是55,所以我們需要在余數(shù)上加上48來獲得正確的字符,但是,使用字符常量而不是整型常量可以提高程序的可移植性。‘0'的ASCII碼是48,所以我們用余數(shù)加上‘0',所以有下面的關(guān)系:
‘0'+ 0 =‘0'
‘0'+ 1 =‘1'
‘0'+ 2 =‘2'
...
從這些關(guān)系中,我們很容易看出在余數(shù)上加上‘0'就可以產(chǎn)生對應(yīng)字符的代碼。接著就打印出余數(shù)。下一步再取商的值,4267/10等于426。然后用這個值重復(fù)上述步驟。
這種處理方法存在的唯一問題是它產(chǎn)生的數(shù)字次序正好相反,它們是逆向打印的。所以在我們的程序中使用遞歸來修正這個問題。
我們這個程序中的函數(shù)是遞歸性質(zhì)的,因為它包含了一個對自身的調(diào)用。乍一看,函數(shù)似乎永遠(yuǎn)不會終止。當(dāng)函數(shù)調(diào)用時,它將調(diào)用自身,第2次調(diào)用還將調(diào)用自身,以此類推,似乎永遠(yuǎn)調(diào)用下去。這也是我們在剛接觸遞歸時最想不明白的事情。但是,事實上并不會出現(xiàn)這種情況。
這個程序的遞歸實現(xiàn)了某種類型的螺旋狀while循環(huán)。while循環(huán)在循環(huán)體每次執(zhí)行時必須取得某種進(jìn)展,逐步迫近循環(huán)終止條件。遞歸函數(shù)也是如此,它在每次遞歸調(diào)用后必須越來越接近某種限制條件。當(dāng)遞歸函數(shù)符合這個限制條件時,它便不在調(diào)用自身。
在程序中,遞歸函數(shù)的限制條件就是變量quotient為零。在每次遞歸調(diào)用之前,我們都把quotient除以10,所以每遞歸調(diào)用一次,它的值就越來越接近零。當(dāng)它最終變成零時,遞歸便告終止。
/*接受一個整型值(無符號0,把它轉(zhuǎn)換為字符并打印它,前導(dǎo)零被刪除*/
#include <stdio.h>
int binary_to_ascii( unsigned int value)
{
unsigned int quotient;
quotient = value / 10;
if( quotient != 0)
binary_to_ascii( quotient);
putchar ( value % 10 + '0' );
}
遞歸是如何幫助我們以正確的順序打印這些字符呢?下面是這個函數(shù)的工作流程。
1. 將參數(shù)值除以10
2. 如果quotient的值為非零,調(diào)用binary-to-ascii打印quotient當(dāng)前值的各位數(shù)字
3. 接著,打印步驟1中除法運(yùn)算的余數(shù)
注意在第2個步驟中,我們需要打印的是quotient當(dāng)前值的各位數(shù)字。我們所面臨的問題和最初的問題完全相同,只是變量quotient的值變小了。我們用剛剛編寫的函數(shù)(把整數(shù)轉(zhuǎn)換為各個數(shù)字字符并打印出來)來解決這個問題。由于quotient的值越來越小,所以遞歸最終會終止。
一旦你理解了遞歸,閱讀遞歸函數(shù)最容易的方法不是糾纏于它的執(zhí)行過程,而是相信遞歸函數(shù)會順利完成它的任務(wù)。如果你的每個步驟正確無誤,你的限制條件設(shè)置正確,并且每次調(diào)用之后更接近限制條件,遞歸函數(shù)總是能正確的完成任務(wù)。
但是,為了理解遞歸的工作原理,你需要追蹤遞歸調(diào)用的執(zhí)行過程,所以讓我們來進(jìn)行這項工作。追蹤一個遞歸函數(shù)的執(zhí)行過程的關(guān)鍵是理解函數(shù)中所聲明的變量是如何存儲的。當(dāng)函數(shù)被調(diào)用時,它的變量的空間是創(chuàng)建于運(yùn)行時堆棧上的。以前調(diào)用的函數(shù)的變量扔保留在堆棧上,但他們被新函數(shù)的變量所掩蓋,因此是不能被訪問的。
當(dāng)遞歸函數(shù)調(diào)用自身時,情況于是如此。每進(jìn)行一次新的調(diào)用,都將創(chuàng)建一批變量,他們將掩蓋遞歸函數(shù)前一次調(diào)用所創(chuàng)建的變量。當(dāng)我追蹤一個遞歸函數(shù)的執(zhí)行過程時,必須把分?jǐn)?shù)不同次調(diào)用的變量區(qū)分開來,以避免混淆。
程序中的函數(shù)有兩個變量:參數(shù)value和局部變量quotient。下面的一些圖顯示了堆棧的狀態(tài),當(dāng)前可以訪問的變量位于棧頂。所有其他調(diào)用的變量飾以灰色的陰影,表示他們不能被當(dāng)前正在執(zhí)行的函數(shù)訪問。
假定我們以4267這個值調(diào)用遞歸函數(shù)。當(dāng)函數(shù)剛開始執(zhí)行時,堆棧的內(nèi)容如下圖所示:






不算遞歸調(diào)用語句本身,到目前為止所執(zhí)行的語句只是除法運(yùn)算以及對quotient的值進(jìn)行測試。由于遞歸調(diào)用這些語句重復(fù)執(zhí)行,所以它的效果類似循環(huán):當(dāng)quotient的值非零時,把它的值作為初始值重新開始循環(huán)。但是,遞歸調(diào)用將會保存一些信息(這點(diǎn)與循環(huán)不同),也就好是保存在堆棧中的變量值。這些信息很快就會變得非常重要。
現(xiàn)在quotient的值變成了零,遞歸函數(shù)便不再調(diào)用自身,而是開始打印輸出。然后函數(shù)返回,并開始銷毀堆棧上的變量值。
每次調(diào)用putchar得到變量value的最后一個數(shù)字,方法是對value進(jìn)行模10取余運(yùn)算,其結(jié)果是一個0到9之間的整數(shù)。把它與字符常量‘0'相加,其結(jié)果便是對應(yīng)于這個數(shù)字的ASCII字符,然后把這個字符打印出來。
輸出4:
接著函數(shù)返回,它的變量從堆棧中銷毀。接著,遞歸函數(shù)的前一次調(diào)用重新繼續(xù)執(zhí)行,她所使用的是自己的變量,他們現(xiàn)在位于堆棧的頂部。因為它的value值是42,所以調(diào)用putchar后打印出來的數(shù)字是2。

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

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

如果你把打印出來的字符一個接一個排在一起,出現(xiàn)在打印機(jī)或屏幕上,你將看到正確的值:4267
使用遞歸一定要有跳出的條件:
這是一個最簡單的遞歸, 不過它會一直執(zhí)行, 可用 Ctrl+C 終止.
#include <stdio.h>
void prn(int num) {
printf("%d/n", num);
if (num > 0) prn(--num);
}
int main(void)
{
prn(9);
getchar();
return 0;
}
實例: 翻轉(zhuǎn)字符串
#include <stdio.h>
void revers(char *cs);
int main(void)
{
revers("123456789");
getchar();
return 0;
}
void revers(char *cs)
{
if (*cs)
{
revers(cs + 1);
putchar(*cs);
}
}
實例: 階乘
#include <stdio.h>
int factorial(int num);
int main(void)
{
int i;
for (i = 1; i <= 9; i++)
printf("%d: %d/n", i, factorial(i));
getchar();
return 0;
}
int factorial(int num)
{
if (num == 1)
return(1);
else
return(num * factorial(num-1));
}
實例: 整數(shù)到二進(jìn)制
#include <stdio.h>
void IntToBinary(unsigned num);
int main(void)
{
IntToBinary(255); /* 11111111 */
getchar();
return 0;
}
void IntToBinary(unsigned num) {
int i = num % 2;
if (num > 1) IntToBinary(num / 2);
putchar(i ? '1' : '0');
// putchar('0' + i); /* 可代替上面一句 */
}
剖析遞歸:
#include <stdio.h>
void prn(unsigned n);
int main(void)
{
prn(1);
getchar();
return 0;
}
void prn(unsigned n) {
printf("%d: %p/n", n, &n); /* A */
if (n < 4)
prn(n+1); /* B */
printf("%d: %p/n", n, &n); /* C */
}
例輸出效果圖:

分析:
程序運(yùn)行到 A, 輸出了第一行.
此時 n=1, 滿足 < 4 的條件, 繼續(xù)執(zhí)行 B 開始了自調(diào)用(接著會輸出第二行); 注意 n=1 時語句 C 還有待執(zhí)行.
...如此循環(huán), 一直到 n=4, A 可以執(zhí)行, 但因不滿足條件 B 執(zhí)行不了了; 終于在 n=4 時得以執(zhí)行 C.
但此時內(nèi)存中有四個函數(shù)都等待返回(分別是 n=1、2、3、4 時), 咱們分別叫它 f1、f2、f3、f4.
f4 執(zhí)行 C 輸出了第五行, 函數(shù)返回, 返回給 f3(此時 n=3), f3 得以繼續(xù)執(zhí)行 C, 輸出了第六行.
f3 -> f2 -> 繼續(xù) C, 輸出了第七行.
f2 -> f1 -> 繼續(xù) C, 輸出了第八行, 執(zhí)行完畢!
如此看來, 遞歸函數(shù)還是很費(fèi)內(nèi)存的(有時不如直接使用循環(huán)), 但的確很巧妙.
相關(guān)文章
C語言實現(xiàn)模擬USB對8bit數(shù)據(jù)的NRZI編碼輸出
今天小編就為大家分享一篇關(guān)于C語言實現(xiàn)模擬USB對8bit數(shù)據(jù)的NRZI編碼輸出,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2018-12-12C語言實現(xiàn)時區(qū)轉(zhuǎn)換函數(shù)的實例
這篇文章主要介紹了C語言實現(xiàn)時區(qū)轉(zhuǎn)換函數(shù)的實例的相關(guān)資料,這里分析需求并提供實現(xiàn)代碼,需要的朋友可以參考下2017-08-08C語言實現(xiàn)校運(yùn)動會項目管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了C語言實現(xiàn)校運(yùn)動會項目管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-02-02C++中volatile關(guān)鍵字的使用詳解以及常見的誤解
volatile 關(guān)鍵字是一種類型修飾符,用它聲明的類型變量表示可以被某些編譯器未知的因素更改,比如:操作系統(tǒng),硬件或者其他線程等2020-01-01UE4 Unlua 調(diào)用異步藍(lán)圖節(jié)點(diǎn)AIMoveTo函數(shù)示例詳解
這篇文章主要為大家介紹了UE4 Unlua 調(diào)用AIMoveTo函數(shù)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09C++實現(xiàn)猜數(shù)小游戲的實現(xiàn)
這篇文章主要介紹了C++實現(xiàn)猜數(shù)小游戲的實現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-02-02