遞歸

遞歸

若一個對象部分的包含自己,或用他自己給自己定義,被稱爲遞歸。
簡而言之,就是自己調用自己。

問大家一個問題,你認爲可以無限遞歸嗎?

首先,遞歸調用會進行入棧和棧幀回退,消耗棧空間;其次,我們棧空間有限,Win下的VS2008編譯器默認堆棧分配大小是1M,當棧空間不足時,會產生棧溢出錯誤。
最後,它不同於死循環,比如while(1),會一直佔用CPU,直至操作系統時間片用完,會由執行狀態轉換到就緒態。


接下來先看一段代碼

struct Node
{
    int m;
    Node node;
    //Node* node1;
};

int main()
{
    Node n;
    return 0;
}

在你看來,這段代碼可以運行嗎?
答案是否定的,Node node;這行代碼會造成程序無限遞歸下去,編譯都無法通過。
編譯器會提示“不允許使用不完整的類型”,從根本上杜絕了你的錯誤,因爲我們在設計時,類中不能包含自己的實體。一般都會用註釋那段代碼,Node* node1。

斐波拉契數列在c語言中算是比較經典的,有遞歸和非遞歸實現方法,
斐波拉契數列,例1, 1, 2, 3, 5, 8, 13, 21…

 //遞歸實現
int Fac(int n)
{
    if(n == 1||n == 2)
        return 1;
    else
        return Fac(n-1)+Fac(n-2);
}

這裏寫圖片描述

這裏寫圖片描述
遞歸下來,經測試,n=10時,Fac(10) = 55,需要函數調用109次;
n=20時,Fac(20) = 6765,需要函數調用13529次。

 //非遞歸
int Fac1(int n)
{
    int a = 1;
    int b = 1;
    int c = 1;
    if(n <= 2)
        return a;
    for(int i = 3;i <= n;++i)
    {
        a = b + c;
        b = c;
        c = a;
    }
    return a;
}

非遞歸下來,相當於迭代,時間複雜度是O(n),效率高。
兩種方法對比一看,遞歸實現效率太低,很多步重複計算,n越大,重複計算量呈指數增長,所以一般斐波拉契數列不建議使用這種遞歸方法。
這裏寫圖片描述

如果我們這裏把非遞歸實現中的循環改爲遞歸,情況會怎麼樣呢?

//循環改爲遞歸
int fac(int a,int b,int n)
{
    if(n<=2)
        return a;
    else
        return fac(a+b,a,n-1);
}

int fun(int n)
{
    int a=1,b=1;
    return fac(a,b,n);
}

這裏寫圖片描述
測試數據後,效率和非遞歸實現相同,時間複雜度是O(n)。


還有一個問題是,我們一般輸出數組元素,簡單地循環輸出就可以,如果改成遞歸輸出,會是怎麼樣呢?

//n是數組br長度
void Print(int *br,int n)
{
    if(n > 0)
    {
        cout<<br[n-1]<<" ";
        Print_Array(br,n-1);
    }
}//34,23,12

void Print_Array1(int *br,int n)
{
    if(br == NULL || n < 1)//需進行參數檢查
        return;
    Print(br,n);
}

int main()
{
    int arr[]={12,23,34};
    int n = sizeof(arr)/sizeof(arr[0]);

    Print_Array(arr,n);

    return 0;
}

這裏寫圖片描述
這樣輸出後,數組元素是逆序的,遞歸調用是先輸出數組元素,然後函數壓入棧,遞歸條件結束,最後棧回退。我們這裏可以將輸出和函數調用順序替換,這樣就會順序輸出數組元素。

void Print1(int *br,int n)
{
    if(n > 0)
    {
        Print1(br,n-1);
        cout<<br[n-1]<<" ";
    }
}

將上邊這段代碼稍加修改,n-1處改爲–n,結果會是什麼?

void Print2(int *br,int n)
{
    if(n > 0)
    {
        Print2(br,--n);
        cout<<br[n-1]<<" ";
    }
}
看到--n,不由地就想到了前置操作及特性,遞歸進行到結束條件,輸出br[n-1]會造成數組越界,輸出br[-1],是個隨機數,接着是數組前兩個元素。

這裏寫圖片描述

再加修改,把–n改成n–,又會怎麼樣呢?相信這會的已經被各種–整懵了,那就猜一下結果。

void Print3(int *br,int n)
{
    if(n > 0)
    {
        Print3(br,n--);
        cout<<br[n-1]<<" ";
    }
}
編譯器在處理後置時:是將值放入棧中,在輸出時直接出棧就行;
     在處理前置時:是等運算完成後,直接從n的地址中取值。

這段代碼運行會奔潰,出現棧溢出錯誤。後置–會在函數調用完後執行–,n一直等於3,會無限遞歸下去,直至棧空間不足,出現棧溢出,程序奔潰。

引用是經常會使用到的,這裏對代碼再加修改,參數列表中進行引用n。

void Print4(int *br,int &n)
{
    if(n > 0)
    {
        Print4(br,--n);
        cout<<br[n]<<" ";
    }
}
結果是12 12 12,先是會進行--操作,然後遞歸調用,遞歸條件結束時,n=0,這裏是引用n,是n的地址,輸出br[n]會連續輸出br[0]。

如果這裏是後置–,程序會編譯無法通過。因爲內置類型產生的臨時量放在寄存器中,具有常性,所以是不能改變的,不能使用普通的引用。

(只是平時學習的一些總結,如果有什麼錯誤或者問題,大佬們可以指點留言,互相交流學習嘛,謝謝支持!)

發佈了40 篇原創文章 · 獲贊 6 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章