遞歸 與 尾遞歸 詳解

  前言:今天上網看帖子的時候,看到關於尾遞歸的應用(http://bbs.csdn.net/topics/390215312),大腦中感覺這個詞好像在哪裏見過,但是又想不起來具體是怎麼回事。如是乎,在網上搜了一下,頓時豁然開朗,知道尾遞歸是怎麼回事了。下面就遞歸與尾遞歸進行總結,以方便日後在工作中使用。


1、遞歸

  關於遞歸的概念,我們都不陌生。簡單的來說遞歸就是一個函數直接或間接地調用自身,是爲直接或間接遞歸。一般來說,遞歸需要有邊界條件、遞歸前進段和遞歸返回段。當邊界條件不滿足時,遞歸前進;當邊界條件滿足時,遞歸返回。用遞歸需要注意以下兩點:(1) 遞歸就是在過程或函數裏調用自身。(2) 在使用遞歸策略時,必須有一個明確的遞歸結束條件,稱爲遞歸出口。

遞歸一般用於解決三類問題:
   (1)數據的定義是按遞歸定義的。(Fibonacci函數,n的階乘)
   (2)問題解法按遞歸實現。(回溯)
   (3)數據的結構形式是按遞歸定義的。(二叉樹的遍歷,圖的搜索)
遞歸的缺點:
  遞歸解題相對常用的算法如普通循環等,運行效率較低。因此,應該儘量避免使用遞歸,除非沒有更好的算法或者某種特定情況,遞歸更爲適合的時候。在遞歸調用的過程當中系統爲每一層的返回點、局部量等開闢了棧來存儲,因此遞歸次數過多容易造成棧溢出
  用線性遞歸實現Fibonacci函數,程序如下所示:
1 int FibonacciRecursive(int n)
2 {
3     if( n < 2)
4         return n;
5     return (FibonacciRecursive(n-1)+FibonacciRecursive(n-2));
6 }

遞歸寫的代碼非常容易懂,完全是根據函數的條件進行選擇計算機步驟。例如現在要計算n=5時的值,遞歸調用過程如下圖所示:


2、尾遞歸

  顧名思義,尾遞歸就是從最後開始計算, 每遞歸一次就算出相應的結果, 也就是說, 函數調用出現在調用者函數的尾部, 因爲是尾部, 所以根本沒有必要去保存任何局部變量. 直接讓被調用的函數返回時越過調用者, 返回到調用者的調用者去。尾遞歸就是把當前的運算結果(或路徑)放在參數裏傳給下層函數,深層函數所面對的不是越來越簡單的問題,而是越來越複雜的問題,因爲參數裏帶有前面若干步的運算路徑。

  尾遞歸是極其重要的,不用尾遞歸,函數的堆棧耗用難以估量,需要保存很多中間函數的堆棧。比如f(n, sum) = f(n-1) + value(n) + sum; 會保存n個函數調用堆棧,而使用尾遞歸f(n, sum) = f(n-1, sum+value(n)); 這樣則只保留後一個函數堆棧即可,之前的可優化刪去。

  採用尾遞歸實現Fibonacci函數,程序如下所示:

1 int FibonacciTailRecursive(int n,int ret1,int ret2)
2 {
3    if(n==0)
4       return ret1; 
5     return FibonacciTailRecursive(n-1,ret2,ret1+ret2);
6 }

例如現在要計算n=5時的值,尾遞歸調用過程如下圖所示:

從圖可以看出,爲遞歸不需要向上返回了,但是需要引入而外的兩個空間來保持當前的結果。

  爲了更好的理解尾遞歸的應用,寫個程序進行練習。採用直接遞歸和尾遞歸的方法求解單鏈表的長度,C語言實現程序如下所示:

複製代碼
 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 
 4 typedef struct node
 5 {
 6   int data;
 7   struct node* next;
 8 }node,*linklist;
 9 
10 void InitLinklist(linklist* head)
11 {
12      if(*head != NULL)
13         free(*head);
14      *head = (node*)malloc(sizeof(node));
15      (*head)->next = NULL;
16 }
17 
18 void InsertNode(linklist* head,int d)
19 {
20      node* newNode = (node*)malloc(sizeof(node));
21      newNode->data = d;
22      newNode->next = (*head)->next;
23      (*head)->next = newNode;
24 }
25 
26 //直接遞歸求鏈表的長度 
27 int GetLengthRecursive(linklist head)
28 {
29     if(head->next == NULL)
30        return 0;
31     return (GetLengthRecursive(head->next) + 1);
32 }
33 //採用尾遞歸求鏈表的長度,藉助變量acc保存當前鏈表的長度,不斷的累加 
34 int GetLengthTailRecursive(linklist head,int *acc)
35 {
36     if(head->next == NULL)
37       return *acc;
38     *acc = *acc+1;
39     return GetLengthTailRecursive(head->next,acc);
40 }
41 
42 void PrintLinklist(linklist head)
43 {
44      node* pnode = head->next;
45      while(pnode)
46      {
47         printf("%d->",pnode->data);
48         pnode = pnode->next;
49      }
50      printf("->NULL\n");
51 }
52 
53 int main()
54 {
55     linklist head = NULL;
56     int len = 0;
57     InitLinklist(&head);
58     InsertNode(&head,10);
59     InsertNode(&head,21);
60     InsertNode(&head,14);
61     InsertNode(&head,19);
62     InsertNode(&head,132);
63     InsertNode(&head,192);
64     PrintLinklist(head);
65     printf("The length of linklist is: %d\n",GetLengthRecursive(head));
66     GetLengthTailRecursive(head,&len);
67     printf("The length of linklist is: %d\n",len);
68     system("pause");
69 }
複製代碼

程序測試結果如下圖所示:

參考:http://www.cnblogs.com/JeffreyZhao/archive/2009/03/26/tail-recursion-and-continuation.html

冷靜思考,勇敢面對,把握未來!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章