做者:archimedeshtml
在計算機科學領域中,遞歸式經過遞歸函數來實現的。程序調用自身的編程技巧稱爲遞歸( recursion)。編程
一個過程或函數在其定義或說明中有直接或間接調用自身的一種方法,它一般把一個大型複雜的問題層層轉化爲一個與原問題類似的規模較小的問題來求解,遞歸策略只需少許的程序就可描述出解題過程所須要的屢次重複計算,大大地減小了程序的代碼量。遞歸的能力在於用有限的語句來定義對象的無限集合。架構
通常來講,遞歸須要有:邊界條件、遞歸前進段和遞歸返回段。併發
當邊界條件不知足時,遞歸前進;當邊界條件知足時,遞歸返回。編程語言
注意:函數
(1) 遞歸就是在過程或函數裏調用自身;高併發
(2) 在使用遞歸策略時,必須有一個明確的遞歸結束條件,稱爲遞歸出口。post
本文地址:http://www.cnblogs.com/archimedes/p/rescuvie-tailrescuvie.html,轉載請註明源地址。優化
c程序在虛擬內存中的地址從低地址到高地址的順序依次是.text段(代碼區)、.rodata段(常量區)、.data段(已初始化的全局變量區)、.bss段(未初始化的全局變量區)、堆、動態庫映射區、棧、內核區(用戶態代碼不可訪問)url
問題:計算n!
數學上的計算公式爲:n!=n×(n-1)×(n-2)……2×1
使用遞歸的方式,能夠定義爲:
以遞歸的方式計算4!
F(4)=4×F(3) 遞歸階段
F(3)=3×F(2)
F(2)=2×F(1)
F(1)=1 終止條件
F(2)=(2)×(1) 迴歸階段
F(3)=(3)×(2)
F(4)=(4)×(6)
24 遞歸完成
以遞歸方式實現階乘函數的實現:
int fact(int n) { if(n < 0) return 0; else if (n == 0 || n == 1) return 1; else return n * fact(n - 1); }
下面來詳細分析遞歸的工做原理
先看看C語言中函數的執行方式,須要瞭解一些關於C程序在內存中的組織方式:
BSS段:(bss segment)一般是指用來存放程序中未初始化的全局變量的一塊內存區域。BSS是英文Block Started by Symbol的簡稱。BSS段屬於靜態內存分配。
數據段 :數據段(data segment)一般是指用來存放程序中已初始化的全局變量的一塊內存區域。數據段屬於靜態內存分配。
代碼段: 代碼段(code segment/text segment)一般是指用來存放 程序執行代碼的一塊內存區域。這部分區域的大小在程序運行前就已經肯定,而且內存區域一般屬於只讀 , 某些架構也容許代碼段爲可寫,即容許修改程序。在代碼段中,也有可能包含一些只讀的常數變量 ,例如字符串常量等。程序段爲程序代碼在內存中的映射.一個程序能夠在內存中多有個副本.
堆(heap) :堆是用於存放進程運行中被動態分配的內存段,它的大小並不固定,可動態擴張或縮減。當進程調用malloc/free等函數分配內存時,新分配的內存就被動態添加到堆上(堆被擴張)/釋放的內存從堆中被剔除(堆被縮減)
棧(stack) :棧又稱堆棧, 存放程序的局部變量(但不包括static聲明的變量, static 意味着 在數據段中存放變量)。除此之外,在函數被調用時,棧用來傳遞參數和返回值。因爲棧的後進先出特色,因此棧特別方便用來保存/恢復調用現場。從這個意義上講,咱們能夠把堆棧當作一個寄存、交換臨時數據的內存區。
堆的增加方向爲從低地址到高地址向上增加,而棧的增加方向恰好相反(實際狀況與CPU的體系結構有關)
當C程序中調用了一個函數時,棧中會分配一塊空間來保存與這個調用相關的信息,每個調用都被看成是活躍的。棧上的那塊存儲空間稱爲活躍記錄或者棧幀
棧幀由5個區域組成:輸入參數、返回值空間、計算表達式時用到的臨時存儲空間、函數調用時保存的狀態信息以及輸出參數,參見下圖:
可使用下面的程序來檢驗:
#include <stdio.h> int g1=0, g2=0, g3=0; int max(int i) { int m1 = 0, m2, m3 = 0, *p_max; static n1_max = 0, n2_max, n3_max = 0; p_max = (int*)malloc(10); printf("打印max程序地址\n"); printf("in max: 0x%08x\n\n",max); printf("打印max傳入參數地址\n"); printf("in max: 0x%08x\n\n",&i); printf("打印max函數中靜態變量地址\n"); printf("0x%08x\n",&n1_max); //打印各本地變量的內存地址 printf("0x%08x\n",&n2_max); printf("0x%08x\n\n",&n3_max); printf("打印max函數中局部變量地址\n"); printf("0x%08x\n",&m1); //打印各本地變量的內存地址 printf("0x%08x\n",&m2); printf("0x%08x\n\n",&m3); printf("打印max函數中malloc分配地址\n"); printf("0x%08x\n\n",p_max); //打印各本地變量的內存地址 if(i) return 1; else return 0; } int main(int argc, char **argv) { static int s1=0, s2, s3=0; int v1=0, v2, v3=0; int *p; p = (int*)malloc(10); printf("打印各全局變量(已初始化)的內存地址\n"); printf("0x%08x\n",&g1); //打印各全局變量的內存地址 printf("0x%08x\n",&g2); printf("0x%08x\n\n",&g3); printf("======================\n"); printf("打印程序初始程序main地址\n"); printf("main: 0x%08x\n\n", main); printf("打印主參地址\n"); printf("argv: 0x%08x\n\n",argv); printf("打印各靜態變量的內存地址\n"); printf("0x%08x\n",&s1); //打印各靜態變量的內存地址 printf("0x%08x\n",&s2); printf("0x%08x\n\n",&s3); printf("打印各局部變量的內存地址\n"); printf("0x%08x\n",&v1); //打印各本地變量的內存地址 printf("0x%08x\n",&v2); printf("0x%08x\n\n",&v3); printf("打印malloc分配的堆地址\n"); printf("malloc: 0x%08x\n\n",p); printf("======================\n"); max(v1); printf("======================\n"); printf("打印子函數起始地址\n"); printf("max: 0x%08x\n\n",max); return 0; }
棧是用來存儲函數調用信息的絕好方案,然而棧也有一些缺點:
棧維護了每一個函數調用的信息直到函數返回後才釋放,這須要佔用至關大的空間,尤爲是在程序中使用了許多的遞歸調用的狀況下。除此以外,由於有大量的信息須要保存和恢復,所以生成和銷燬活躍記錄須要消耗必定的時間。咱們須要考慮採用迭代的方案。幸運的是咱們能夠採用一種稱爲尾遞歸的特殊遞歸方式來避免前面提到的這些缺點。
若是一個函數中全部遞歸形式的調用都出如今函數的末尾,咱們稱這個遞歸函數是尾遞歸的。當遞歸調用是整個函數體中最後執行的語句且它的返回值不屬於表達式的一部分時,這個遞歸調用就是尾遞歸。尾遞歸函數的特色是在迴歸過程當中不用作任何操做,這個特性很重要,由於大多數現代的編譯器會利用這種特色自動生成優化的代碼。
當編譯器檢測到一個函數調用是尾遞歸的時候,它就覆蓋當前的活動記錄而不是在棧中去建立一個新的。編譯器能夠作到這點,由於遞歸調用是當前活躍期內最後一條待執行的語句,因而當這個調用返回時棧幀中並無其餘事情可作,所以也就沒有保存棧幀的必要了。經過覆蓋當前的棧幀而不是在其之上從新添加一個,這樣所使用的棧空間就大大縮減了,這使得實際的運行效率會變得更高。雖然編譯器可以優化尾遞歸形成的棧溢出問題,可是在編程中,咱們仍是應該儘可能避免尾遞歸的出現,由於全部的尾遞歸都是能夠用簡單的goto循環替代的。
爲了理解尾遞歸是如何工做的,讓咱們再次以遞歸的形式計算階乘。首先,這能夠很容易讓咱們理解爲何以前所定義的遞歸不是尾遞歸。回憶以前對計算n!的定義:在每一個活躍期計算n倍的(n-1)!的值,讓n=n-1並持續這個過程直到n=1爲止。這種定義不是尾遞歸的,由於每一個活躍期的返回值都依賴於用n乘如下一個活躍期的返回值,所以每次調用產生的棧幀將不得不保存在棧上直到下一個子調用的返回值肯定。如今讓咱們考慮以尾遞歸的形式來定義計算n!的過程。
這種定義還須要接受第二個參數a,除此以外並無太大區別。a(初始化爲1)維護遞歸層次的深度。這就讓咱們避免了每次還須要將返回值再乘以n。然而,在每次遞歸調用中,令a=na而且n=n-1。繼續遞歸調用,直到n=1,這知足結束條件,此時直接返回a便可。
代碼實例給出了一個C函數facttail,它接受一個整數n並以尾遞歸的形式計算n!。這個函數還接受一個參數a,a的初始值爲1。facttail使用a來維護遞歸層次的深度,除此以外它和fact很類似。讀者能夠注意一下函數的具體實現和尾遞歸定義的類似之處。
int facttail(int n, int a) { if (n < 0) return 0; else if (n == 0) return 1; else if (n == 1) return a; else return facttail(n - 1, n * a); }
示例中的函數是尾遞歸的,由於對facttail的單次遞歸調用是函數返回前最後執行的一條語句。在facttail中碰巧最後一條語句也是對facttail的調用,但這並非必需的。換句話說,在遞歸調用以後還能夠有其餘的語句執行,只是它們只能在遞歸調用沒有執行時才能夠執行。
尾遞歸是極其重要的,不用尾遞歸,函數的堆棧耗用難以估量,須要保存不少中間函數的堆棧。好比f(n, sum) = f(n-1) + value(n) + sum; 會保存n個函數調用堆棧,而使用尾遞歸f(n, sum) = f(n-1, sum+value(n)); 這樣則只保留後一個函數堆棧便可,以前的可優化刪去。
也許在C語言中有不少的特例,但編程語言不僅有C語言,在函數式語言Erlang中(亦是棧語言),若是想要保持語言的高併發特性,就必須用尾遞歸來替代傳統的遞歸。