一看就會,一寫就廢?詳解遞歸

歡迎關注 代碼宇宙,每天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(上)

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章