插曲:關於遞迴

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

1 概述

迴圈與遞迴是演算法中最常見的控制過程的方法,迴圈自不必說,只要學過電腦語言,必然都會講這種控制結構;而對於遞迴,大家也能寫得很漂亮(樹演算法中和圖演算法中使用遞迴的情境也特別多)。

遞迴的好處也顯而易見,代碼體積小,容易維護。然而,遞迴並不是萬能鑰匙,特別是當運行環境記憶體空間有限以及要求高效能的情境。下文首先介紹遞迴的運行原理,然後用執行個體說明遞迴的代價,接著講解遞迴如何轉換成迴圈及其限制,最後對本文進行總結。

2 遞迴的運行原理

這裡的遞迴特指遞迴函式,遞迴函式在程式中執行的原理是什麼(馮諾伊曼體系下)?這是認識遞迴函式執行效率的關鍵。下面我們以構建斐波那契數列為例,說明遞迴函式的執行過程。構建斐波那契數列的遞迴函式(golang實現)如下:
fun Fib(n int) int{ ret : = 0 if n == 0 || n == 1 { // 1 ret = 1 // 2 } ret = Fib(n - 1) + Fib(n - 2) // 3 return ret}
學過編譯原理的童鞋都知道,函數是在棧(對,就是童鞋們在資料結構上學習的棧)上啟動並執行,如果n=4,Fib(4)的原理示意1所示:


圖1:fib(4)動態

要求fib(4),必須在棧上求Fib(3)(要求Fib(3),必須再為在棧上求Fib(2)和Fib(1),要求Fib(2),在棧上繼續求fib(1)和Fib(0))和Fib(2)(類似fib(3)的過程)。這樣的遞迴演算法,必須在棧上記錄函內的局部變數、傳遞參數、返回地址(直到調用結束後回到哪)和上一棧幀的EBP和BP(恢複調用者棧),並且頻繁出棧入棧是需要系統開銷的,雖然單次入棧出棧開銷不大,但是如果要求Fib(1000)這樣的函數,恐怕一般的單機估計得跑幾十分鐘甚至半天了(在筆者的mac本上跑了幾分鐘都沒出來,直接把進程殺了,不能忍)。

為了有個直觀的感受,筆者特意做了一個簡單的實驗(見我的git),分別以遞迴和非遞迴求解Fib(10), Fib(20), Fib(30), Fib(40), Fib(50)的運行結果,如所示:


圖2:遞迴和非遞迴實驗結果


從運行結果可以看出,當n值較小時(<10)時,遞迴啟動並執行時間少於非遞迴已耗用時間(原因應該是非遞迴分配slice需要佔用相對較長的時間,這種寫法有些弱智,其實只需要兩個中間變數即可,類似於不用第三個變數實現兩變數值交換的思路),當n>=20後,非遞迴已耗用時間遠低於遞迴已耗用時間,n越大,非遞迴相對遞迴越高效。

當然,非遞迴高效運行也不是沒有代價的,相比遞迴函式,編寫代碼的難度要更高並且更難維護。

3 遞迴轉非遞迴

那如何將遞迴函式轉換為非遞迴函式呢?是否所有的遞迴函式都能換成非遞迴函式?

首先必須弄清楚遞迴有哪些種類,遞迴有兩種,一種是單向遞迴,類似於Fib(n)的這種是一種典型的單向遞迴(Fib(n)->Fib(n-1)->Fib(n-2)->...->Fib(1));另一種的遞迴(不妨稱其為互動遞迴,不一定準確)的形式為:F1(n) -> F2(n) -> F1(n-1) -> F2(n-1) -> ...

單向遞迴中,以尾遞迴效率最高,尾遞迴是指以調用自身結尾的函數。上文中提到的Fib(n)就不是尾遞迴,但是可以轉換為遞迴函式如下(不防比較一下尾遞迴寫法和非遞迴寫法的效率):
fun TailFib(n, f1, f2 int) int{ if n < 2{ return f1 } return TailFib(n-1, f2, f1 + f2)}
其對應的非遞迴寫法為:
func NotRecursion(n, f1, f2 int) int{ if n < 2{ return 1; } i := 0 for i <= n { f2 = f1 + f2 f1 = f2 - f1 } return f1}
尾遞迴在遞迴函式中的效率最高,因為當遞迴調用返回時,返回到上一層遞迴調用語句的下一語句,而這個位置正好是程式的結尾,因此遞迴工作棧中可以不儲存返回地址;除了傳回值和引用值外,其他參數和局部變數都不再需要。根據尾遞迴的邏輯,很容易寫出非遞迴的演算法。

對於互動遞迴,其形式相對較為複雜,可能在作具體項目時,無意中就寫出來了。互動遞迴相對單向遞迴而言,難以直接寫出相應的非遞迴函式,需要抽取出F1(n) -> F2(n) -> F1(n-1) -> F2(n-1) -> ...裡面的F1(i)->F2(i)->F1(i-1)->F2(i-1)這裡面的公用邏輯,即用一個迭代可以表述其中的轉換。如果難以抽取,最直接的辦法是類比入棧出棧過程,抽取裡面的參數特徵,建立迴圈迭代(後續想到了補一個例子)。

4 總結

遞迴是演算法中特別是樹、圖等結構中常用的方法,理解遞迴的運行原理是寫出高效代碼的關鍵。針對效率要求比較高的情境,可以嘗試寫出非遞迴的版本。總體上而言,遞迴寫法的代碼體積小、易維護,非遞迴寫法代碼理解維護稍顯困難,但執行效率相對較高。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.