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)

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

遞歸函數就是函數直接或者間接調用自身的函數问潭。許多教科書把階乘運算用來說明遞歸猿诸,這是非常不幸的。因為這個時候使用狡忙,它的效率非常之低梳虽。

這里有一個簡單的程序,可用于說明遞歸灾茁。程序的目的是把一個整數轉換為可打印的字符形式窜觉。例如,給出一個值4267北专,我們需要依次產生字符'4'禀挫、'2'、'6'拓颓、'7'语婴。

我們采用的策略是把這個值反復對10取余得到余數處理打印后,并除以10驶睦。例如砰左,4267除10的余數是7,但是我們不能直接打印這個余數场航。我們需要打印的是機器字符集中表示數字'7'的值缠导。在ASCII碼中,字符'7'的值是55溉痢,所以我們需要在余數上加上48來獲得正確的字符僻造。但是憋他,使用字符常量而不是整型常量可以提高程序的可移植性〉找猓考慮下面的關系:

'0' + 0 = '0'

'0' + 1 = '1'

'0' + 2 = '2'

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

這種處理方法存在的問題是它產生的數字次序正好相反,它們是逆向打印的忍些。在下面的程序中鲁猩,我們就用遞歸來修正這個問題。

void binary_to_ascii(unsigned int value)

{

? unsigned int quotient = value / 10;

? if (quotient !=0)

? ? binary_to_ascii(quotient);

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

}

上面程序中的函數是遞歸性質的罢坝,因為它包含了一個對自身的調用廓握。乍一看,函數似乎永遠也不會終止嘁酿,當函數調用時隙券,它將調用自身,第二次調用還是調用自身闹司,以此類推娱仔,似乎永遠調用下去。但是游桩,事實上并不會出現這種情況牲迫。

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

該程序中滨达,遞歸函數的限制條件就是變量quotient為0奶稠。在每次遞歸調用之前,我們都把quotient除以10捡遍,所以每遞歸一次锌订,它的值就更接近0。當它最終變成0時画株,遞歸便告終了辆飘。

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

1.將參數值除以10

2.如果quotient的值為非零芹关,調用binary_to_ascii打印quotient當前值的各位數字

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

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

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

追蹤遞歸函數

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

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

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

假定我們以4267這個值調用遞歸函數。當函數開始執(zhí)行時戈擒,堆棧的內容如下圖所示眶明。

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

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

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

接著筐高,if語句判斷出quotient的值非零搜囱,所以對該函數進行遞歸調用。當這個函數第二次被調用之初凯傲,堆棧的內容如下:

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

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

![在這里插入圖片描述](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的值現在為42灸促,仍然非零诫欠,所以需要繼續(xù)執(zhí)行遞歸調用,并再創(chuàng)建一批變量浴栽。在執(zhí)行完這次調用的除法運算之后荒叼,堆棧的內容如下:

![在這里插入圖片描述](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í)行遞歸調用被廓。在執(zhí)行除法運算之后,堆棧的內容如下:

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

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

現在quotient的值變成了0豆拨,遞歸函數便不再調用自身直奋,而是開始打印輸出。然后函數返回施禾,并開始銷毀堆棧上的變量值脚线。

每次調用putchar得到變量value的最后一個數字,方法是對value進行模10取余運算拾积,其結果是一個0到9之間的整數殉挽。當它與字符常量'0'相加丰涉,其結果便是對應于這個數字的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)

接著函數返回一死,它的變量從堆棧中銷毀。接著傻唾,遞歸函數的前一次調用重新繼續(xù)執(zhí)行投慈,它所使用的是自己的變量,它們現在位于堆棧的頂部冠骄。因為它的value值是42伪煤,所以調用putchar后打印出來的數字是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)

接著遞歸函數的這次調用也返回凛辣,它的變量也被銷毀抱既,此時位于堆棧頂部的是遞歸函數再前一次調用的變量。遞歸調用從這個位置繼續(xù)執(zhí)行扁誓,這次打印的數字是6防泵。在這次調用返回之前,堆棧的內容如下:

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

現在我們已經展開了整個遞歸過程蝗敢,并回到該函數最初的調用捷泞。這次調用打印出的數字7,也就是它的value參數除10的余數寿谴。

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

然后锁右,整個遞歸函數就徹底返回到一開始調用它的地點。

如果你把打印出來的字符一個接一個排在一起讶泰,出現在打印機或屏幕上咏瑟,你將看到正確的值:4267。

遞歸與迭代

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

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

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

用這種方式定義階乘往往會引導人們使用遞歸來實現階乘函數辜王,如下程序:

? ? long fun(int n)

? ? {

? ? ? if (n <= 0)

? ? ? ? return 1;

? ? ? else

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

? ? }

這個函數能夠產生正確的結果劈狐,但它并不是遞歸的良好用法。遞歸調用將涉及一些運行時開銷------參數必須壓倒堆棧中呐馆,為局部變量分配內存空間(所有遞歸均是如此肥缔,并非特指這個例子),寄存器的值必須保存等汹来。當遞歸函數的每次調用返回時续膳,上述這些操作必須還原改艇,恢復成原來的樣子。所以坟岔,基于這些開銷谒兄,對于這個程序而言,它并沒有簡化問題的解決方案社付。

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

? ? long fun(int n)

? ? {

? ? ? int result = -1;

? ? ? while(n > 1)

? ? ? {

? ? ? ? result *= n;

? ? ? ? n -= 1;

? ? ? }

? ? ? return result;

? ? }

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

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末鸥拧,一起剝皮案震驚了整個濱河市悲伶,隨后出現的幾起案子,更是在濱河造成了極大的恐慌住涉,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,651評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件钠绍,死亡現場離奇詭異舆声,居然都是意外死亡,警方通過查閱死者的電腦和手機柳爽,發(fā)現死者居然都...
    沈念sama閱讀 92,468評論 3 392
  • 文/潘曉璐 我一進店門媳握,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人磷脯,你說我怎么就攤上這事蛾找。” “怎么了赵誓?”我有些...
    開封第一講書人閱讀 162,931評論 0 353
  • 文/不壞的土叔 我叫張陵打毛,是天一觀的道長。 經常有香客問我俩功,道長幻枉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,218評論 1 292
  • 正文 為了忘掉前任诡蜓,我火速辦了婚禮熬甫,結果婚禮上,老公的妹妹穿的比我還像新娘蔓罚。我一直安慰自己椿肩,他們只是感情好瞻颂,可當我...
    茶點故事閱讀 67,234評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著郑象,像睡著了一般贡这。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上扣唱,一...
    開封第一講書人閱讀 51,198評論 1 299
  • 那天藕坯,我揣著相機與錄音,去河邊找鬼噪沙。 笑死炼彪,一個胖子當著我的面吹牛,可吹牛的內容都是我干的正歼。 我是一名探鬼主播辐马,決...
    沈念sama閱讀 40,084評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼局义!你這毒婦竟也來了喜爷?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,926評論 0 274
  • 序言:老撾萬榮一對情侶失蹤萄唇,失蹤者是張志新(化名)和其女友劉穎檩帐,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體另萤,經...
    沈念sama閱讀 45,341評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡湃密,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,563評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了四敞。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片泛源。...
    茶點故事閱讀 39,731評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖忿危,靈堂內的尸體忽然破棺而出达箍,到底是詐尸還是另有隱情,我是刑警寧澤铺厨,帶...
    沈念sama閱讀 35,430評論 5 343
  • 正文 年R本政府宣布缎玫,位于F島的核電站,受9級特大地震影響努释,放射性物質發(fā)生泄漏碘梢。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,036評論 3 326
  • 文/蒙蒙 一伐蒂、第九天 我趴在偏房一處隱蔽的房頂上張望煞躬。 院中可真熱鬧,春花似錦、人聲如沸恩沛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽雷客。三九已至芒珠,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間搅裙,已是汗流浹背皱卓。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留部逮,地道東北人娜汁。 一個月前我還...
    沈念sama閱讀 47,743評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像兄朋,于是被迫代替她去往敵國和親掐禁。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,629評論 2 354

推薦閱讀更多精彩內容