遞歸和尾遞歸的運行流程解釋

遞歸和尾遞歸的運行流程解釋

遞歸定義


遞歸(英語:recursion)在計算機科學中是指一種經過重複將問題分解爲同類的子問題而解決問題的方法。[1] 遞歸式方法能夠被用於解決不少的計算機科學問題,所以它是計算機科學中十分重要的一個概念。[2] 絕大多數編程語言支持函數的自調用,在這些語言中函數能夠經過調用自身來進行遞歸。計算理論能夠證實遞歸的做用能夠徹底取代循環,所以有不少在函數編程語言(如Scheme)中用遞歸來取代循環的例子。(摘自維基百科)編程

尾遞歸定義


在計算機學裏,尾調用是指一個函數裏的最後一個動做是返回一個函數的調用結果的情形,即最後一步新調用的返回值直接被當前函數的返回結果。[1]此時,該尾部調用位置被稱爲尾位置。尾調用中有一種重要而特殊的情形叫作尾遞歸。通過適當處理,尾遞歸形式的函數的運行效率能夠被極大地優化。[1]尾調用原則上均可以經過簡化函數調用棧的結構而得到性能優化(稱爲「尾調用消除」),可是優化尾調用是否方即可行取決於運行環境對此類優化的支持程度如何。(摘自維基百科)性能優化

前提知識


  • 遞歸我將它分爲兩個過程,一個我將它稱爲遞歸,另外一個我將它稱爲回溯. 遞歸的函數的運行主要有這兩個流程,遞歸的進入,回溯的退出,這兩個過程的分界是以遞歸出口爲分界的.
  • 遞歸的實現形式是使用,遞歸函數的進入(遞歸)相似與壓棧,遞歸函數的退出(回溯)相似於出棧.

遞歸樣例和解釋

【編程題】冪運算三(遞歸函數
題目ID:1137
【問題描述】 求x^n。
【輸入形式】一行2個數,第一個整數表示x,第二個大於等於零的整數表示n,二數之間用空格分隔。
【輸出形式】一行一個整數,表示x的n次方
【樣例輸入】2 3
【樣例輸出】8編程語言

【樣例說明】2的3次方結果爲8
【評分標準】5組測試用例,每組2分,共計10分 函數

【測試用例】
1)
輸入:
2 3
輸出:
8
2)
輸入:
3 5
輸出:
243性能

3)
輸入:
-17 4
輸出:
83521測試

4)
輸入:
22 0
輸出:
1優化

5)
輸入:
-1287 0
輸出:
1spa

//普通遞歸
#include<stdio.h>
long my_pow1(long x,int n){
    if(n==0) return 1;      //遞歸出口
    return x*(my_pow1(x,--n));  //除了調用自身外還乘多了個x,即通常的遞歸
}
int main(){
    long  x;
    int  n;
    scanf("%ld%d",&x,&n);
    printf("%ld\n",my_pow1(x,n));
    return 0;
}
  • 運行圖解

圖片描述
解釋:
普通的遞歸過程是在一個函數中,結果依靠自身的調用來得出,例如求冪運算,pow(2,3)表明求2的3次方,因爲pow(2,3)未知,咱們能夠把它分解成2*pow(2,2),pow(2,2)也未知,又可分解成2*pow(2,1),以此類推,直到pow(2,0)可知(if中定義0時返回1),即pow(2,0)返回值是1.
在這個遞歸過程當中,pow函數的創建就是一個個壓棧的過程
我把它稱爲函數棧3d

clipboard.png
壓棧壓入因此函數後,直到最後一個,能夠得到最後一個函數的返回值,由這個返回值能夠依次推出棧內全部函數的返回值(回溯),即退棧,pow(2,0)返回1,推的pow(2,1)返回2*pow(2,0),即2*1=2,pow(2,2)返回2*pow(2,1),即2*2=4,直到退到棧內最後一個函數pow(2,3),可得到pow(2,3)的返回值爲2*pow(2,2)即8;code

尾遞歸樣例和解釋


【編程題】吃糖(尾遞歸函數
題目ID:1135
【問題描述】小櫻是個愛吃糖的女孩, 哥哥送了她n(1<=n<=30)顆糖,怎麼吃?一天吃1顆;一天吃2顆。嗯,那就天天吃一顆或兩顆吧。
1顆糖,確定只有(1)一種吃法;2顆糖,有(1,1)和(2)兩種吃法;3顆糖,有(1,1,1)、(1,2)和(2,1)三種吃法。注 (2,1)表示第一天吃2顆,次日吃1顆。*
你能幫小櫻算出,吃完這n顆糖,有多少種吃法嗎?請編寫一個尾遞歸函數來解決此問題
【輸入形式】

clipboard.png

【測試用例】
1)
輸入:
1
輸出:
result=1

2)
輸入:
4
輸出:
result=5

3)
輸入:
15
輸出:
result=987

4)
輸入:
20
輸出:
result=10946

5)
輸入:
30
輸出:
result=1346269

實際上這道題是一個斐波那契數列的變體,可用尾遞歸函數解決

//尾遞歸
#include <stdio.h>
int ci(int n,int pre,int next)
{
    int sum;
    if (n==1){              //遞歸出口(遞歸和回溯的分界點)
        return pre;
    }
    return ci(n-1,next,pre+next);  //除了調用自身外沒有其餘操做即爲尾遞歸
}
int main()
{
    int n;
    int sum;
    scanf ("%d",&n);
    printf ("result=%d",ci(n,1,2));
    return 0;
}

運行圖解
圖片描述

解釋:

  • 尾遞歸是屬於遞歸的,它也有遞歸壓棧兩個過程,可是因爲尾遞歸除了調用自身外沒有任何其餘多餘的操做,因此它在進行到遞歸出口的時候,得到一個返回值,進行回溯的每個函數的返回值都是它的調用的另外一個規模縮小的函數,也就是說:函數在壓棧的時候,壓到棧頂函數,得到一個返回值,退棧,當前棧頂得到返回值,這個值是退棧的那個元素的返回值,依次類推,直到全部函數退棧,得到的返回值都是一個值,也就是遞歸出口的那個值.
  • 用例子來表示,便是回溯過程當中,ci(1,5,8)得到了返回值5,ci(2,3,5)的返回值是ci(1,5,8),也就是5,ci(3,2,3)的返回值是ci(2,3,5),也就是5,ci(4,1,2)的返回值也是5;
  • 從尾遞歸的流程中能夠看出,尾遞歸的回溯的過程是徹底沒用的,直到遞歸出口就已經得到了想要的值了.
  • 遞歸的過程也只是至關於循環而已,ci(4,1,2),4是要求斐波那契變體的第4項,1是第一項,2是第二項,從4減到1起到一個計數做用,1和2也是做爲遞推的起點
  • 因此,在不少語言中,尾遞歸可被優化成和循環(遞推)同樣的效率.

總結

  • 遞歸有兩個過程,遞歸和回溯,遞歸到最深處得到一個返回值,由這個返回值回溯推出想要求的函數的返回值,起到縮小規模計算的做用.
  • 尾遞歸與遞歸的不一樣處在於尾遞歸在return處除了調用自身外沒有其餘操做,致使回溯的過程是徹底浪費的.
  • 尾遞歸在一些語言中能夠優化成和循環同樣的執行效率.

*下圖是普通遞歸和尾遞歸的執行圖示:
圖片描述
圖片描述

相關文章
相關標籤/搜索