遞歸---Recursion
在學習清華大學鄧俊輝鄧公的數據結構這門課中,鄧公引用了這樣一句話:
To iterate is human, to recurse, divine. (迭代乃人工,遞歸方神通。)
足見遞歸算法的重要性。
什么是遞歸?
程序調用自身的方式叫做遞歸,這里直接傳送百度百科:遞歸。
遞歸基(Recursion-Base)
遞歸一般會有邊界條件,也稱遞歸基。一般是平凡問題,即能直接求解或給出答案的問題。
遞歸思路
遞歸算法的思路就是不斷將復雜問題,或分而治之(Divide-And-Conquer),或減而治之(Decrease-And-Conquer),這是一個從上而下的過程,直到觸碰到遞歸基后,再一步步自下而上,不斷將子問題的答案合並,最終形成最終答案。
分而治之(Divide-And-Conquer)
分:即將一個復雜問題根據某個依據,分為兩個同等規模(不一定完全相同)的兩個子問題,然后不斷分解下去,直到某個子問題退化為一個平凡問題(規模為0或1,或該子問題已有解等情況)。
治:代將平凡問題結果返回,兩個平凡問題答案合並為一個子問題答案,並不斷返回、合並,最后得到問題解。
應用:如著名的快速排序(Quick-Sort),即將規模為n的序列排序先分解為n/2的兩個子問題,再逐自分解,最后再合並。
減而治之(Decrease-And-Conquer)
與“分”不同,“減”是將當前復雜問題划分為:一個平凡問題,一個規模縮減的子問題。子問題會不斷縮減,直到退化為平凡問題,再逐步合並,得到答案。
遞歸算法復雜度分析
遞歸算法的復雜度分析也是一個重點、難點。這里介紹兩種常用方法:
- 遞歸跟蹤分析:檢查每個遞歸實例,累計所需時間,調用語句本身計入對應子實例,其總和即算法執行時間;
- 遞歸方程:通過寫出遞推方程,逐步推出復雜度。
這里需要說明的是:
1,分析遞歸算法復雜度是個難點,需要一定的數學知識,通過不斷練習、分析會逐步提高這方面能力;
2,我們要學會“封底估算”,這是一大技巧,重點在於我們要利用數學而不能依賴於數學,原因是很多時間我們並不需要得到一個很精確的數字來表明復雜度是多少,我們只要能大抵推算出與實際答案同一個數量級的答案即可,比如1000000與1,我們認為是相同的,因為都是常量。大多時候我們甚至能通過直覺、經驗直接得出一個與精確答案相差無幾的答案,而這種估算我們隨便找張紙都能算出來。
遞歸算法好嗎?
沒有東西在任何時候都是最完美的,只有最合適的。遞歸算法也是這樣。
不得不承認,遞歸算法很重要!很多復雜問題能夠通過遞歸算法很簡明的描述出來,而且應該很廣泛,樹、圖等都有應用。
但遞歸算法對計算機棧的占用很大,每次調用一次自身,系統棧就得將自身函數的一些環境壓入,直到算出答案才能彈出,有時候這是不能接受的。尤其是尾遞歸(在函數尾部返回),現在很多編譯器會自動將尾遞歸改寫。
而且遞歸並沒有加快運行效率,它縮短了找到問題之前需要走步數,從而減少了運行時間,但這個前提是要合並編寫這個遞歸函數,不然有時候子問題的重復計算會使運行時間更長,比如下面應用中要講到的Fibonacci數。
遞歸應用
Fibonacci數
求進制數
參數:1,要轉換的十進制數;2,保存結果的字符數組;3,要轉換的進制。
返回值:字符數組有效字符個數。
int dectobase(int num, char* conversion, int base){ if( num > 0 ){ int cnt = dectobase( num/base, conversion, base) + 1; *(conversion + cnt - 1) = num%base + '0'; return cnt; } return 0; }
HanoiTower
1 void hanota(int N, char s, char t, char g){ 2 if( N > 0 ){ 3 hanota( N-1, s, g, t); 4 printf("%c->%c\n", s, g); 5 hanota( N-1, t, s, g); 6 } 7 }
排列組合
尋找中位數
兩個有序序列的中位數
最長公共子序列
八皇后問題