數據結構之遞歸(Recursion)------分而治之

1.遞歸的定義
在定義一個過程或函數時出現調用本過程或本函數的成分,稱之爲遞歸。
直接遞歸:函數調用自身。
間接遞歸:過程或函數p調用過程或函數q,而q又調用p。
尾遞歸:一個遞歸過程或遞歸函數中遞歸調用語句是最後一條執行語句。尾遞歸只是一個變形的循環,可以很容易用循環來代替。在含有循環結構的語言中,不推薦使用尾部遞歸。

例如階乘函數的定義: 在該函數fun(n)求解過程中,直接調用fun(n-1)(語句4)自身,所以它是一個直接遞歸函數。又由於遞歸調用是最後一條語句,所以它又屬於尾遞歸。

    public int fun(int n) {          
        if (n == 1) return 1;
        else return fun(n - 1) * n;         
    }
  public int fun(int n) {
        int result = 1;
        for (int i = 1; i <= n; i++)
            result *= i;
        return result;
    }

2.遞歸是如何實現的
2.1遞歸調用不是表面上的函數自身調用,而是一個函數的實例調用同一個函數的另一個實例(是兩個不同的實例)
2.2函數調用的過程中處理要素包括:函數控制權的轉接工作(利用函數入口地址和返回地址),局部變量的空間分配工作,實參和形參的傳遞,函數的返回結果傳遞。

3.何時使用遞歸
1. 定義是遞歸的(數學公式、數列等的定義)
2. 數據結構是遞歸的 (單鏈表就是一種遞歸數據結構 sum函數)
3. 問題的求解方法是遞歸的
eg:1.有三根杆子A,B,C。A杆上有若干碟子
   2.每次移動一塊碟子,小的只能疊在大的上面
   3.把所有碟子從A杆全部移到C杆上
解:題中只給了三座塔,我們利用C塔將圓盤堆在B塔。首先將A塔的1號圓盤放在B塔,A塔的2號圓盤放在C塔,再把放在B塔的1號圓盤放在C塔,此時C塔擁有兩個圓盤按要求自下而上從小到大排列。接下來將A塔的3號圓盤放在B塔,將C塔的1號圓盤放在B塔,把C塔德2號圓盤放在A塔,再把B塔的1號圓盤放在A塔,此時C塔空,1號2號按要求排在A塔,B塔只有3號圓盤。此時把B塔3號圓盤放在C塔,把A塔德1號放在B塔嗎,把A塔德2號房在C塔,再把B塔德1號放在C塔,此時B塔空,C塔按要求排有123號圓盤。這次把A塔的4號圓盤放在B塔,這次就比較麻煩了先把C塔的1號放在A塔,C塔的2號房在B塔,再把A塔德1號放在B塔,把C塔德3號放在A塔,再把B塔的1號放在C塔,把B塔德2號放在A塔,再把C塔德1號放在A塔,此時C塔空,B塔只有4號圓盤,A塔按要求房有123到N號圓盤,缺4號圓盤。現在把B塔的4號圓盤房在C塔,現在推回去,把A塔德1號房在C塔,A塔的2號房在B塔,再把C塔的1號放在B塔,把A塔德3號房再C塔,此時剛好是3號壓4號於C塔,再把,B塔的1號房在A塔,把C塔的2號放在C塔,把A塔的1號放在C塔,這下剛好推回來,此時B塔空,A塔最上面是5號圓盤,C塔按要求放有1234號圓盤。 按這樣的遞推方法,將n-1個圓盤按要求放在C塔,第n個圓盤放在B塔,現在A塔空。n號圓盤是最大的圓盤,按問題要求我們終於把n號最大的圓盤放在了B塔,這下藉助已空的A塔聯合BC塔推回來,就可以把n個圓盤按要求放在B塔。

public class Recursion {

    public void move(int n, char a, char b, char c) {
        if (n == 1)
            System.out.println("盤 " + n + " 由 " + a + " 移至 " + c);
        else {
            move(n - 1, a, c, b);
            System.out.println("盤 " + n + " 由 " + a + " 移至 " + c);
            move(n - 1, b, a, c);
        }
    }

    public static void main(String[] args) {
        new Recursion().move(5,'A','B','C');
    }
}

eg:八皇后問題,是一個古老而著名的問題,是回溯算法的典型案例。該問題是國際西洋棋棋手馬克斯·貝瑟爾於1848年提出:在8×8格的國際象棋上擺放八個皇后,使其不能互相攻擊,即任意兩個皇后都不能處於同一行、同一列或同一斜線上,問有多少種擺法。 高斯認爲有76種方案。1854年在柏林的象棋雜誌上不同的作者發表了40種不同的解,後來有人用圖論的方法解出92種結果。計算機發明後,有多種計算機語言可以解決此問題。———以上節選自百度百科。

解:從第一行第一列開始逐行擺放皇后依題意每行只能有一個皇后,遂逐行擺放,每行一個皇后即可擺放後立即調用一個驗證函數(傳遞整個棋盤的數據),驗證合理性,安全則擺放下一個,不安全則嘗試擺放這一行的下一個位置,直至擺到棋盤邊當這一行所有位置都無法保證皇后安全時,需要回退到上一行,清除上一行的擺放記錄,並且在上一行嘗試擺放下一位置的皇后(回溯算法的核心)當擺放到最後一行,並且調用驗證函數確定安全後,累積數自增1,表示有一個解成功算出,驗證函數中,需要掃描當前擺放皇后的左上,中上,右上方向是否有其他皇后,有的話存在危險,沒有則表示安全,並不需要考慮當前位置棋盤下方的安全性,因爲下面的皇后還沒有擺放。

public class Recursion {

    private int[][] arry = new int[8][8];    //棋盤,放皇后
    private int map = 0;     //存儲方案結果

    public void find(int i) {    //尋找皇后節點
        if (i > 7) {    //八皇后解
            map++;
            print();
            return;

        }

        for (int m = 0; m < 8; m++) {       //深度優先,遞歸算法
            if (rule(arry, i, m)) {
                arry[i][m] = 1;
                find(i + 1);
                arry[i][m] = 0;
            }
        }

    }

    public boolean rule(int arry[][], int k, int j) {    //判斷節點是否合適
        for (int i = 0; i < 8; i++) {       //行列衝突
            if (arry[i][j] == 1)
                return false;
        }
        for (int i = k - 1, m = j - 1; i >= 0 && m >= 0; i--, m--) {    //左對角線
            if (arry[i][m] == 1)
                return false;
        }
        for (int i = k - 1, m = j + 1; i >= 0 && m <= 7; i--, m++) {    //右對角線
            if (arry[i][m] == 1)
                return false;
        }
        return true;
    }

    public void print() {      //打印方法結果
        System.out.print("方案" + map + ":");
        for (int i = 0; i < 8; i++) {
            for (int m = 0; m < 8; m++) {
                if (arry[i][m] == 1) {
                    System.out.print("皇后" + (i + 1) + "在第" + i + "行,第" + m + "列\t");

                }
            }
        }
        System.out.println();
    }

    public static void main(String[] args) {
//        new Recursion().move(5, 'A', 'B', 'C');
        new Recursion().find(0);
    }

}

eg:斐波納契數列,又稱黃金分割數列,指的是這樣一個數列:1、1、2、3、5、8、13、21、……在數學上,斐波納契數列以如下被以遞歸的方法定義:F0=0,F1=1,Fn=F(n-1)+F(n-2)(n>=2,n∈N*)。

public class Fibonacci {

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.println("Please input this fibonacci n:");
        int n = scanner.nextInt(); // 假設輸入爲大於零的整數  
        System.out.println(fibonacci(6) + ":" + fibonacciNormal(6));
        int sum = 0;
        for(int i = 1; i <= n; i++){
            sum += fibonacci(i);
        }
        System.out.println(sum);
    }

    // 遞歸實現方式  
    public static int fibonacci(int n){
        if(n <= 2){
            return 1;
        }else{
            return fibonacci(n-1) + fibonacci(n-2);
        }
    }

    // 遞推實現方式  
    public static int fibonacciNormal(int n){
        if(n <= 2){
            return 1;
        }
        int n1 = 1, n2 = 1, sn = 0;
        for(int i = 0; i < n - 2; i ++){
            sn = n1 + n2;
            n1 = n2;
            n2 = sn;
        }
        return sn;
    }
}

4.遞歸算法的設計 之 分而治之
遞歸的求解的過程均有這樣的特徵:先將整個問題劃分爲若干個子問題,通過分別求解子問題,最後獲得整個問題的解。而這些子問題具有與原問題相同的求解方法,於是可以再將它們劃分成若干個子問題,分別求解,如此反覆進行,直到不能再劃分成子問題,或已經可以求解爲止。這種自上而下將問題分解、求解,再自上而下引用、合併,求出最後解答的過程稱爲遞歸求解過程。這是一種分而治之的算法設計方法。
遞歸設計的步驟如下:
(1)對原問題f(s)進行分析,假設出合理的“較小問題”f(s’)(與數學歸納法中假設n=k-1時等式成立相似);
(2)假設f(s’)是可解的,在此基礎上確定f(s)的解,即給出f(s)與f(s’)之間的關係(與數學歸納法中求證n=k時等式成立的過程相似);
(3)確定一個特定情況(如f(1)或f(0))的解,由此作爲遞歸出口(與數學歸納法中求證n=1時等式成立相似)。

5.遞歸算法到非遞歸算法的轉換
遞歸算法的時間效率通常比較差。因此,對求解某些問題時,我們希望用遞歸算法分析問題,用非遞歸算法具體求解問題。這就需要把遞歸算法轉換爲非遞歸算法。
把遞歸算法轉化爲非遞歸算法有如下三種基本方法:
(1)對於尾遞歸和單向遞歸的算法,可用循環結構的算法替代。
(2)自己用棧模擬系統的運行時棧,通過分析只保存必須保存的信息,從而用非遞歸算法替代遞歸算法。
(3)利用棧保存參數,由於棧的後進先出特性吻合遞歸算法的執行過程,因而可以用非遞歸算法替代遞歸算法。

第(1)種和第(2)種情況的遞歸算法轉化爲非遞歸算法的問題,前者是一種是直接轉化法,不需要使用棧,後者是間接轉化法,需要使用棧。第(3)種情況也需要使用棧,但因具體情況而異,

尾遞歸是遞歸調用語句只有一個,而且是處於算法的最後。 這個特別容易轉換。典型的例子是 求階乘算法。
單向遞歸是指遞歸函數中雖然有一處以上的遞歸調用語句,但各次遞歸調用語句的參數只和主調用函數有關,相互之間參數無關,並且這些遞歸調用語句,也與尾遞歸一樣處於算法的最後。單向遞歸的典例就是Fibonacci數列。

一般對於尾遞歸和單向遞歸可採用循環消除。(原因是,遞歸返回時,正好是算法的末尾,相當於保存的返回信息和返回值根本不需要被保存)採用循環結構消除遞歸沒有通用的轉換算法,對於具體問題要深入分析對應的遞歸結構,設計有效的循環語句進行遞歸到非遞歸的轉換。
模擬系統運行時的棧消除遞歸(從現在開始培養解決問題運用遞歸的分而治之的思維並培養消除遞歸的能力) 對於不屬於尾遞歸和單向遞歸的遞歸算法,很難轉化爲與之等價的循環算法。但所有的遞歸程序都可以轉化爲與之等價的非遞歸程序。
直接使用棧保存中間結果,從而將遞歸算法轉化爲非遞歸。

在設計棧時,除了保存遞歸函數的參數外,還增加了一個標誌成員(tag),對於某個遞歸小問題f(s`),其值爲1表示對應的遞歸問題尚未求出,需進一步分解轉換,值爲0表示對應遞歸問題已求出,需通過該結果求解大問題f(s).
直接使用棧保存中間結果,從而將遞歸算法轉化爲非遞歸。

在設計棧時,除了保存遞歸函數的參數外,還增加了一個標誌成員(tag),對於某個遞歸小問題f(s`),其值爲1表示對應的遞歸問題尚未求出,需進一步分解轉換,值爲0表示對應遞歸問題已求出,需通過該結果求解大問題f(s).

直接使用棧保存中間結果,從而將遞歸算法轉化爲非遞歸。

在設計棧時,除了保存遞歸函數的參數外,還增加了一個標誌成員(tag),對於某個遞歸小問題f(s`),其值爲1表示對應的遞歸問題尚未求出,需進一步分解轉換,值爲0表示對應遞歸問題已求出,需通過該結果求解大問題f(s).

爲方便討論,將遞歸模型分爲等值關係和等價關係兩種。
等值關係:“大問題”的函數值等於“小問題”的函數值的某種運算結果,例如求n!對應的遞歸模型就是等值關係。
對於“等值關係”通用解決方案:
1、設計壓棧數據元素
2、首先將初始壓棧,tag初始爲0(還可跟據需要,加入返回值等),如果有按條件返回的多個分支遞歸,可設置一個trans標記,然後壓棧的時,轉到對應的分支
3、當棧不爲空時,循環(每次最棧頂元素判斷是壓棧過程還是解棧過程)
壓棧存儲參數,迴歸時期解決具體問題。

等價關係:
等價關係是指“大問題”的求解過程轉化爲“小問題”求解而得到的,它們之間不是值的相等關係,而是解的等價關係 。 例如,求梵塔問題對應的遞歸模型就是等價關係,也就是說,Hanoi(n,x,y,z)與Hanoi(n-1,x,z,y)、move(n,x,z)和Hanoi(n-1,y,x,z)是等價的。

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