遞歸——很常見的一種編程技巧

什麼是遞歸?(what)

  • 遞歸時一種應用廣泛的算法(或者編程技巧),很多算法的編碼實現都要用到遞歸,比如DFS深度優先搜索、前中後序二叉樹遍歷等;
  • 所有的遞歸問題都可以用遞推公式來表達:f(n)=f(n-1)+a,f(1)=b;
    -遞歸就是函數調用函數本身,函數有返回值,並在一定條件下終止調用一層層返回。
  • 遞歸,函數調用自身:遞 ,函數有返回值:歸;

爲什麼用遞歸?(why)

優點:代碼表達力強,簡潔;
缺點:空間複雜度高,有堆棧溢出風險,存在重複計算可能,以及過多的函數調用會比較耗時;

什麼樣的問題用遞歸?(when)

  1. 一個問題的解可以分解爲幾個子問題的解;
    例如 f(n)=f(n-1)+f(n-2);
  2. 這個問題與分解的子問題,除數據規模不同,求解思路完全一樣;
    例如 f(n)=f(n-1)+f(n-2); n 、n-1 、 n-2只是數據不同但求解函數一樣都是f
  3. 存在遞歸終止條件;
    例如 f(1)=1;

怎樣編寫遞歸代碼?

理解

寫出遞推公式,找到終止條件

遞歸其實就是一個方程式:f(n) = f(n-1) + a;也就是說在設計遞歸的時候應該考慮下面三個方面:

  1. 求解f(n)的時候,假設f(n-1)已經求解出來了。我們不要去考慮f(n-1)是如何求解出來的。
  2. 關鍵點在於找到遞歸的終止條件。
  3. 遞歸往往和分治法是分不開的。對於複雜的遞歸,往往將遞歸拆分,然後再合併。

步驟

寫一個遞歸方法。

  1. 先寫判斷遞歸結束時候的操作;
  2. 再寫遞歸分解操作

栗子

漢諾塔問題就是使用典型的遞歸思想。
先推導最簡單的f(2),從而推f(n)的解可以分爲:

  1. 將 n-1 個圓盤從 from -> buffer
  2. 將 1 個圓盤從 from -> to
  3. 將 n-1 個圓盤從 buffer -> to
  4. 以上三步都是爲了求解f(n),最後我們給出遞歸結束的條件。只有一個圓盤的時候,只需一次移動操作即可from -> to。
/**
     * 漢諾塔問題
     */
    public static void move(int n,String from,String buffer,String to){
        if(n==1){
            System.out.println(from+"—>"+to);
            //必須有return
            return;
        }
        move(n-1,from,to,buffer);
        move(1,from,buffer,to);
        move(n-1,buffer,from,to);
    }

電影院問座位第幾排問題,f(n)=f(n-1)+1,f(1)=1,

	/**
     * 電影院座位排數問題
     * @param n 前面一排
     * @return
     */
    public static int ask(int n){
        if(n==1){
            return 1;
        }
        return ask(n-1)+1;
    }

注意點

1.警惕堆棧溢出:

遞歸用的是系統棧或者虛擬機函數調用棧,棧空間一般都不大,
如果遞歸求解的數據規模很大,調用層次很深,一直壓入棧,就會有堆棧溢出的風險。
可以聲明一個全局變量來控制遞歸的深度,從而避免堆棧溢出。

2.警惕重複計算:

例如 f(n)=f(n-1)+f(n-2) , f(5)=(f4)+f(3),f(4)=f(3)+f(2),這裏f(3)重複計算
通過散列表來保存已經求解過的值,從而避免重複計算。

熄燈

  • 遞歸必須有函數調用函數自身;
  • 遞歸必須有return;
  • 寫遞歸代碼的關鍵就是找到如何將大問題分解爲小問題的規律,並且基於此寫出遞推公式,然後再推敲終止條件,最後將遞推公式和終止條件翻譯成代碼。
  • 編寫遞歸代碼的關鍵是,只要遇到遞歸,我們就把它抽象成一個遞推公式,不用想一層層的調用關係,不要試圖用人腦去分解遞歸的每個步驟。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章