1. 程式人生 > >自學C之遞迴理解

自學C之遞迴理解

一、 理解概念

  C語言允許一個函式呼叫自身,這種過程被稱為遞迴(Recursion)。程式使用遞迴處理特殊的問題,如階乘、 Ackermann函式,反序等等。實際上,如果不考慮執行時記憶體的開消,任何使用賦值語句、if-else和while結構的函 數都可以用遞迴方式重寫。

  “遞迴”可以跟據字面去理解,“遞”就是一級一級遞進,“遞”可以想象成通過一段臺階走下地下室,“歸”則是歸來 的意思,就像通個了“遞”這些階級到了地下室之後,又延著所走的臺階返回原點。

你還可以將“遞迴”想象成“我肚裡面有一個我”,如果我肚裡有一個我,那麼肚裡的我肚裡還有一個我,這樣一路我 下去,我我我我,將會無窮無盡,因而遞迴需要一個終止的條件,就假設世界上只能同時存在5個我,這樣,“我肚裡面 有我”就一共只有五個肚,五個我,最後第五個我的肚裡不能再有我了。

  “我肚裡有我”,這就相當函式裡面有本函式,函式處理事務,就相當於我吃飯, 當我吃飯時,我裡面的我也要吃 飯,但所有的我入口都是最外面的一張嘴,於是,我吃飯,飯到了肚子裡被肚子裡的我吃了我下去的飯,肚子裡的我的 肚子裡的我再把他的飯吃了,這樣的情況,只有把最裡面的我的肚子餵飽了,外面的我才可以吃到飯。程式的函式也是 一樣,它必須要等裡面的函式處理完了問題,外面的函式才可以著手處理問題。就好比去地下室拿東西,你必須走完所 有階梯才到達地下室,然後才可以返回。

  總之,我們記著,只要是遞迴,就必須要等裡面的我(函式)完成了任務,才輪到外面一級的我(函式)做任務。

二、 遞迴的基本原理

   1. 每一級的函式呼叫都有自已的變數。

   2. 每一次函式呼叫都會有一次返回。

   3. 遞迴函式中,位於遞迴呼叫前的語句和各級被調函式具有相同的執行順序。

   4. 遞迴函式中,位於遞迴呼叫後的語句和各級被調函式的順序相反。

   5.雖然每一級都有自己的變數,但是函式程式碼並不會複製。函式程式碼是一系列的計算機指令,而函式呼叫就是從頭執行這個指令集的一條命令。

   6.遞迴函式中必須包含可以終止遞迴的語句。

三、原理驗證

#include <stdio.h>
void up_and_down(int);
int
main(void) { up_and_down(1); return 0; } void up_and_down(int n) { printf("Level %d: n location %p\n", n, &n); if(n < 3) up_and_down(n+1); printf("Level %d: n location %p\n", n, &n); }

編譯後執行的結果:


Level 1: n location 0x7fffffffe30c 從level1、level2、level3中可以看出n有三個不同的地址,即是說經過三次遞迴呼叫,各有不同的n變數。

Level 2: n location 0x7fffffffe2ec 下面的3級level,可以看出3次遞迴呼叫返回了三次,驗證了每次呼叫都有返回。

Level 3: n location 0x7fffffffe2cc 第一和第二個printf用來驗證遞迴呼叫前的函式執行順序,驗證了它和遞迴呼叫有相同的執行順序。

Level 3: n location 0x7fffffffe2cc 第三和第四個printf則說明了遞迴後的函式是以反序方式來執行的

Level 2: n location 0x7fffffffe2ec

Level 1: n location 0x7fffffffe30c


我們再用GDB跟蹤程式:


先用gdb 載入程式,鍵入start執行後停在main入口處

Temporary breakpoint 2, main () at recur.c:6
6 up_and_down(1);

stepi跟蹤程式進入呼叫函式

(gdb) stepi
0x0000000000400589 6 up_and_down(1);

用backtrace檢視記憶體執行棧,看出函式已壓入棧中

(gdb) bt
#0 up_and_down (n=0) at recur.c:11
#1 0x000000000040058e in main () at recur.c:6

往下執行到n=3時,棧中已壓入三個函式在main函式之上,從中可以看出,每一次函式呼叫,就需要向棧中壓入資料,因為每次呼叫都要壓棧,所以每次呼叫的函式的變數都是獨立的,又所以每一次呼叫都會有返回。

(gdb) bt
#0 up_and_down (n=3) at recur.c:13
#1 0x00000000004005d7 in up_and_down (n=2) at recur.c:15
#2 0x00000000004005d7 in up_and_down (n=1) at recur.c:15
#3 0x000000000040058e in main () at recur.c:6

因為棧的特性,後進入棧的函式獲得先返回的機會,所以位於遞迴呼叫後的語句和各級被調函式的順序相
反,當n>時函式開始返回,首先返回的是n=3時的函式。
跟上面比較, up_and_down(n=3)的函式已經返回

(gdb) bt
#0 up_and_down (n=2) at recur.c:17
#1 0x00000000004005d7 in up_and_down (n=1) at recur.c:15
#2 0x000000000040058e in main () at recur.c:6

往下執行直到main返回,程式結束。

(gdb) c
Continuing.
Level 2: n location 0x7fffffffe2ec
Level 1: n location 0x7fffffffe30c
[Inferior 1 (process 6893) exited normally]

用GDB除錯可以看出,遞迴如果層數太多,超出了棧的容量就會造成程式宕機,這就是遞迴的缺點;同時,函式呼叫每次都要壓棧處理,遞迴層數過多就會影響程式的執行速度。

四、遞迴處理反序

返回值需要反序排列時,遞迴能有效地精簡程式碼

例如:十進位制轉換成二進位制,用除二取餘,倒序排列
意思是:將一個十進位制數除以二,得到的商再除以二,依此類推直到商等於一或零時為止,倒取將除得的餘數,即換算為二進位制數的結果。 例如把52換算成二進位制數,計算結果如圖:
除二取餘
52除以2得到的餘數依次為:0、0、1、0、1、1,倒序排列,所以52對應的二進位制數就是110100。

用C語言可以這樣編碼:

#include <stdio.h>

void to_binary(int);

int main(void)
{
    int number;
    printf("Enter a number or 'q' to exit:\n");
    while (scanf("%d", &number) == 1)
    {
        printf("Binary equivalent:");
        to_binary(number);
        putchar('\n');
        printf("Enter a number or 'q' to exit:\n");
    }
    printf("Done!\n");

    return 0;

}

void to_binary(int n) {
    int r;
    r = n%2;
    if (n >= 2)
        to_binary(n/2);
    putchar('0' + r);
    return;
}

上面用遞迴的方法比用迴圈的方法要簡單很多,你可以用迴圈的方法重編上面的函式去對比。