漫談遞歸和迭代

先講個故事吧。
算法

從前有座山,山裏有座廟,廟裏有個老和尚,正在給小和尚講故事呢!故事是什麼呢?「從前有座山,山裏有座廟,廟裏有個老和尚,正在給小和尚講故事呢!故事是什麼呢?‘從前有座山,山裏有座廟,廟裏有個老和尚,正在給小和尚講故事呢!故事是什麼呢?……’」。數據庫

這個故事永遠也講不完,由於沒有遞歸結束條件。老師講遞歸時老是說,遞歸很簡單,一個遞歸結束條件,一個本身調用本身。若是遞歸沒有結束條件,那麼就會無限遞歸下去。在編程的時候,沒有遞歸結束條件或者遞歸過深,通常會形成棧溢出。編程

下面這個函數,能夠利用棧溢出來估測棧的大小:ruby

void stack_size()
{
    static int call_time = 0;
    char dummy[1024*1024];
    call_time++;
    printf("call time: %d\n",call_time);
    stack_size();
}

這個函數定義了1M的局部變量,而後調用本身。棧溢出時會崩潰,根據最後打印出的數字能夠算一下棧的大小。app

遞歸算法通常用於解決三類問題:
編程語言

(1)數據的定義是按遞歸定義的。(Fibonacci函數)ide

(2)問題解法按遞歸算法實現。(回溯)wordpress

(3)數據的結構形式是按遞歸定義的。(樹的遍歷,圖的搜索)函數式編程

對於求1+2+3+…+n這種問題,大部分人不會用遞歸方式求解:函數

int sum1(int n)
{
    if(n == 0)
        return 0;
    else
        return n+sum1(n-1);
}

而是使用迭代的方式:

int sum2(int n)
{
    int ret = 0;
    for(int i = 1;  i <= n; i++)
              ret += i;
    return ret;
}


迭代算法是用計算機解決問題的一種基本方法。它利用計算機運算速度快、適合作重複性操做的特色,讓計算機對一組指令(或必定步驟)進行重複執行,在每次執行這組指令(或這些步驟)時,都從變量的原值推出它的一個新值。

爲何使用迭代而不用遞歸呢?

很明顯,使用遞歸時每調用一次,就須要在棧上開闢一塊空間,而使用遞歸就不須要了,所以,不少時候設計出了遞歸算法,還要想法設法修改爲迭代算法。

假如如今咱們不考慮編程,咱們僅僅看一下上面使用遞歸和迭代求1+2+3…+n的過程。

使用遞歸:

sum(5)
5+sum(4)
5+4+sum(3)
5+4+3+sum(2)
5+4+3+2+sum(1)
5+4+3+2+1+sum(0)
5+4+3+2+1+0
5+4+3+2+1
5+4+3+3
5+4+6
5+10
15

使用迭代

0+1=1
1+2=3
3+3=6
6+4=10
10+5=15

上面兩個計算過程所需的步驟都是O(n)。可是兩個計算過程的形狀不同。

遞歸過程是一個先逐步展開然後收縮的形狀,在展開階段,這一計算過程構造起一個推遲進行的操做所造成的的鏈條(這裏是+),在收縮階段纔會實際執行這些操做。這種類型的計算過程由一個推遲執行的運算鏈條刻畫,稱爲一個遞歸計算過程。要執行這種計算過程,就須要維護之後將要執行的操做的軌跡。在計算1+2+3+…+n時,推遲執行的加法鏈條的長度就是爲了保存其軌跡須要保存的信息量,這個長度隨着n值而線性增加,這樣的過程稱爲線性遞歸過程。

迭代過程的造成沒有任何增加或收縮。對於任意一個n,在計算的每一步,咱們須要保存的就只有i,ret,這個過程就是一個迭代計算過程。通常來講,迭代計算過程就是那種其狀態能夠用固定數目的狀態變量描述的結算過程。在計算1+2+…+n時,所需的計算步驟與n成正比,這種過程稱爲線性迭代過程。

如今再回到編程語言中。

上面提到的推遲執行的運算鏈條就存在棧裏,因爲棧很小,若是鏈條太長,就會溢出了。

那咱們再來看下面的函數

int sum3(int n, int acc)
{
    if(n == 0)
        return acc;
    else
        return sum3(n-1,acc+n);
}

調用的時候acc=0,以sum(5,0)爲例這是一個遞歸函數,咱們來看看它的計算過程。

sum(5,0)
sum(4,5)
sum(3,9)
sum(2,12)
sum(1,14)
sum(0,15)
15

這個計算過程是遞歸的仍是迭代的呢?

是迭代的!

可是命名函數sum又調用了本身。

咱們須要將遞歸計算過程與遞歸過程分隔開。

當咱們說一個過程(函數)是遞歸的時候,論述的是一個語法形式上的事實,說明這個過程的定義中(直接或間接的)調用了本身。咱們說一個計算過程具備某種模式時(例如線性遞歸),咱們說的是這一計算過程的進展方式,而不是過程說些上的語法形式。

一個遞歸過程,若是它的計算過程是迭代的,那麼咱們稱這種遞歸爲尾遞歸。尾遞歸不須要保存遞歸的推遲計算鏈,那麼是否是就意味着不會形成棧溢出了?

咱們來試一下

int sum3(int n, int acc)
{
    if(n == 0)
        return acc;
    else
        return sum3(n-1,acc+n);
}
int main()
{
    int n;
    scanf("%d",&n);
    printf("%d\n",sum(n,0));
    return 0;
}


運行結果
2013062001.png

看來仍是會棧溢出。

爲啥呢?由於c語言默認不會對尾遞歸進行優化,即便你的程序是尾遞歸的,它仍是按通常的遞歸進行編譯。加上優化選項就能夠對尾遞歸進行優化。

2013062002-300x130.png

下面哪些是尾遞歸呢?

int fib(int n)
{
    if(n == 0 || n == 1)
        return 1;
    else
        return fib(n-1) + fib(n-2);
}
void qsort(int A, int p, int q)
{
    r = partition(A,p,q);
    qsort(A,p,r-1);
    qsort(A,r+1,q);
}
int gcd(int a, int b)
{
    if(b == 0)
        return a;
    else
        gcd(b, a%b);
}

在函數式編程語言中,不存在變量,所以任何的循環都須要用遞歸實現。若是遞歸使用了尾遞歸,那麼編譯器或解釋器能自動優化,若是不是尾遞歸,那麼就存在棧溢出的風險。前面兩個不是尾遞歸,第三個是尾遞歸。

任何遞歸均可以轉化成迭代,那麼任何遞歸均可以轉化成尾遞歸。

斐波那契數列改爲尾遞歸後以下

int fib(int n,int count, int a , int b)
{
    if(n == 0 || n == 1)
        return 1;
    else if (count > n)
        return b;
    else
        return fib(n,count+1,b,a+b);
}
int FIB(int n)
{
    return fib(n,2,1,1);
}

下面這段代碼

i = 1, ret = 0
for(;i <= n; i++)
        ret += i;

對應的遞歸形式就是

int fun(int i, int ret) {
    if(i > n)
        return ret;
    else
        return fun(ret+i,i+1);
}

fun(1,0)至關於給i和ret賦初值。

若是將快速排序改爲迭代的話,那麼須要一個棧!它的變量個數是有限的嗎?咱們能夠把棧當作一個變量就能夠了。

先修改爲迭代形式

void qsort_iterate(int a[],int p,int q)
{
        stack s;
        s.push(p);
        s.push(q);
        while(!s.empty())
        {
                int high = s.top();
                s.pop();
                int low = s.top();
                s.pop();
                if(high > low)
                {
                        int r = partition(a,low,high);
                        s.push(low);
                        s.push(r-1);
                        s.push(r+1);
                        s.push(high);
                }
        }
}

上面的迭代形式能夠很容易的改爲尾遞歸:

void qsort_tail(int a[],stack s)
{
        if(!s.empty())
        {
                int high = s.top();
                s.pop();
                int low = s.top();
                s.pop();
                if(high > low)
                {
                        int r = partition(a,low,high);
                        s.push(low);
                        s.push(r-1);
                        s.push(r+1);
                        s.push(high);
                }
                qsort_tail(a,s);
        }
}

那麼在函數式編程語言裏,快排是否是就是這樣實現的?答案是No。函數式編程爲何不能用循環?就是由於沒有變量,因此在函數式編程語言裏不能進行原地排序的。

(define (qsort s)
  (cond ((null? s) s)
        ((null? (cdr s)) s)
        (else
         (let ((h (car s))
               (left (filter (lambda (x) (<= x (car s))) (cdr s)))
               (right (filter (lambda (x) (> x (car s))) (cdr s))))
           (append (qsort left) (list h) (qsort right))))))

咱們把這段代碼翻譯成Python(翻譯成C或者C++挺囉嗦的)上面這段代碼是用Lisp的方言Scheme實現的,不是尾遞歸的。

def qsort_lisp(A):
    if len(A) == 0 or len(A) == 1:
        return A
    left = []
    right = []
    pivot = A[0]
    for i in range(1,len(A)):
        if A[i]             left.append(A[i]);
        else:
            right.append(A[i]);
    return qsort_lisp(left) + [pivot] + qsort_lisp(right)
x = [3,4,5,6,2,34,6,2,2,5,7,2,7]
print qsort_lisp(x)

其實剛纔我說謊了,大部分函數式編程語言,例如Scheme,Erlang,Clojure等都提供可變的變量,數據庫裏有上G的數據,不能把它拷貝一份在寫回去,這時候就須要使用真正的變量了。函數式編程語言都是比較高級的語言,排序時通常使用自帶的sort函數就好了。上面這段代碼沒有對變量作修改的操做,因此能夠看作是函數式編程。這個函數能改爲尾遞歸嗎?應該是能夠的,可是挺麻煩的,我是沒找到好辦法。到網上找了找也沒找到好的方法。

總結一下尾遞歸:(1)計算過程是迭代的(2)在函數最後一步調用本身,並且是僅有調用語句,或者是一句fun(),或者是return fun(),不存在x = fun()這樣的狀況(3)函數執行最後一句調用本身的語句時,將狀態變量以參數形式傳遞給下一次調用,本身的棧沒用了,形象的說,它告訴下一次被調用的函數,我已經死了,你幹完活後直接向個人上級報告就好了,不須要和我說了(4)gcc開啓優化選項後能夠對尾遞歸進行優化,大部分函數式編程語言會對尾遞歸進行優化

相關文章
相關標籤/搜索