歡迎關注 代碼宇宙,每天9點半,不見不散
前言
遞歸解法總是給人一種“只可意會不可言傳”的感覺,代碼一看就懂,自己動手一寫就呆住了,很難受。究其原因,一是我們練習不夠,二是理解不夠。
什麼是遞歸
遞歸的例子在平時生活中很容易見到,比如:
開個玩笑😁
什麼是遞歸呢?函數在運行時調用自己,這個函數就叫遞歸函數,調用的過程叫做遞歸。
比如定義函數 f(x)=x+f(x-1)f(x)=x+f(x−1):
def f(x):
return x + f(x-1)
如果代入 f(2)f(2):
返回 2+f(1)2+f(1);
調用 f(1)f(1);
返回 1+f(0)1+f(0);
調用 f(0)f(0);
返回 0+f(-1)0+f(−1)
……
這時程序會無休止地運行下去,直到崩潰。
如果我們加一個判斷語句 x > 0:
def f(x):
if x > 0:
return x + f(x-1)
else: # f(0) = 0
return 0
這次計算
f(2)=2+f(1)=2+1+f(0)=2+1+0=3f(2)=2+f(1)=2+1+f(0)=2+1+0=3
我們從中總結兩個規律:
遞歸函數必須要有終止條件,否則會出錯;
遞歸函數先不斷調用自身,直到遇到終止條件後進行回溯,最終返回答案。
例題
將兩個有序鏈表合併爲一個新的有序鏈表並返回。新鏈表是通過拼接給定的兩個鏈表的所有節點組成的。
示例:
輸入:1->2->4
, 1->3->4
輸出:1->1->2->3->4->4
遞歸解法
我們可以如下遞歸地定義在兩個鏈表裏的 merge 操作(忽略邊界情況,比如空鏈表等):
也就是說,兩個鏈表頭部較小的一個與剩下元素的 merge 操作結果合併。
根據以上規律考慮本題目:
終止條件:當兩個鏈表都爲空時,表示我們對鏈表已合併完成。
如何遞歸:我們判斷 l1 和 l2 頭結點哪個更小,然後較小結點的 next 指針指向其餘結點的合併結果。(調用遞歸)
代碼
Python:
class Solution:
def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:
if not l1: return l2 # 終止條件,直到兩個鏈表都空
if not l2: return l1
if l1.val <= l2.val: # 遞歸調用
l1.next = self.mergeTwoLists(l1.next,l2)
return l1
else:
l2.next = self.mergeTwoLists(l1,l2.next)
return l2
Java:
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if (l1 == null) {
return l2;
}
else if (l2 == null) {
return l1;
}
else if (l1.val < l2.val) {
l1.next = mergeTwoLists(l1.next, l2);
return l1;
}
else {
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
}
}
C++:
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
if (l1 == NULL) {
return l2;
}
if (l2 == NULL) {
return l1;
}
if (l1->val <= l2->val) {
l1->next = mergeTwoLists(l1->next, l2);
return l1;
}
l2->next = mergeTwoLists(l1, l2->next);
return l2;
}
};
複雜度分析
如何計算遞歸的時間複雜度和空間複雜度呢?其中時間複雜度可以這樣計算:
給出一個遞歸算法,其時間複雜度 O(T)
,通常是遞歸調用的數量記作 R
和計算的時間複雜度的乘積(表示爲O(S)
)的乘積:O(T) = R * O(s)
時間複雜度O(m+n)。
m 和 n 爲 l1 和 l2 的元素個數。遞歸函數每次去掉一個元素,直到兩個鏈表都爲空,因此需要調用 R=O(m + n)R=O(m+n)
次。而在遞歸函數中我們只進行了 next 指針的賦值操作,複雜度爲 O(1)
,故遞歸的總時間複雜度爲 O(T) = R * O(1) = O(m+n)
。
空間複雜度:O(m+n)。
對於遞歸調用
self.mergeTwoLists()
當它遇到終止條件準備回溯時,已經遞歸調用了 m+nm+n 次,使用了 m+nm+n 個棧幀,故最後的空間複雜度爲O(m+n)。
相關題目
以下是一些基礎但很經典的題目,值得我們好好練習:
反轉字符串(https://leetcode-cn.com/problems/reverse-string/)
漢諾塔問題(https://leetcode-cn.com/problems/hanota-lcci/solution/)
兩兩交換鏈表中的節點(https://leetcode-cn.com/problems/swap-nodes-in-pairs/)
二叉樹的最大深度(https://leetcode-cn.com/problems/maximum-depth-of-binary-tree/)
如有問題,歡迎討論~
文章推薦(公衆號:代碼宇宙,閱讀)
一看就會:最大自序和狀態壓縮算法
堅持做一件事,究竟難在哪裏?
有趣的多線程和無趣的線程鎖
做技術,如何使自己在重複性業務中持續提升?
Openresty 配合 redis 實現無感知灰度發佈系統(基礎篇)
「純手打」2萬字長文從0開始Spring Boot(上)