遞迴演算法理解

2021-06-26 00:54:51 字數 3308 閱讀 4875

遞迴演算法看起來比較簡單,當總覺得沒能領會到它的精髓,平常也沒可以使用它。今天看到這篇文章,說的比較透徹:

1,遞迴與迴圈之間的關係

看過這樣一道題,問,「程式結構化設計的三種基礎結構,順序、選擇、迴圈是不是必須的?」當然,你知道這樣乙個論斷,只要有這三種就足夠了;但是能不能更少呢?答案是「可以」,原因就是遞迴能取代迴圈的作用,例如下面的對乙個陣列裡面元素求和的函式:

float rsum (float a, const int n)

實際上就是:

sum = 0;

for (int i = 0; i < n; i++) sum += a;

但實際的情況是,任何的一種語言裡面都有迴圈結構,但不是任何的語言都支援遞迴;套用一句話,遞迴不是萬能的,但沒有遞迴不是萬萬不能的。然而,我看到現在的某些人,不管什麼問題都要遞迴,明明迴圈是第乙個想到的方法,偏偏費盡腦筋去尋找遞迴演算法。對此,我真的不知道該說什麼。

------我的理解是,迴圈和遞迴是2種結構,能實現類似的功能,至於哪種效率高,很簡潔易懂,要具體情況具體分析了。

2,什麼時候用遞迴,是必要的? 

從學排列組合那天開始,我所知道的階乘就是這個樣子n! = 1×2×……n。如果讓我來寫階乘的演算法,我也只會想到從1乘到n。再如,斐波那契數列,如果有人用自然語言描述的話,一定是這樣的,開始兩項是0、1,以後的每項都是前面兩項的和。所以讓我寫也只會得到「儲存前兩項,然後相加得到結果」的迭代解法。——現在只要是講到遞迴幾乎就有他們的登場,美其名曰:「定義是遞迴的,所以我們寫遞迴演算法」。我想問的是,定義的遞迴抽象是從**來的?顯然階乘的定義是從乙個迴圈過程抽象來的,斐波那契數列的定義是個迭代的抽象。於是,我們先從乙個本不是遞迴的事實抽象出乙個遞迴的定義,然後我們說,「因為問題的定義是遞迴的,因此我們很容易寫出遞迴演算法」,接著說,「我們也能將這個遞迴演算法轉化為迴圈、迭代演算法」,給人的感覺就像是1÷3=0.33……,0.33……×3=0.99……,然後我們花了好大的心智才明白1=0.99……。

還是有那麼些人樂此不疲,是凡講到遞迴就要提到這兩個,結果,沒有乙個學生看到階乘那樣定義沒有疑問的,沒有乙個對於那個遞迴的階乘函式抱有欽佩之情的——瞎折騰什麼呢?

————我的理解是,如果乙個問題在解決時,被描述/被抽象為 前幾項的什麼什麼時(迭代),自然而然就會想到使用遞迴演算法。

3,有效使用遞迴

我的理解:有些問題用遞迴的方法來解決,非常合適,乙個典型的例子就是漢諾塔。通過下面的使用非遞迴演算法來求解漢諾塔的例子,讓我理解到:遞迴在於在於準確的回歸,並且可以有較大的深度(棧)。而人腦的逆推深度是有限的,而計算機要比人腦深很多。這種思維在某種程度上提高了人解決問題的能力!

「但我堅信,如果乙個問題能用分析的辦法解決——遞迴實際上就是乙個分析解法,能將問題分解成-1規模的同等問題和移動乙個盤子,如果這樣分解下去一定會有解,最後分解到移動1號盤子,問題就解決了——那麼我也應該能用綜合的辦法解決,就是從當前的狀態來確定怎樣移動,而不是逆推得到決定。這是對實際工作過程的乙個模擬,試想如果讓我們去搬盤子,我們肯定不會用遞迴來思考現在應該怎麼搬——只要8個盤子,我們腦子裡的「工作棧」恐怕就要溢位了——我們要立即決定怎麼搬,而不是從多少步之後的情景來知道怎麼搬。下面我們通過模擬人的正向思維來尋找這個解法。

假設如下搬7個盤子的初始狀態(選用7個是因為我曾經寫出了乙個1~6結果正確的演算法,而在7個的時候才發現乙個條件的選擇錯誤,具體大家自己嘗試吧),我們唯一的選擇就是搬動1號盤子,但是我們的問題是向b搬還是向c搬?

顯然,我們必須將7號盤子搬到c,在這之前要把6號搬到b,5號就要搬到c,……以此類推,就會得出結論(規律1):當前柱最上面的盤子的目標柱應該是,從當前柱上「需要搬動的盤子」最下面乙個的目標柱,向上交替交換目標柱到它時的目標柱。就是說,如果當前柱是a,需要移動m個盤子,從上面向下數的第m個盤子的目標柱是c,那麼最上面的盤子的目標柱就是這樣:if (m % 2) 目標和第m個盤子的目標相同(c);else 目標和第m個盤子的目標不同(b)。接下來,我們需要考慮如果發生了阻塞,該怎麼辦,如下所示:

3號盤子的目標柱是c,但是已經有了1號盤子,我們最直覺的反映就是——將礙事的盤子搬到另一根柱子上面去。於是,我們要做的是(規律2):儲存當前柱的資訊(柱子號、應該搬動的最下面一塊盤子的號,和它的目標柱),以備當障礙清除後回到現在的柱子繼續搬,將當前柱轉換為礙事的盤子所在的柱子。假設這樣若干步後,我們將7號盤子從a搬到了c,此時,儲存當前柱號的棧一定是空了,我們該怎麼辦呢?

顯而易見的,轉換當前柱為b,把6號盤子搬到c。由此可得出(規律3):假設當前的問題規模為n,搬動第n個盤子到c後,問題規模減1,當前柱轉換到另乙個柱子,最下面的盤子的目標柱為c。

綜上,我們已經把這個問題解決了,可以看出,關鍵是如何確定當前柱需要移動多少盤子,這個問題請大家自己考慮,給出如下例程,因為沒有經過任何優化,本人的編碼水平又比較低,所以這個函式很慢——比遞迴的還慢10倍。

#include 

#include

using namespace std;

class needle

//每乙個柱子都有乙個底座

void push(int n)

int top()

int pop()

int movenum(int n)

int size()

int operator (int n)

private:

vector a;

};void hanoi(int n)

//障礙盤子搬走後,回到原來的當前柱

if (m % 2) target = target_m; else target = 3 - source - target_m;//規律1的實現

if (needle.top() < needle.top())//當前柱頂端盤子可以搬動時,移動盤子

規律3的實現

}else//規律2的實現}}

這個演算法實現比遞迴演算法複雜了很多(遞迴演算法在網上、書上隨便都可以找到),而且還慢很多,似乎是多餘的,然而,這是有現實意義的。我不知道現在還在搬64個盤子的僧人是怎麼搬的,不過我猜想一定不是先遞迴到1個盤子,然後再搬——等遞迴出來,估計鬍子一打把了(能不能在人世還兩說)。我們一定是馬上決定下一步怎麼搬,就如我上面寫的那樣,這才是人的正常思維,而用遞迴來思考,想出來怎麼搬的時候,黃瓜菜都涼了。正像我們做事的方法,雖然我今生今世完不成這項事業,但我一定要為後人完成我能完成的,而不是在那空想後人應該怎麼完成——如果達不到最終的結果,那也一定保證向正確的方向前進,而不是呆在原地空想。

由此看出,計算機程式設計實際上和正常的做事步驟的差距還是很大的——我們的做事步驟如果直接用計算機來實現的話,其實並不能最優,原因就是,實際中的相關性在計算機中可能並不存在——比如人腦的逆推深度是有限的,而計算機要比人腦深很多,論記憶的準確性,計算機要比人腦強很多。這也導致了乙個普通的程式設計師和乙個資深的程式設計師寫的演算法的速度常常有天壤之別。因為,後者知道計算機喜歡怎麼思考。

A 演算法理解

廣度優先 bfs 和深度優先 dfs 搜尋 深度優先搜尋,用俗話說就是不見棺材不回頭。演算法會朝乙個方向進發,直到遇到邊界或者障礙物,才回溯。一般在實現的時候,我們採用遞迴的方式來進行,也可以採用模擬壓棧的方式來實現。如下圖,s代表起點,e代表終點。我們如果按照右 下 左 上這樣的擴充套件順序的話,...

dijkstra 演算法理解

求有向圖中乙個源點到其他頂點的最短距離 自己的理解 剛開始的時候相當於有三個集合 v 頂點集合 s 已求得的最短距離頂點集合,假設求a到其他頂點的最短距離 s 剩餘頂點集合 d 儲存的是最短距離值 求一維陣列中的最小值的下標,這個陣列不包含已經求得的頂點 找出最小值下標後,放入到s中,然後求剩餘頂點...

KM演算法理解

二分圖帶權匹配與最佳匹配 什麼是二分圖的帶權匹配?二分圖的帶權匹配就是求出乙個匹配集合,使得集合中邊的權值之和最大或最小。而二分圖的最佳匹配則一定為完備匹配,在此基礎上,才要求匹配的邊權值之和最大或最小。二分圖的帶權匹配與最佳匹配不等價,也不互相包含。我們可以使用km演算法實現求二分圖的最佳匹配。方...