数据结构杂谈番外篇——搞懂递归的小文章
文章目錄
- 1 難題
- 2 遞歸
- 2.1 n的階層
- 2.2 斐波那契數列的第n項
- 2.3 逆序打印數組
- 3 反轉鏈表
- 4 回顧遞歸
1 難題
如果不想聽我談學習的過程而注重怎么學習,可以直接跳到第二小節
這個遞歸的問題是在我刷題的時候遇到的。事實上,我對遞歸是一竅不通的,第一次學遞歸是在大二上學期學的數據分析和可視化中遇到了,但是那時候老師叫我們背,所以沒怎么注意這個問題。
沒注意的問題在后面就開始暴露出來了。在Leetcode刷題的時候,第一次遇見遞歸是在反轉單鏈表的時候。反轉單鏈表一文可以在每日一題——劍指 Offer24反轉鏈表_塵魚好美的小屋-CSDN博客中查看。在當時,我用的僅僅是新手都能接受的迭代法。而無法接受思維混亂的遞歸,但是在解決Leetcode上的另外一道題的時候就開始出問題了,這道題必須用到遞歸或者棧。
我們學過棧的都知道,棧的本質是遞歸,這就意味著這個知識點是一個跨不過的坎,我知道我必須面對了。
對于解決這個問題,我首先是看了一下大佬的遞歸解法劍指 Offer 24. 反轉鏈表(迭代 / 遞歸,清晰圖解) - 反轉鏈表 - 力扣(LeetCode) (leetcode-cn.com)。但是我發現其對于遞歸的本質沒有詳細的闡述,反而是只提解法,這對于新手顯然十分不友好。我又在看不懂遞歸的看過來,希望能幫到你! - 反轉鏈表 - 力扣(LeetCode) (leetcode-cn.com)上面看到了另外一個大佬的解法,雖然講的挺好,但是在單鏈表反轉中又是讓人無法接受了,但是至此,我突然腦路一開,發現了一種新思路,我十分愿意和你分享我思考的思路,希望你耐心看完我的文章。
2 遞歸
大多數講述遞歸都是先引出斐波那契數列。實際上,我們無需畏懼遞歸這個名詞,我們先用另外一個詞來體會遞歸,即遞推公式,這在我們高中數學中幾乎人人學過。在講述斐波那契數列前,我們來解決一個問題。如何解決用遞歸實現n的階層計算?
2.1 n的階層
完成遞歸實際上就是三部曲:
- 明確函數目的
- 尋找遞歸結束條件
- 找出函數的等價關系式
這么說好像太空了,我們來給出一個圖,實際上這個圖就是遞歸。
沒錯,我們可以把俄羅斯套娃看成是遞歸,也就是說,每一層的娃都是在解決問題,遞歸的過程是把解決的問題都留在最后,先從外到里一步一步取娃,然后在最里面的娃從里到外解決問題。
現在我們看往例子:如果我們要解決階層問題,那么結束遞歸的條件就是一個你知道的數,比如你從5的階層,那么自然1的階層是你知道的,那你就可以把1作為結束條件。當然,2你也知道是多少,甚至于更高。我們先用1來作為結束條件:
//結束條件 if(n == 1) {return 1; }那我們接下來就是要寫函數等價式了,這實際上是一個創造套娃的過程,從最小的娃開始,和相鄰的娃建立聯系。也就是說,我們只關注第n個娃和n-1個娃之間的關系,在這個例子中,它們的關系就是f(n) = n*f(n-1)。
等價關系式的尋找在這里看起來十分簡單,可實際上,遞歸最難的就是此步。
接下來我們把上述寫成代碼,如下所示:
int func(int n) {if(n == 1)return 1;return n*func(n-1); }也可以用2為結束遞歸條件,如下所示:
int func(int n) {if(n <= 2)return 2; //2的階層return n*func(n-1); }綜上所述,這就是一個最簡單的遞歸了。在下面,我們層層遞進,來解決一些實際的問題。
2.2 斐波那契數列的第n項
我們來解決這么一個問題:
斐波那契數列的是這樣一個數列: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 項的值是多少。
按照上面的套路,函數實際上是要返回一個第n項的值,所以我們可以這么定義:
int func(int n) {}接下來我們要尋找遞歸結束的條件,根據題意,我們知道f(1) = 1,f(2) = 1… 那么根據我們在最開始講到的,我們實際上是尋求函數關系等價式,在本題中,如果你采用n = 1作為遞歸結束條件,那么在函數等價式中(本題已給出)有一個f(n) = f(n-1)+f(n-2),你把2填進去,會出現一個f(0),這樣的話越過了f(0)越過了遞歸結束條件n = 1,會無限死循環下去。
這顯然是我們不希望的,所以我們可以用n<=2來作為循環結束條件,這樣,f(n)中的n只能填3以上的數字才會出現循環。
綜上所述,代碼如下所示:
int func(int n){if(n <= 2){return 1;}return func(n-1) + func(n-2) }2.3 逆序打印數組
上面的斐波那契數列問題實際上很容易看出函數等價關系式,讓我們來一個不那么明顯地例子。
我們需要逆序打印一個長度為n的數組。請問如何解決?
也就是說,我們要的是打印一個數組?我們可以這么做:
int func(int arrs[]) {}接下來我們需要尋找遞歸結束條件,這個結束的條件就是數組為空,即n = 0就結束。
int func(int arrs[]) {if(n == 0)return false; }現在讓我們來找函數等價關系,在這里,我們明顯要倒序打印,所以首要任務是先打印再倒推,倒推的過程實際上是一個類似于指針移動的過程:n = n-1,所以我們可以寫出如下代碼:
void func(int arrs[], int n) {if (n <= 0)return;cout << arrs[n - 1] << endl;return func(arrs, n - 1); }3 反轉鏈表
回到我們的主題,我們要解決的最終問題是,如何解決反轉鏈表,乃至解決更多問題,既然要使用遞歸這個工具乘風破浪,那就先拿這道破題開刀吧。
定義一個函數,輸入一個鏈表的頭節點,反轉該鏈表并輸出反轉后鏈表的頭節點。
示例:
輸入: 1->2->3->4->5->NULL
輸出: 5->4->3->2->1->NULL
限制:
0 <= 節點個數 <= 5000
來源:力扣(LeetCode)
鏈接:https://leetcode-cn.com/problems/fan-zhuan-lian-biao-lcof
著作權歸領扣網絡所有。商業轉載請聯系官方授權,非商業轉載請注明出處。
采用遞歸,首先要明白函數要干嘛,函數要反轉鏈表并且返回頭結點。
//逆置鏈表函數 ListNode* reverseList(ListNode * head) {}但是實際上,我們要實現遞歸的地方不是在這個逆置函數內,所以我們可以另外寫一個遞歸函數。我們的思路是,分別指定兩根指針,一根為pre,一根為cur,反轉鏈表后cur.next = pre。最開始pre一定是空,cur一定是處于head的位置。
//逆置鏈表函數 ListNode* reverseList(ListNode * head) {}//遞歸函數 ListNode* recur(ListNode* cur, ListNode* pre) {}接下來找結束條件。最開始pre是空,cur處于head的位置。當遞歸執行時,最內層的循環是cur快跑到null了。所以結束遞歸的條件一定是cur->next = NULL。
在最內層循環中,我們做的是:改變指針指向,即cur.next = pre。并且在最內層循環是,cur所處位置恰好是逆置后鏈表頭結點所處位置。當遞歸函數執行完成,返回逆置鏈表頭結點位置。而在逆置鏈表函數中,僅僅需要調用遞歸函數并且返回逆置鏈表頭結點位置即可。
class Solution { public:ListNode* reverseList(ListNode* head) {return recur(head, nullptr); // 調用遞歸并返回} private:ListNode* recur(ListNode* cur, ListNode* pre) {if (cur == nullptr) return pre; // 終止條件ListNode* res = recur(cur->next, cur); // 遞歸后繼節點cur->next = pre; // 修改節點引用指向return res; // 返回反轉鏈表的頭節點} };從套娃的角度來看,我們可以這樣做:
4 回顧遞歸
遞歸實際上是一個解決子問題的過程。我們要解決f(n),實際上首先解決f(n-1),要解決f(n-1),實際上要先解決f(n-2),以此類推直至先解決最根本的問題,再回溯整個過程。
遞歸實際上也是需要優化的,比如f(n) = f(n-1)+f(n-2),如果n = 5,那么n-1 = 4,n-2 = 3,后續在遞歸的過程中會出現多次f(4)、f(3)等,如果每次都計算,開銷挺大,一般可以用某個值來保存,但是這篇文章是針對像我一樣的初學者的,我們就偷個懶,放自己一馬吧。
好了,彥祖,別太累了,好好消化一下就休息吧。
總結
以上是生活随笔為你收集整理的数据结构杂谈番外篇——搞懂递归的小文章的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: XP硬盘安装Fedora14图文教程
- 下一篇: 小白学习一eNSP华为模拟器(3) 交换