1. 程式人生 > >遞歸的入門介紹

遞歸的入門介紹

遞歸

技術分享圖片

這是一只海龜,但小程要介紹的是遞歸。

遞歸是一種很重要的結構與設計思想。之所以重要,是因為讀者經常會遇到遞歸結構,或遞歸算法。而為了解決某些問題,遞歸的設計思想有時很有效果。

本文介紹遞歸的設計與實現。

小程先介紹遞歸的表現,再介紹怎麽理解遞歸、實現遞歸,最後再舉一些使用遞歸算法的例子。

(一)遞歸的表現

有遞,有歸,才為遞歸。

日常生活中,有很多遞歸或不完整遞歸的表現。比如下面這些:

  1. 和尚講故事

    從前有座山,山裏有座廟,廟裏有個老和尚,老和尚給小和尚講故事,講的是從前有座山。。。。

  2. 沙灘是怎麽形成的

    A:沙堆是怎麽形成的?
    B:沙堆是一個沙堆加上一粒沙。
    A:那這一個沙堆是怎麽形成的?

    B:這一個沙堆是一個沙堆加上一粒沙。
    A:...

  3. 嚇得我
    技術分享圖片

  4. 遞歸函數

    f(x) = g(f(x-1))

  5. 巧妙的衣服
    技術分享圖片

  6. 謠言成真,那還算造謠嗎?

    上海一男子因造謠稱自己因造謠而被拘留15日而被拘留15日。

  7. 洋蔥的構成

    一個洋蔥就是帶著一層洋蔥皮的洋蔥。

  8. 轉發通知
    技術分享圖片

這些例子可以看到遞歸的影子,可以感受到遞歸的一個特點就是“嵌套”,一層套一層,層層新。

(二)理解遞歸

遞歸的表現是調用自己。

之所以能調用自己,是因為子問題也能用原問題的解決辦法。

遞歸的設計,就是把問題分解成更小的問題,而且更小的問題也能用原問題的解決辦法。在問題規模足夠小的時候,把它解決掉,再層層返回,層層組合。

設計遞歸算法,需要能從大到小、從全局到局部地去想問題。

有時,先假設這個算法已經能解決此類問題,再讓同構的問題大膽調用這個算法。

(三)實現遞歸

實現遞歸時,有幾個要點可以考慮。

一是如何調用到自己,也就是如何讓第二層調用到第一層。

二是在什麽時候結束遞歸。

三是每一層遞歸返回時需要處理什麽。

小程覺得,讀者可以在實戰中去參透這幾個要點,或者總結出自己的要點(或技巧)。下面就舉一些例子,並用遞歸的算法來實現。

(四)示例

這裏,小程不考慮性能的問題,只展示怎麽設計遞歸算法。

問題1:輸入數字n,打印出1到n的所有數字。

主體:要打印1到n,那先打印1到n-1,再打印一個n就可以了。這個就是主體,只考慮n跟n-1。

結束:在n為1時打印並結束遞歸。

代碼示例:

void pr(int n) {
    if (n == 1) {
        printf("1\n");
        return;
    }
    pr(n-1);
    printf("%d\n", n);
}

效果:
技術分享圖片

問題2:輸出“我當然知道 我知道 我知道 我知道 ...我是個sb 這件事 這件事...這件事”,輸入n來控制次數。

主體:把“我當然知道”放在遞歸函數外,因為它不符合同構的原則。先輸出“我知道”,再遞歸到下一層即可。
結束:在遞減到0時,結束遞歸。
收尾:在遞歸返回後,輸出“這件事”。

代碼示例:

#include <stdio.h>

void pr(int n) {
    if (n == 0) {
        printf("[我是sb]");
        return;
    }
    printf("我知道[");
    pr(n-1);
    printf("這件事]");
}

int main(int argc, char *argv[])
{
    printf("我當然知道{");
    pr(10);
    printf("}\n");
    return 0;
}

效果:
技術分享圖片

問題3:求二叉樹的高度(最長路徑)。

主體:左子樹與右子樹的高度的最大值,加1就是當前樹的高度。
結束:沒有子樹了。

代碼示例:

int treeHeight(struct TreeNode* root) {
    if (!root) return 0;
    int left = treeHeight(root->left);
    int right = treeHeight(root->right);
    return MAX(left, right) + 1;
}

問題4:求數組中的最大值與最小值。

演示代碼:

#include <iostream>
using namespace std;

template<typename T>
void max_min( T a[], int low, int high, T & max, T & min)
{
    if ( low == high )  // 只有一個元素不再劃分
    {
        max = min = a[low];
        return;
    }
    else if ( low == high -1 )  // 只有兩上元素不再劃分
    {
        if ( a[low] < a[high] )
        {
            max = a[high];
            min = a[low];
        }
        else
        {
            max = a[low];
            min = a[high];
        }
        return;
    }

    int mid = (low + high) / 2;
    T  max_another;
    T  min_another;
    max_min( a, low, mid, max, min );  
    max_min( a, mid+1, high, max_another, min_another );

    if ( max < max_another )
        max = max_another;
    if ( min > min_another )
        min = min_another;
}

int _tmain(int argc, _TCHAR* argv[])
{
    double a[5] = {23.23, 23.45, .3, -89.3, -2.1};
    double max, min;
    max_min<double>( a, 0, 4, max, min );
    cout << "max: " << max << " min: " << min << endl;

    return 1;
}

最後,小程再提一下遞歸的深度。

每次遞歸調用都意味著部分數值壓入棧中(比如系統維護的下壓棧),這是跟叠代的區別。在叠代中每次循環結束時所有局部變量都獲得釋放,而遞歸卻會不斷累計,所以使用遞歸算法必須考慮它的深度,考慮是否會造成棧溢出,與及對效率造成的影響。

另外,每一次遞歸調用,問題的規模都應該有所減少,並最終達到終止條件的要求,從而結束遞歸調用。

至此,遞歸的入門介紹完畢了。


總結一下,本文介紹了遞歸的表現、遞歸的理解與設計,最後舉了幾個例子並用遞歸的思路來實現。遞歸是一個重要的思考問題的思路,希望本文能幫到讀者理解這種思路。

遞歸的入門介紹