算法一看就懂之「 遞歸 」

以前的文章我們已經聊過了「 數組和鏈表 」「 堆棧 」「 隊列 」,今天我們來看看「 遞歸 」,固然「 遞歸 」並非一種數據結構,它是不少算法都使用的一種編程方法。它太廣泛了,而且用它來解決問題很是的優雅,但它又不是那麼容易弄懂,因此我特地用一篇文章來介紹它。算法

1、「 遞歸 」是什麼?

遞歸 就是指函數直接或間接的調用本身,遞歸是基於棧來實現的。遞歸的經典例子就是 斐波拉契數列(Fibonacci)。通常若是能用遞歸來實現的程序,那它也能用循環來實現。用遞歸來實現的話,代碼看起來更清晰一些,但遞歸的性能並不佔優點,時間複雜度甚至也會更大一些。編程


上圖爲 斐波拉契數列 圖例。數組

要實現遞歸,必須知足2個條件:微信

  1. 可調用本身數據結構

    就是咱們要解決的這個問題,能夠經過函數調用本身的方式來解決,便可以經過將大問題分解爲子問題,而後子問題再能夠分解爲子子問題,這樣不停的分解。而且大問題與子問題/子子問題的解決思路是徹底同樣的,只不過數據不同。所以這些問題都是經過某一個函數去解決的,最終咱們看到的就是不停得函數調用本身,而後就把問題化解了。架構

    若是這個問題不能分解爲子問題,或子問題的解決方法與大問題不同,那就沒法經過遞歸調用來解決。函數

  2. 可中止調用本身性能

    中止調用的條件很是關鍵,就是大問題不停的一層層分解爲小問題後,最終必須有一個條件是來終止這種分解動做的(也就是中止調用本身),作遞歸運算必定要有這個終止條件,不然就會陷入無限循環。大數據

下面仍是以 斐波拉契數列(Fibonacci)爲例,咱們來理解一下遞歸:spa

斐波拉契數列就是由數字 1,1,2,3,5,8,13…… 組成的這麼一組序列,特色是每位數字都是前面相鄰兩項之和。若是咱們但願得出第N位的數字是多少?

  1. 可使用循環的方式求解:

    這裏就不列代碼了,思路是:咱們知道最基本的狀況是 f(0)=0,f(1)=1,所以咱們能夠設置一個一個循環,循環從i=2開始,循環N-1次,在循環體內 f(i)=f(i-1)+f(i-2),直到i=N-1,這樣循環結束的時候就求出了f(N)的值了。

  2. 更優雅的方式是使用遞歸的方式求解:

    咱們知道斐波拉契數列的邏輯就是:


    能夠看出,這個邏輯是知足上面2個基本條件,假如求解 f(3),那 f(3)=f(2)+f(1),所以咱們得繼續去求解f(2),而 f(2)=f(1)+f(0),所以整個求解過程其實就在不斷的分解問題的過程,將大問題f(3),分解爲f(2)和f(1)的問題,以此類推。既然能夠分解成子問題,而且子問題的解決方法與大問題一致,所以這個問題是知足「可調用本身」的遞歸要求。

    同時,咱們也知道應該在什麼時候中止調用本身,即當子問題變成了f(0)和f(1)時,就再也不須要往下分解了,所以也知足遞歸中「可中止調用本身」的這個要求。

    因此,斐波拉契數列問題能夠採用遞歸的方式去編寫代碼,先看圖:


    咱們將代碼寫出來:

    int Fb(int n){
       if(n<=1) return n==0?0:1;
       return Fb(n-1)+Fb(n-2); //這裏就是函數本身調用本身
    }

    從上面的例子能夠看出,咱們寫遞歸代碼最重要的就是寫2點:

  3. 遞推公式

    上面代碼中,遞推公式就是 Fb(n)=Fb(n-1)+Fb(n-2),正是這個公式,才能夠一步步遞推下去,這也是函數本身調用本身的關鍵點。所以咱們在寫遞歸代碼的時候最首先要作的就是思考整個邏輯中的遞推公式。

  4. 遞歸中止條件

    上面代碼中的中止條件很明顯就是:if(n<=1) return n==0?0:1;這就是遞歸的出口,想出了遞推公司以後,就要考慮遞歸中止條件是啥,沒有中止條件就會無限循環了,一般遞歸的中止條件是程序的邊界值。

    咱們對比實現斐波拉契數列問題的2種方式,能夠看出遞歸的方式比循環的方式在程序結構上更簡潔清晰,代碼也更易讀。但遞歸調用的過程當中會創建函數副本,建立大量的調用棧,若是遞歸的數據量很大,調用層次不少,就會致使消耗大量的時間和空間,不只性能較低,甚至會出現堆棧溢出的狀況。

    咱們在寫遞歸的時候,必定要注意遞歸深度的問題,隨時作好判斷,防止出現堆棧溢出。

    另外,咱們在思考遞歸邏輯的時候,不必在大腦中將整個遞推邏輯一層層的想透徹,通常人都會繞暈的。大腦很辛苦的,咱們應該對它好一點。咱們只須要關注當前這一層是否成當即可,至於下一層不用去關注,當前這一層邏輯成立了,下一層確定也會成立的,最後只須要拿張紙和筆,模擬一些簡單數據代入到公式中去校驗一下遞推公式對不對便可。

2、「 遞歸 」的算法實踐?

咱們看看常常涉及到 遞歸 的 算法題(來源leetcode)

算法題:實現 pow(x, n) ,即計算 x 的 n 次冪函數。

說明:
    -100.0 < x < 100.0
    n 是 32 位有符號整數,其數值範圍是 [−2^31, 2^31 − 1]

示例:
輸入: 2.00000, 10
輸出: 1024.00000

解題思路:

方法一:
暴力解法,直接寫一個循環讓n個x相乘嘛,固然了這種方式就沒啥技術含量了,時間複雜度O(1),代碼省略了。

方法二:
基於遞歸原理,很容易就找出遞推公式 f(n)=x*f(n-1),再找出遞歸中止條件即n==0或1的狀況就能夠了。不過稍微須要注意的是,由於n的取值能夠是負數,因此當n小於0的時候,就要取倒數計算。代碼以下:
class Solution {
    public double myPow(double x, int n) {
        if(n==0) return 1;
        if(n==1) return x;
        if(n<0) return 1/(x*myPow(x,Math.abs(n)-1));
        return x*myPow(x,n-1);
    }
}
這個方法其實也有問題,當n的數值過大時,會堆棧溢出的,看來也是不最佳解,繼續往下看。

方法三:
利用分治的思路,將n個x先分紅左右兩組,分別求每一組的值,而後再將兩組的值相乘就是總值了。即 x的n次方 等於 x的n/2次方 乘以 x的n/2次方。以此類推,左右兩組其實還能夠分別各自繼續往下分組,就是一個遞推思想了。可是這裏須要考慮一下當n是奇數的狀況,作一個特殊處理便可,代碼以下:
class Solution {
    public double myPow(double x, int n) {
        //若是n是負數,則改成正數,但把x取倒數
        if(n<0) {
            n = -n;
            x = 1/x;
        }
        return pow(x,n);

    }

    private double pow(double x, int n) {
        if(n==0) return 1;
        if(n==1) return x;
        double half = pow(x,n/2);
        //偶數個
        if(n%2==0) {
            return half*half;
        }
        //奇數個
        return half*half*x;
    }
}
這種方法的時間複雜度就是O(logN)了。

以上,就是對數據結構中「 遞歸 」的一些思考。

碼字不易啊,喜歡的話不妨轉發朋友,或點擊文章右下角的「在看」吧。😊

本文原創發佈於微信公衆號「 不止思考 」,歡迎關注。涉及 思惟認知、我的成長、架構、大數據、Web技術 等。 

相關文章
相關標籤/搜索