如何理解遞迴(一)

2021-08-29 01:39:15 字數 2797 閱讀 6445

遞迴是程式執行時的一種現象,也是解決某些特定問題時較迭代演算法來說更自然更優雅的**組織方式。作為程式設計師工作了多年後,我發現乙個能不能理解好遞迴,能不能用遞迴來解決問題是區分程式設計師和非程式設計師,甚至於區分好程式設計師和差程式設計師的試金石。很多人通過學習掌握了某些語言的語法,也能寫一些**,但是一遇遞迴就頭大。我曾經也是經歷過這樣的一段時間,所以希望這篇文多少能對這樣的人有點幫助。

談到遞迴,程式設計課程老師可能會說遞迴就是「函式自己呼叫自己」。比如下面的**段:

void

foo(

)

我們知道這個函式要是執行起來,除了讓你的程式報出乙個「堆疊溢位」的錯誤外,其他什麼作用也沒有。在某種程度上說,我們的大腦就是乙個計算機。當我們嘗試去理解「自己呼叫自己」這句話時,大腦也會陷入乙個無限的遞迴過程裡,然後「轟」的一聲「堆疊溢位」了,所以也就無法去理解了。當然老師還會告訴你遞迴除了「自己呼叫自己」外,還有很重要的一部分就是在滿足條件的時候函式會返回,這樣就避免出現無限遞迴的過程了。所以乙個完整的遞迴函式組成如下:

void

foo(

)// 遞迴部分

foo();

}

unsigned

intfibo

(unsigned

int n)

// 遞迴部分

return

fibo

(n -1)

+fibo

(n -2)

;}

到這裡老師大概就會來個總結陳詞,你看這就是遞迴的用處了,它可以很優雅的解決這樣的問題。但是有多少人是真的理解的呢?反正我自己在學的時候,完全是懵掉的,而且一直持續到工作很多年。有一段時間我甚至覺得遞迴只是用來計算斐波那契數列的……

要理解遞迴,我們要退一步,了解另乙個概念——「分而治之」。稍微了解程式設計的人,對這個概念應該不陌生。通俗的說,假如我們有這樣乙個問題a,如果能把a分解成一系列比a更容易解決的子問題(a0,a1,a2……an),通過解決子問題(a0,a1,a2……an)來最終達成解決問題a,這個就是「分而治之」。從本質上來說遞迴就是「分而治之」概念的乙個應用。以遞迴計算斐波納切數列來舉例,要計算斐波納切數列的第n項該怎麼辦?通過數列的定義我們知道第n項的值等於第n-1項加上第n-2項,所以我們可以把計算第n項這個問題分解成計算n-1項和n-2項兩個子問題。我們知道計算n-1和n-2項要比計算n項更容易點(因為n-1和n-2都比n要來得小)。那麼n-1項由如何計算呢?根據定義n-1項等於(n-1)- 1項和(n-1)-2項的值,好了,我們在這裡碰到了遞迴,好,我們先就此打住。因為n在一直減少,最終會減到1,再減到0,而第0項和第1項的值我們不用計算就知道的,這就是遞迴終結的時候了。

通用概括一下,在面對遞迴問題時,我們可以用「分而治之」的概念去幫助理解。步驟如下:

把問題分解成更容易解決的子問題集合,比如可以把計算斐波那契數列的第n項問題分解轉換成計算第n-1項加上第n-2項這兩個子問題

假設我們有乙個函式可以應用在所有的子問題上,比如計算斐波那契數列的fibo函式

基於步驟2的函式,實現如何把子問題的解拼成最終問題的解,這就是遞迴部分,在計算斐波那契數列的例子裡就是fibo(n-1) + fibo(n-2)部分

遞迴部分確定了,然後再考慮子問題最終簡化到到最底層時該返回什麼值。

上面4步都做好了之後,剩下的就只是毫無條件的相信計算機了……

根據上面的步驟我們來嘗試一下解決下面這個很常見的面試題目:「實現乙個函式翻轉給定的字串」。假設我們要翻轉的字串是「abcdef」,那麼翻轉之後的結果應該是「fedcba」。毫無疑問這個字串我們是沒辦法一下子就翻轉過來的,那麼「分而治之」吧。我們不能把多個字元一下子就完全翻轉過來,但是假如字串裡的字元只有乙個呢?我們翻轉起來最方便了,因為什麼都不用做。好了,我們可以把「abcdef」分解成「a」和「bcdef」兩個子字串。「bcdef」比「abcdef」短乙個字元,理論上也稍微容易點。假設我們有這樣的函式能接受乙個字串,然後像變戲法一樣就能返回乙個翻轉後的字串,如下:

string revertstring

(const string& str)

那麼有了這個函式之後,我們怎麼應用這個函式把「a」和「bcdef」組合成最後的結果呢?很簡單我們只要把「a」加到「bcdef」的翻轉字串後面去就可以了,如下:

string revertstring

(const string& str)

當然現在這個函式還不能直接交給計算機去執行,因為我們傳給revertstring的引數字串在不斷的縮短,當縮短到乙個字元或者乙個字元也沒有時,我們就沒辦法再繼續縮短了,我們要把這些情況也處理掉才行:

string revertstring

(const string& str)

// str = 'abcdef'

// str[0] = 'a'

// str.substr(1) = 'bcdef'

return

revertstring

(str.

substr(1

))+ str[0]

;}

好了,這樣的乙個函式就可以交給計算機去執行了,然後剩下的事情就是「見證奇蹟」的時刻了。以上的思維過程可以應用於絕大多數遞迴演算法上。作為練習,讀者朋友可以嘗試用遞迴來解決另乙個常見的演算法面試問題:如何判斷乙個字串是否是回文。友情提示,可以把問題分解成判斷頭尾兩個字元和除了頭尾之外的子字串都是否符合回文條件。

用遞迴來解決問題,在**組織上毫無疑問是很優雅的,但是在時間和空間的複雜度上往往不是最優的,如何解決這個問題,我將在接下來的一篇文裡跟大家聊一聊。

如何理解遞迴?

理解遞迴的真正含義 當執行到調運自身時先停止執行後邊的 重新呼叫自身當所有呼叫自身的 運 行完成後再重新按棧的形式重新由後向前一次呼叫自身之後的 特點 遞迴呼叫很容易導致占用大量的空間,遞迴的效率不高 下列 中通過輸出n的值理解遞迴呼叫其實就是堆疊的運用使用遞迴很容易造成堆疊溢位 package a...

如何去理解遞迴,想到遞迴,運用遞迴

舉例子理解 int strlen const char s 比如 s abcdef 逆序後變為 fedcba 先找遞推關係 如果想逆序abcdef 步驟1 將a和f進行交換 步驟2 對bcde進行逆序 需要同樣的功能,所以必須用遞迴 這就是遞推關係,逆序 abcdef 需要逆序 bcde 進一步可知...

演算法 如何理解遞迴,寫好遞迴函式

不是每個程式設計師天生對遞迴理解深刻,剛入大一時候,當別人寫出第乙個求最大公約數的遞迴函式時,對其多麼的驚嘆,竟然可以不用迴圈,竟然 可以這麼簡潔,確實遞迴在大多數情況下實現時候 很短,大部分人也知道遞迴,也能基本看懂遞迴,可是卻經常不知道怎麼寫,或者寫出來的遞迴經常死迴圈,寫演算法往往也是學的是套...