在线观看www成人影院-在线观看www日本免费网站-在线观看www视频-在线观看操-欧美18在线-欧美1级

0
  • 聊天消息
  • 系統消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發帖/加入社區
會員中心
創作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

遞歸的三大要素!有關遞歸的一些優化思路

算法與數據結構 ? 來源:lp ? 2019-04-03 14:11 ? 次閱讀

可能很多人在大一的時候,就已經接觸了遞歸了,不過,我敢保證很多人初學者剛開始接觸遞歸的時候,是一臉懵逼的,我當初也是,給我的感覺就是,遞歸太神奇了!

可能也有一大部分人知道遞歸,也能看的懂遞歸,但在實際做題過程中,卻不知道怎么使用,有時候還容易被遞歸給搞暈。也有好幾個人來問我有沒有快速掌握遞歸的捷徑啊。說實話,哪來那么多捷徑啊,不過,我還是想寫一篇文章,談談我的一些經驗,或許,能夠給你帶來一些幫助。

為了兼顧初學者,我會從最簡單的題講起!

遞歸的三大要素

第一要素:明確你這個函數想要干什么

對于遞歸,我覺得很重要的一個事就是,這個函數的功能是什么,他要完成什么樣的一件事,而這個,是完全由你自己來定義的。也就是說,我們先不管函數里面的代碼什么,而是要先明白,你這個函數是要用來干什么。

例如,我定義了一個函數

1//算n的階乘(假設n不為0)2intf(intn){34}

這個函數的功能是算 n 的階乘。好了,我們已經定義了一個函數,并且定義了它的功能是什么,接下來我們看第二要素。

第二要素:尋找遞歸結束條件

所謂遞歸,就是會在函數內部代碼中,調用這個函數本身,所以,我們必須要找出遞歸的結束條件,不然的話,會一直調用自己,進入無底洞。也就是說,我們需要找出當參數為啥時,遞歸結束,之后直接把結果返回,請注意,這個時候我們必須能根據這個參數的值,能夠直接知道函數的結果是什么。

例如,上面那個例子,當 n = 1 時,那你應該能夠直接知道 f(n) 是啥吧?此時,f(1) = 1。完善我們函數內部的代碼,把第二要素加進代碼里面,如下

1//算n的階乘(假設n不為0)2intf(intn){3if(n==1){4return1;5}6}

有人可能會說,當 n = 2 時,那我們可以直接知道 f(n) 等于多少啊,那我可以把 n = 2 作為遞歸的結束條件嗎?

當然可以,只要你覺得參數是什么時,你能夠直接知道函數的結果,那么你就可以把這個參數作為結束的條件,所以下面這段代碼也是可以的。

1//算n的階乘(假設n>=2)2intf(intn){3if(n==2){4return2;5}6}

注意我代碼里面寫的注釋,假設 n >= 2,因為如果 n = 1時,會被漏掉,當 n <= 2時,f(n) = n,所以為了更加嚴謹,我們可以寫成這樣:

1//算n的階乘(假設n不為0)2intf(intn){3if(n<=?2){4????????return?n;5????}6}

第三要素:找出函數的等價關系式

第三要素就是,我們要不斷縮小參數的范圍,縮小之后,我們可以通過一些輔助的變量或者操作,使原函數的結果不變。

例如,f(n) 這個范圍比較大,我們可以讓 f(n) = n * f(n-1)。這樣,范圍就由 n 變成了 n-1 了,范圍變小了,并且為了原函數f(n) 不變,我們需要讓 f(n-1) 乘以 n。

說白了,就是要找到原函數的一個等價關系式,f(n) 的等價關系式為 n * f(n-1),即

f(n) = n * f(n-1)。

這個等價關系式的尋找,可以說是最難的一步了,如果你不大懂也沒關系,因為你不是天才,你還需要多接觸幾道題,我會在接下來的文章中,找 10 道遞歸題,讓你慢慢熟悉起來。

找出了這個等價,繼續完善我們的代碼,我們把這個等價式寫進函數里。如下:

1//算n的階乘(假設n不為0)2intf(intn){3if(n<=?2){4????????return?n;5????}6????//?把?f(n)?的等價操作寫進去7????return?f(n-1)?*?n;8}

至此,遞歸三要素已經都寫進代碼里了,所以這個 f(n) 功能的內部代碼我們已經寫好了。

這就是遞歸最重要的三要素,每次做遞歸的時候,你就強迫自己試著去尋找這三個要素。

還是不懂?沒關系,我再按照這個模式講一些題。

有些有點小基礎的可能覺得我寫的太簡單了,沒耐心看?少俠,請繼續看,我下面還會講如何優化遞歸。當然,大佬請隨意,可以直接拉動最下面留言給我一些建議,萬分感謝!

案例1:斐波那契數列

斐波那契數列的是這樣一個數列:1、1、2、3、5、8、13、21、34….,即第一項 f(1) = 1,第二項 f(2) = 1…..,第 n 項目為 f(n) = f(n-1) + f(n-2)。求第 n 項的值是多少。

1、第一遞歸函數功能

假設 f(n) 的功能是求第 n 項的值,代碼如下:

1intf(intn){23}

2、找出遞歸結束的條件

顯然,當 n = 1 或者 n = 2 ,我們可以輕易著知道結果 f(1) = f(2) = 1。所以遞歸結束條件可以為 n <= 2。代碼如下:

1intf(intn){2if(n<=?2){3????????return?1;4????}5}

第三要素:找出函數的等價關系式

題目已經把等價關系式給我們了,所以我們很容易就能夠知道 f(n) = f(n-1) + f(n-2)。我說過,等價關系式是最難找的一個,而這個題目卻把關系式給我們了,這也太容易,好吧,我這是為了兼顧幾乎零基礎的讀者。

所以最終代碼如下:

1intf(intn){2//1.先寫遞歸結束條件3if(n<=?2){4????????return?1;5????}6????//?2.接著寫等價關系式7????return?f(n-1)?+?f(n?-?2);8}

搞定,是不是很簡單?

零基礎的可能還是不大懂,沒關系,之后慢慢按照這個模式練習!好吧,有大佬可能在吐槽太簡單了。

案例2:小青蛙跳臺階

一只青蛙一次可以跳上1級臺階,也可以跳上2級。求該青蛙跳上一個n級的臺階總共有多少種跳法。

1、第一遞歸函數功能

假設 f(n) 的功能是求青蛙跳上一個n級的臺階總共有多少種跳法,代碼如下:

1intf(intn){23}

2、找出遞歸結束的條件

我說了,求遞歸結束的條件,你直接把 n 壓縮到很小很小就行了,因為 n 越小,我們就越容易直觀著算出 f(n) 的多少,所以當 n = 1時,你知道 f(1) 為多少吧?夠直觀吧?即 f(1) = 1。代碼如下:

1intf(intn){2if(n==1){3return1;4}5}

第三要素:找出函數的等價關系式

每次跳的時候,小青蛙可以跳一個臺階,也可以跳兩個臺階,也就是說,每次跳的時候,小青蛙有兩種跳法。

第一種跳法:第一次我跳了一個臺階,那么還剩下n-1個臺階還沒跳,剩下的n-1個臺階的跳法有f(n-1)種。

第二種跳法:第一次跳了兩個臺階,那么還剩下n-2個臺階還沒,剩下的n-2個臺階的跳法有f(n-2)種。

所以,小青蛙的全部跳法就是這兩種跳法之和了,即 f(n) = f(n-1) + f(n-2)。至此,等價關系式就求出來了。于是寫出代碼:

1intf(intn){2if(n==1){3return1;4}5ruturnf(n-1)+f(n-2);6}

大家覺得上面的代碼對不對?

答是不大對,當 n = 2 時,顯然會有 f(2) = f(1) + f(0)。我們知道,f(0) = 0,按道理是遞歸結束,不用繼續往下調用的,但我們上面的代碼邏輯中,會繼續調用 f(0) = f(-1) + f(-2)。這會導致無限調用,進入死循環。

這也是我要和你們說的,關于遞歸結束條件是否夠嚴謹問題,有很多人在使用遞歸的時候,由于結束條件不夠嚴謹,導致出現死循環。也就是說,當我們在第二步找出了一個遞歸結束條件的時候,可以把結束條件寫進代碼,然后進行第三步,但是請注意,當我們第三步找出等價函數之后,還得再返回去第二步,根據第三步函數的調用關系,會不會出現一些漏掉的結束條件。就像上面,f(n-2)這個函數的調用,有可能出現 f(0) 的情況,導致死循環,所以我們把它補上。代碼如下:

1intf(intn){2//f(0)=0,f(1)=1,等價于n<=1時,f(n)?=?n。3????if(n?<=?1){4????????return?n;5????}6????ruturn?f(n-1)?+?f(n-2);7}

有人可能會說,我不知道我的結束條件有沒有漏掉怎么辦?別怕,多練幾道就知道怎么辦了。

看到這里有人可能要吐槽了,這兩道題也太容易了吧??能不能被這么敷衍。少俠,別走啊,下面出道難一點的。

下面其實也不難了,就比上面的題目難一點點而已,特別是第三步等價的尋找。

案例3:反轉單鏈表。

反轉單鏈表。例如鏈表為:1->2->3->4。反轉后為 4->3->2->1

鏈表的節點定義如下:

1classNode{2intdate;3Nodenext;4}

雖然是 Java語言,但就算你沒學過 Java,我覺得也是影響不大,能看懂。

還是老套路,三要素一步一步來。

1、定義遞歸函數功能

假設函數 reverseList(head) 的功能是反轉但鏈表,其中 head 表示鏈表的頭節點。代碼如下:

1NodereverseList(Nodehead){23}

2. 尋找結束條件

當鏈表只有一個節點,或者如果是空表的話,你應該知道結果吧?直接啥也不用干,直接把 head 返回唄。代碼如下:

1NodereverseList(Nodehead){2if(head==null||head.next==null){3returnhead;4}5}

3. 尋找等價關系

這個的等價關系不像 n 是個數值那樣,比較容易尋找。但是我告訴你,它的等價條件中,一定是范圍不斷在縮小,對于鏈表來說,就是鏈表的節點個數不斷在變小,所以,如果你實在找不出,你就先對 reverseList(head.next) 遞歸走一遍,看看結果是咋樣的。例如鏈表節點如下

我們就縮小范圍,先對 2->3->4遞歸下試試,即代碼如下

1NodereverseList(Nodehead){2if(head==null||head.next==null){3returnhead;4}5//我們先把遞歸的結果保存起來,先不返回,因為我們還不清楚這樣遞歸是對還是錯。,6NodenewList=reverseList(head.next);7}

我們在第一步的時候,就已經定義了 reverseLis t函數的功能可以把一個單鏈表反轉,所以,我們對 2->3->4反轉之后的結果應該是這樣:

我們把 2->3->4 遞歸成 4->3->2。不過,1 這個節點我們并沒有去碰它,所以 1 的 next 節點仍然是連接這 2。

接下來呢?該怎么辦?

其實,接下來就簡單了,我們接下來只需要把節點 2 的 next 指向 1,然后把 1 的 next 指向 null,不就行了?,即通過改變 newList 鏈表之后的結果如下:

也就是說,reverseList(head) 等價于 ** reverseList(head.next)** +改變一下1,2兩個節點的指向。好了,等價關系找出來了,代碼如下(有詳細的解釋):

1//用遞歸的方法反轉鏈表 2publicstaticNodereverseList2(Nodehead){ 3//1.遞歸結束條件 4if(head==null||head.next==null){ 5returnhead; 6} 7//遞歸反轉子鏈表 8NodenewList=reverseList2(head.next); 9//改變1,2節點的指向。10//通過head.next獲取節點211Nodet1=head.next;12//讓2的next指向213t1.next=head;14//1的next指向null.15head.next=null;16//把調整之后的鏈表返回。17returnnewList;18}

這道題的第三步看的很懵?正常,因為你做的太少了,可能沒有想到還可以這樣,多練幾道就可以了。但是,我希望通過這三道題,給了你以后用遞歸做題時的一些思路,你以后做題可以按照我這個模式去想。通過一篇文章是不可能掌握遞歸的,還得多練,我相信,只要你認真看我的這篇文章,多看幾次,一定能找到一些思路!!

我已經強調了好多次,多練幾道了,所以呢,后面我也會找大概 10 道遞歸的練習題供大家學習,不過,我找的可能會有一定的難度。不會像今天這樣,比較簡單,所以呢,初學者還得自己多去找題練練,相信我,掌握了遞歸,你的思維抽象能力會更強!

接下來我講講有關遞歸的一些優化。

有關遞歸的一些優化思路

1. 考慮是否重復計算

告訴你吧,如果你使用遞歸的時候不進行優化,是有非常非常非常多的子問題被重復計算的。

啥是子問題? f(n-1),f(n-2)….就是 f(n) 的子問題了。

例如對于案例2那道題,f(n) = f(n-1) + f(n-2)。遞歸調用的狀態圖如下:

看到沒有,遞歸計算的時候,重復計算了兩次 f(5),五次 f(4)。。。。這是非常恐怖的,n 越大,重復計算的就越多,所以我們必須進行優化。

如何優化?一般我們可以把我們計算的結果保證起來,例如把 f(4) 的計算結果保證起來,當再次要計算 f(4) 的時候,我們先判斷一下,之前是否計算過,如果計算過,直接把 f(4) 的結果取出來就可以了,沒有計算過的話,再遞歸計算。

用什么保存呢?可以用數組或者 HashMap 保存,我們用數組來保存把,把 n 作為我們的數組下標,f(n) 作為值,例如 arr[n] = f(n)。f(n) 還沒有計算過的時候,我們讓 arr[n] 等于一個特殊值,例如 arr[n] = -1。

當我們要判斷的時候,如果 arr[n] = -1,則證明 f(n) 沒有計算過,否則, f(n) 就已經計算過了,且 f(n) = arr[n]。直接把值取出來就行了。代碼如下:

1//我們實現假定arr數組已經初始化好的了。 2intf(intn){ 3if(n<=?1){ 4????????return?n; 5????} 6????//先判斷有沒計算過 7????if(arr[n]?!=?-1){ 8????????//計算過,直接返回 9????????return?arr[n];10????}else{11????????//?沒有計算過,遞歸計算,并且把結果保存到?arr數組里12????????arr[n]?=?f(n-1)?+?f(n-1);13????????reutrn?arr[n];14????}15}

也就是說,使用遞歸的時候,必要須要考慮有沒有重復計算,如果重復計算了,一定要把計算過的狀態保存起來。

2. 考慮是否可以自底向上

對于遞歸的問題,我們一般都是從上往下遞歸的,直到遞歸到最底,再一層一層著把值返回。

不過,有時候當 n 比較大的時候,例如當 n = 10000 時,那么必須要往下遞歸10000層直到 n <=1 才將結果慢慢返回,如果n太大的話,可能棧空間會不夠用。

對于這種情況,其實我們是可以考慮自底向上的做法的。例如我知道

f(1) = 1;

f(2) = 2;

那么我們就可以推出 f(3) = f(2) + f(1) = 3。從而可以推出f(4),f(5)等直到f(n)。因此,我們可以考慮使用自底向上的方法來取代遞歸,代碼如下:

1publicintf(intn){ 2if(n<=?2) 3???????????return?n; 4???????int?f1?=?1; 5???????int?f2?=?2; 6???????int?sum?=?0; 7 8???????for?(int?i?=?3;?i?<=?n;?i++)?{ 9???????????sum?=?f1?+?f2;10???????????f1?=?f2;11???????????f2?=?sum;12???????}13???????return?sum;14???}

這種方法,其實也被稱之為遞推。

最后總結

其實,遞歸不一定總是從上往下,也是有很多是從下往上的,例如 n = 1 開始,一直遞歸到 n = 1000,例如一些排序組合。對于這種從下往上的,也是有對應的優化技巧,不過,我就先不寫了,后面再慢慢寫。這篇文章寫了很久了,脖子有點受不了了,,,,頸椎病?害怕。。。。

說實話,對于遞歸這種比較抽象的思想,要把他講明白,特別是講給初學者聽,還是挺難的,這也是我這篇文章用了很長時間的原因,不過,只要能讓你們看完,有所收獲,我覺得值得!有些人可能覺得講的有點簡單,沒事,我后面會找一些不怎么簡單的題。

聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規問題,請聯系本站處理。 舉報投訴
  • 函數
    +關注

    關注

    3

    文章

    4344

    瀏覽量

    62847
  • 遞歸
    +關注

    關注

    0

    文章

    29

    瀏覽量

    9044

原文標題:為什么你學不會遞歸?告別遞歸,談談我的一些經驗

文章出處:【微信號:TheAlgorithm,微信公眾號:算法與數據結構】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏

    評論

    相關推薦

    【初級】labview教程每日教之14遞歸與可重入+15各種節點

    各種節點在 LabVIEW 中很多時候需要進行一些較為復雜的數學計算,如果單純的采用四則運算來實現,不僅費時耗力,而且大量的占用了程序框圖空間。為此,LabVIEW 提供了幾種方便快捷的實現方法,包括
    發表于 11-08 10:31

    《C Primer Plus》讀書筆記——遞歸

    本帖最后由 cugwyman 于 2017-2-5 20:14 編輯 遞歸的原理個函數調用其本身,此調用過程為遞歸(recursion)。遞歸的使用舉個栗子:/*用來測試UpA
    發表于 02-05 20:06

    個水桶等分8升水問題---用LabVIEW遞歸解題

    個水桶等分8升水問題。書中用到了遞歸的思想。關于遞歸,我之前總認為遞歸對程序開發者而言是可有可無的,但是我看了算法的樂趣前幾章之后,再也不這樣認為了。
    發表于 02-14 22:06

    LabVIEW遞歸

    我的上遍主題寫了“個水桶等分8升水問題”,在其中提到了遞歸的重要性以及LabVIEW如何設置VI才能使得該VI可以實現遞歸調用。而最近看了下《算法的樂趣》中,看到愛因斯坦問題這
    發表于 02-19 11:52

    labview中的遞歸調用的一些個人理解

    自己在網上看了一些網友的看法,加上自己的理解,覺得遞歸的調用簡單地說就是,在處理方法致的時候,函數本身調用自己。去把個較大的問題簡單化,類似像是
    發表于 09-05 16:12

    LabVIEW中的遞歸調用

    .NI提供的遞歸調用使用的步驟如下1.將VI設置成重載模式2.使用靜態調用調用調用VI,實現自身調用看見下圖NI自帶遞歸方法二、如果將靜態調用改成直接調用自身也可得到相同的結果,而且程序更直
    發表于 05-18 10:36

    Labview遞歸函數的使用案例

    Labview遞歸函數的使用案例,簡單的1+2+3...+100求和,簡單易懂,充分理解遞歸函數的思想
    發表于 10-09 09:37

    LabVIEW中使用遞歸算法

    LabVIEW中使用遞歸算法LabVIEW支持遞歸嗎?如何在LabVIEW中創建遞歸的VI?LabVIEW確實支持遞歸。按照下面的步驟來創建
    發表于 04-17 20:11

    C++教程之函數的遞歸調用

    C++教程之函數的遞歸調用 在執行函數 f 的過程中,又要調用 f 函數本身,稱為函數的遞歸調用;形式上:個正在執行的函數調用了自身;這種遞歸稱之
    發表于 05-15 18:00 ?35次下載

    遞歸算法的設計模式與調試

    文中提出種通用遞歸算法的設計模式,并結合實例說明該模式的應用方法和有效性,為研究遞歸算法提供了有效的解決方案,可推廣性強。同時給出了遞歸程序在調試過程中的
    發表于 11-03 15:04 ?24次下載

    種資源路徑高速遞歸算法

    為解決無線移動自組織網絡存在的資源路徑遞歸困難,控制開銷巨大等實際部署難題。基于動量自優機制,本文提出了種資源路徑高速遞歸算法。首先通過分布在網絡中的節點動量的監測,綜合計算路徑高速遞歸
    發表于 11-11 17:32 ?0次下載
    <b class='flag-5'>一</b>種資源路徑高速<b class='flag-5'>遞歸</b>算法

    種任意重積分自適應遞歸式快速計算方法

    當前一些科學計算軟件雖然能夠利用教值積分得到復雜積分的近似解,但最多只能求解重積分。為解決積分計算重數受限問題,基于累次積分的解析計算思路,結合遞歸算法,提岀了
    發表于 05-10 15:10 ?3次下載

    Python支持遞歸函數

    Python支持遞歸函數——即直接或間接地調用自身以進行循環的函數。遞歸是頗為高級的話題,并且它在Python中相對少見。然而,它是項應該了解的有用的技術,因為它允許程序遍歷擁有任意的、不可預知的形狀的結構。
    的頭像 發表于 02-21 14:28 ?670次閱讀

    C語言,你真的懂遞歸了嗎?

    要說到遞歸如果不說棧的話,我覺得有點不合適,遞歸特點就是不斷的調用同個函數,如果這個函數沒有遞歸界限,那么就是死循環了,所以討論
    的頭像 發表于 06-06 15:24 ?1042次閱讀
    C語言,你真的懂<b class='flag-5'>遞歸</b>了嗎?

    Python遞歸的經典案例

    當我們碰到諸如需要求階乘或斐波那契數列的問題時,使用普通的循環往往比較麻煩,但如果我們使用遞歸時,會簡單許多,起到事半功倍的效果。這篇文章主要和大家分享一些遞歸有關的經典案例,結合
    的頭像 發表于 08-05 15:57 ?363次閱讀
    主站蜘蛛池模板: 一级特黄性生活大片免费观看| a欧美视频| 深爱五月网| 亚洲一二三四| 国产高清小视频| 日韩伊人网| 夜夜夜夜夜操| 国产三级日本三级韩国三级在线观看| 看日本黄色大片| 色偷偷网| 欧洲综合色| 色五月激情小说| 欧美视频在线观在线看| 美女骚网站| 免费视频你懂得| 狠狠色丁香婷婷综合欧美| 女人张开腿 让男人桶个爽 免费观看| 五月综合激情网| 一级毛片真人免费播放视频| 男女视频在线观看免费| 在线国产你懂的| 手机在线精品视频| 毛片区| 国产精品17p| 黄欧美| 最新午夜| 天天天天天天操| 天天操天天擦| 天天狠天天透天干天天怕处| 午夜国产精品久久影院| 午夜在线视频观看| 亚洲看黄| 精品久久久久久| 2023天天操| 国产在线欧美精品卡通动漫| 扒开末成年粉嫩的小缝强文| 色网站综合| 成 年 人 视频在线播放| 在线免费观看视频黄| 奇米第四狠狠777高清秒播| 黄色在线观看视频网站|