遞歸適用的范疇:
既然的遞歸的思想是把問題分解成規模更小但和原問題有着相同解法的問題,那是不是所有具有這樣特性的問題都能用遞歸來解決呢?答案是否定的。除了這個特性,能用遞歸解決的問題還必須具有一個特性:存在一種簡單情境,能讓遞歸在簡單情境下退出,也就是要有一個遞歸出口。總結一下就是,能用遞歸解決的問題,必須滿足以下兩個條件:
- 一個問題能夠分解成規模更小,且與原問題有着相同解的問題;
- 存在一個能讓遞歸調用退出的簡單出口
遞歸導致一個函數反復調用自己,我們知道函數調用是通過一個工作棧來實現的,在大多數機器上,每次調用函數時大致要做三個工作:調用前先保存寄存器,並在返回時恢復;復制實參;程序必須轉向一個新位置執行。其中,具體要保存的內容包括:局部變量、形參、調用函數地址、返回值。那么,如果遞歸調用N次,就要分配N*局部變量、N*形參、N*調用函數地址、N*返回值。這勢必是影響效率的。在C++中,inline函數就是為了改善函數調用所帶來的效率問題而做的一種優化。遞歸就是利用系統的堆棧保存函數當中的局部變量來解決問題的,說白了就是利用堆棧上的一堆指針指向內存中的對象,並且這些對象一直不被釋放,直到遇到簡單情境時才一一出棧釋放,所以總的開銷就很大。棧空間都是有限的,如果沒有設置好出口,或者調用層級太多,有可能導致棧空間不夠,而出現棧溢出的問題。為了防止無窮遞歸現象,有些語言是規定棧的長度的,比如python語言規定堆棧的長度不能超過1000。還有就是當規模很大的時候,盡量不使用遞歸,而改為非遞歸的形式,或者優化成尾遞歸的形式(后面講)。
尾遞歸:遞歸轉尾遞歸
有些簡單的遞歸問題,可以不借助堆棧結構而改成循環的非遞歸問題。這里說的簡單,是指可以通過一個簡單的數學公式來進行推導,如階乘問題和斐波那契數列數列問題。這些可以轉換成循環結構的遞歸問題,一般都可以優化成尾遞歸的形式。很多編譯器都能夠將尾遞歸的形式優化成循環的形式。那什么是尾遞歸呢?
我們先討論一個概念:尾調用。顧名思義,一個函數的調用返回都集中在尾部,單個函數調用就是最簡單的尾調用。如果兩個函數調用:函數A調用函數B,當函數B返回時,函數A也返回了。同理,多個函數也是同樣的情況。這就相當於執行完函數B后,函數A也執行完了,從數據結構上看,在執行函數B時,函數A的堆棧已經大部分被函數B修改或替換了,所以,棧空間沒有遞增或者說遞增的程度沒有普通遞歸那么大。這樣在效率上就大大降低了。
遞歸轉非遞歸
不可否認,遞歸便於算法的理解,代碼精煉,容易閱讀,但遞歸的效率往往是我們最在意的問題。如果能用循環解決遞歸問題,就盡可能使用循環;如果用循環解決不了,或者能解決但代碼很冗長且晦澀,則盡可能使用遞歸。另外,有些低級語言(如匯編)一般不支持遞歸。很多時候我們需要把遞歸轉化成非遞歸形式,這不僅能讓我們加深對遞歸的理解,而且能提升問題解決的效率。這時候就需要掌握一些轉化的技巧,便於我們在用到時信手捏來。
一般來說,遞歸轉化為非遞歸有兩種情況:
第一種方法:借助堆棧模擬遞歸的執行過程。這種方法幾乎是通用的方法,因為遞歸本身就是通過堆棧實現的,我們只要把遞歸函數調用的局部變量和相應的狀態放入到一個棧結構中,在函數調用和返回時做好push和pop操作,就可以了(后面有一個模擬快排的例子)。
第二種方法:借助堆棧的循環結構算法。這種方法常常適用於某些局部變量有依賴關系,且需要重復執行的場景,例如二叉樹的遍歷算法,就采用的這種方法。
更一般的遞歸,想要轉化為非遞歸,就需要模擬棧的行為。
首先需要自己建個棧。棧保存的東西是一個記錄,包括所有局部變量的值,執行到的代碼位置。
首先講局部變量初始化位一開始的狀態,然后進入一個循環
執行代碼時,遇到遞歸,就制作狀態壓棧保存,然后更新局部變量進入下一層。
如果一個調用結束了,就要返回上層狀態。直接講棧里的記錄彈出,拿來更新當前狀態即可。
某個調用結束時如果棧為空則所有調用都結束,退出主循環。
下面的代碼給出中序遍歷的非遞歸實現:
#include <stack> using namespace std; typedef struct node { node* left; node* right; int x; }; struct record{ node* a; int state; record(node* a, int state) :a(a), state(state){} }; //中序遍歷的非遞歸實現 void non_recursive_inorder(node* root){ stack<record> s; node* cur = root; //初始化狀態 int state = 0; while (1){ if (!cur){ //如果遇到null結點,返回上一層 if (cur == root)break;//如果沒有上一層,退出循環 cur = s.top().a; state = s.top().state; //返回上層狀態 s.pop(); } else if (state == 0){ //狀態位0,執行第一個遞歸inorder(cur->left); s.push(record(cur, 1));//保存本層狀態 cur = cur->left; //更新到下層狀態 state = 0; } else if (state == 1){ //狀態為1,執行print和inorder(cur->right) printf("%d ", cur->x); s.push(record(cur, 2)); //保存本層狀態 cur = cur->right; //進入下層狀態 state = 0; } else if (state == 2){ //狀態2,函數結束,返回上層狀態 if (cur == root)break; //初始結點的退出狀態,遍歷結束 cur = s.top().a; //返回上層狀態 state = s.top().state; s.pop(); } } putchar(10); }
最后,通過一個用堆棧模擬快排的例子來結束本文。通過一個結構體record來記錄函數的局部變量和相應的狀態。