遞歸的一些問題實現及尾遞歸思考

Part 1 什麼是遞歸:
我們知道循環(iteration)和遞歸(recursion)可以理解爲孿生兄弟,遞歸是函數抽象表達的一種。遞歸的優點顯而易見,它在某些條件下,比循環代碼量更少。遞歸簡單來說,就是在運行過程中調用自己。而遞歸的實現需要滿足兩個條件,存在限制條件,在函數體同時在遞歸過程中不斷逼近限制條件。(此階段暫不考慮棧溢出)
Part 2 一些遞歸的問題(任何理論逃不開實例):
(1)漢諾塔問題:
首先我們要知道什麼是漢諾塔問題:
這源於一個印度的傳說,作者爲避免文字誤會,直接引用:“大梵天創造世界的時候做了三根金剛石柱子,在一根柱子上從下往上按照大小順序摞着64片黃金圓盤。大梵天命令婆羅門把圓盤從下面開始按大小順序重新擺放在另一根柱子上。並且規定,在小圓盤上不能放大圓盤,在三根柱子之間一次只能移動一個圓盤。”在這裏插入圖片描述
簡言之,現在有3個柱子,其中A柱有n個鐵片從上至下爲從小至大的順序,B,C柱爲空柱,需要利用B柱,將A柱上貼片轉移到C柱,但是在過程中滿足,大鐵片不能出現在小鐵片之上。
首先我們將n設置爲3,觀察移動過程:






借用知乎博主醬紫君gif
在這裏插入圖片描述

假設n=3的過程我們已經知道,此時我們來思考n=4時的過程
首先,我們將A上的前3個貼片移到B柱子上,則會出現下面的情景(過程一):
在這裏插入圖片描述
再將A上的最後一個貼片移到C柱上(過程2)
在這裏插入圖片描述
此時我們只需要將B柱子上的的三個貼片重複n=3的過程轉移到C柱上(過程3)我們便完成了大業,以下是全部的過程的gif:
在這裏插入圖片描述
此時我們已經很輕鬆的理解了移動次數的遞推公式A4=(A3)*2+1
下面我們來思考一下全過程的實現函數,首先我們來定義一下函數。







void hanoi(char A,char B,char C,int n)

這時候n=3的過程可以表示爲hanoi(A,B,C,3),首先我們需要理解此函數,代表將A柱子上的n個貼片,通過B柱,移動到C柱;
在編寫代碼之前我們需要將n=3推廣到n,我們抽象一下過程1,2,3;請記住n-1個貼片移動過程我們假設實現了!!!(此處爲區別形參,實參,形參用大寫表示,實參用小寫)
中止條件:當n=1的時候,將1個貼片從a柱移動到c柱;
過程一:將n-1個鐵片從a柱通過c柱子移動b柱;
過程二:將1個貼片從a柱移動到c柱;
過程三:將n-1個貼片從b柱通過a柱移動到c柱;
好的有了以上的過程我們的代碼就寫好了:





#include<stdio.h>
void hanoi(char A,char B,char C,int n)
{
   
   
    if(n==1){
   
   
        printf("\n move %d from %c to %c ",1,A,C);//中止條件
    }else{
   
   
        hanoi(A, C, B, n - 1);//將n-1個鐵片從a柱通過c柱子移動b柱
        printf("\n move %d from %c to %c", 1, A, C);//將1個貼片從a柱移動到c柱
        hanoi(B, A, C, n - 1);//將n-1個貼片從b柱通過a柱移動到c柱
    }
    
}
int main(){
   
   
    hanoi('a', 'b', 'c', 4);
    return 0;
}

運行結果是:(當然你可以將n設置爲64,看看地球毀滅還有多久)
在這裏插入圖片描述
(2)青蛙跳臺階問題
問題:
青蛙一次可以跳躍兩級或一級臺階,現有n級臺階,那麼他有多少種跳法?
有了漢諾塔問題的思路,我們可以模仿以上思路。
中止條件:
1、當n=1時候,顯然只有一種跳法;
2、當n=2時候,有兩種跳法(一步兩級,兩次一級);
抽象過程:
先建立兩個變量,最後一步跳兩級的two_step,最後一步跳一級的one_step,再假設helper(n-2),helper(n-1)已經完成了,那這個問題就迎刃而解啦!代碼如下:









#include <stdio.h>
int helper(int step){
   
   
    if(step==1){
   
   
        return 1;//中止條件
    }else if(step==2){
   
   
        return 2;//中止條件
    }else{
   
   
        int two_step = helper(step - 2);//最後一步兩級
        int one_step = helper(step - 1);//最後一步一級
        return one_step + two_step;//所有的可能就是最後一步兩級+最後一步一級嘛
    }
}
int main(){
   
   
    printf("%d", helper(5));
    return 0;
}

(3)分巧克力問題
問題:
現有n塊巧克力(函數中形參chocolate就是n),將n塊巧克力分配,一份最多有limit塊巧克力,那麼有多少種分法呢?(假設有6塊,limit爲4,那麼6=4+2就是一種分法)
中止條件:
1、巧克力是0塊,1種分法
2、巧克力小於0塊(防止多分),0種分法
3、limit是0,那麼也是0種分法
抽象過程:
我們知道limit以下直至1的所有自然數都可以使用,那麼我們將這一次分配分爲兩種情況,with_it(有這個數字,那自然總數爲chocolate-limit,假設limit可重複,那麼limit不改變),without_it(沒有這個數字,總數還是chocolate,而limit-1了),那麼代碼如下:







#include<stdio.h>
int helper(int chocolate,int limit){
   
   
    if(chocolate==0){
   
   
        return 1;
    }else if(chocolate <0){
   
   
        return 0;
    }else if(limit==0){
   
   
        return 0;
    }else{
   
   
        int with_it = helper(chocolate - limit, limit);
        int without_it = helper(chocolate, limit - 1);
        return with_it+without_it;
    }
}
int main(){
   
   
    printf("%d", helper(6, 4));
    return 0;
}

Part 3 遞歸思路的總結和迭代比較:
通過以上3個問題不難發現,遞歸對n的問題,我們只需要假設n-1的做法已經完成,並尋求n和n-1(甚至是n-2)之間的關係(利用n-1情況已完成去實現n),就可以弄清楚遞歸的總過程。另一點便是尋找中止條件,換句話說就是特殊情況,n=0,1,2時的情況。
相比於迭代,代碼量明顯減少。但是由於遞歸分爲,遞歸前進段和遞歸返回段,對棧的調用極大,容易造成棧溢出。
Part 4 尾遞歸思考:
上文提到棧溢出的問題,我們需要一些方法去解決他,這時候尾遞歸便來啦!
在說明尾遞歸之前,我們先要知道,遞歸的實現過程,我們用比較簡單的階乘來描述一下:




int fact_recursion(int n){
   
   
    if(n==1){
   
   
        return 1;
    }
    return n * fact_recursion(n - 1);
}

而它的實現過程是這樣的
在這裏插入圖片描述
不難發現由於遞歸過程,3,2,在過程中都需要佔用棧來記住,當n較小時,問題不大,當n很大時,自然就棧溢出了,那我們來試一下n=100000;
在這裏插入圖片描述
那如何解決呢,我們來修改一下代碼。問題的根源在於,調用過程中3,2佔用了內存,那假設我們設計另一個函數,在函數每一次運行的過程中將3,2作爲參數傳入,是不是就可以避免了呢?



long tail_recusion_fact(long n,long result){
   
   
    if(n==1){
   
   
        return result;
    }
    return tail_recusion_fact(n - 1, result * n);
}

由於利用result這個參數去記錄了每一層的結果,那就不需要棧去存儲每一個3,2了
下面引用一下百科的尾遞歸原理:

當編譯器檢測到一個函數調用是尾遞歸的時候,它就覆蓋當前的活動記錄而不是在棧中去創建一個新的。編譯器可以做到這點,因爲遞歸調用是當前活躍期內最後一條待執行的語句,於是當這個調用返回時棧幀中並沒有其他事情可做,因此也就沒有保存棧幀的必要了。通過覆蓋當前的棧幀而不是在其之上重新添加一個,這樣所使用的棧空間就大大縮減了,這使得實際的運行效率會變得更高。

當然不是所有語言都支持尾遞歸優化。
Part 5總結:
遞歸是一種高效的編程技巧,在函數編寫和數據結構中發揮着重要的作用。當然遞歸也不是萬能的,畢竟電腦的算力也是有限的,有時候多寫幾行代碼,或許比棧溢出來的更爲實效哦!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章