1. 程式人生 > >遞迴與試探回溯(1) 簡單遞迴分析

遞迴與試探回溯(1) 簡單遞迴分析

遞迴分析往往是新手在學習資料結構是難點,見到遞迴就頭大。網上看了諸多總結,千變萬化,其實都不如自己從頭到尾分析一下。在這一個章節,我就先不對遞迴的漸進複雜度空間複雜度做分析,因為我實在沒有完全搞懂。

其實遞迴呼叫(recursive call) 就是某一方法的自身呼叫。它的價值在於,許多問題都可以簡介而準確地描述為遞迴形式。而且比較重要的一點是,如果遞迴沒有搞清楚,後面關於資料結構和演算法的試探回溯,樹和圖論部分的廣度優先搜尋與深度優先都沒有辦法很好的理解,所以我選擇先徹底弄懂遞迴方法。

線性遞迴
先看看最簡單的線性遞迴,下面先看一個數組求和的程式碼例子:

int sum(int A[],int
n){ if(n>1) return 0; else return (A,n-1)+A[n-1]; }

在程式碼中可以看出,我們首先要判斷並處理n=0之類的平凡情況,以免因無限遞迴而導致系統溢位。這類平凡情況統稱為“遞迴基”(base case of recursion)。線性遞迴的模式,往往對應於所謂減而治之(decrease-and-conquer)的演算法策略:遞迴每深入一層,待求解問題的規模都縮減一個常數,直至最終蛻化為平凡的小(簡單)問題。

下面我們介紹一種直觀且可視的方法:遞迴跟蹤(recursion trace),它可以用來分析遞迴演算法的總體執行時間與空間,下面介紹一下步驟:
1.演算法的每一遞迴例項都表示為一個方框,其中註明了該例項呼叫的引數;
2.若例項M呼叫N,則在M與N對應的方框之間新增一條有向聯線

這裡寫圖片描述

多基遞迴
在這個例子程式中,我們解決了一個數組倒置的問題:

A[]={1,2,3,4,5,6,7,8}
void reverse(int* A,int lo,int hi){
     if(lo<hi){
        swap(A[lo],A[hi]);
        reverse(A,lo+1,hi+1);
     } 
} 

在這裡例子裡面,多基遞迴也是線性遞迴。在這裡也說明一下尾遞迴,尾遞迴就是:狹義的定義裡面,若遞迴呼叫在遞迴例項中恰好以最後一步操作的形式出現,則稱作尾遞迴(tail recursion)。在這個例子裡面,其最後一步操作是對去除了首,尾元素之後總長縮減兩個單元的子陣列進行遞迴倒置,即屬於典型的尾遞迴。

二分遞迴
二分遞迴採用了分而治之(divide-and-conquer)的策略,與減而治之的策略類似。每一處遞迴例項都可能做多次遞迴,故也稱作“多路遞迴”(multi-way-recursion)。我們通常將問題一分為二,故也稱“二分遞迴(binary recursion)”。但是需要強調的是,無論是分解為兩個還是更大常數個子問題,對演算法總體的漸進複雜度並無實質影響。

int sum(int A[],int lo,int hi){
    if(lo==hi)
       return A[lo]; //直接返回該元素
    else{
       int mi=(lo+hi)>>i;
       return sum(A,lo,hi)+sum(A,mi+1,hi);
    }   
}

這裡寫圖片描述