5000字徹底搞明白 遞歸

下載第1-第4周 週報pdf版 請移步星球

本週算法刷題集中遞歸專題,下面是從我的知識星球裏選取的星友們的精華回答,推送在公衆號裏,希望能真正幫助到更多朋友。如果你對算法感興趣,歡迎加入我的星球(掃描文末二維碼),只有幾十塊錢,收益卻是無價的,每天成長都是可見的。

Day 25:遞歸求斐波那契數列前 N 項

先總結 Day 24 作業題,再佈置 Day 25 作業題。

Day 24 作業總結

給定一個非負整數 numRows,生成楊輝三角的前 numRows 行。

已知楊輝三角第 i-1行,生成第 i 行爲:

[1] + 
[yanghui[-1][i-1] + yanghui[-1][i] for i in range(1, numRows-1)] + 
[1]

完整代碼:

class Solution():
 def generate(self, numRows):
  if numRows == 0:
   return []
  elif numRows == 1:
   return [[1]]
  else: # 調用自身生成前 numRows - 1 行的楊輝三角
   yanghui = self.generate(numRows - 1) 
      # 根據倒數第二行再生成最後一行:
      last_row = [1] + [yanghui[-1][i-1] + yanghui[-1][i] for i in range(1, numRows-1)] + [1]
   yanghui.append(last_row)
  return yanghui

遞歸總結

在實現遞歸函數之前,有件重要的事情需要解決:找出遞推關係。

Day25 作業

下面再進一步,學習遞歸的其他知識。

通常情況下,遞歸是一種直觀而有效的實現算法的方法。但是,如果使用不合理,會造成大量的重複計算。那麼,你有什麼辦法能消除某些重複計算呢?通過求斐波那契數問題,體會如何消除遞歸計算中的重複計算問題。

def fib(self, N):
    pass

補全以上代碼,返回斐波那契數列前 N 項。

Day 26:現實中,一名算法工程師的日常是什麼?

總結 Day 25 作業題

通常情況下,遞歸是一種直觀而有效的實現算法的方法。但是,如果使用不合理,會造成大量的重複計算。

例如求斐波那契數問題時,參考星友 infrared62 的解釋:Fibonacci Number,使用遞歸來解,首先如果直接遞歸會用很多重複子問題計算,畫完二叉樹會發現是指數級的時間複雜度,每一層的計算比上一層多2倍。

下面的樹顯示了在計算 時發生的所有重複計算(按顏色分組)。

那麼,你有什麼辦法能消除某些重複計算呢?很自然的一個想法,將中間結果存儲在緩存中,以便以後可以重用它們,而不需要重新計算。

這是一種經常與遞歸一起使用的技術。

通過求斐波那契數問題,體會如何消除遞歸計算中的重複計算問題。

具體代碼實現:

def fib(self, N):
    history = {}
    def recur(N):
        if N in history:
            return history[N]
        if N < 2:
            result = N
        else:
            result = recur(N-1) + recur(N-2)
        history[N] = result
        return result

    return recur_fib(N)

補全以上代碼,返回斐波那契數列前 N 項。

星友 infrared62 還提出一個緩存的方法:在Python中也可以用語言特性 @lru_cache 來自動緩存。

代碼參考下面:

Day 26 :現實中,一名算法工程師的日常是什麼?

如果你是學生,還未畢業工作,你可能會暢想着將來成爲一名算法工程師,看起來高大上,待遇各方面都不錯。那麼現實中,一名算法工程師的日常又是什麼呢?你可以想象一下然後打卡。如果你是算法工程師,那麼平時大部分時間在做什麼,也歡迎打卡留言。

Day 27:如何分析遞歸的時間複雜度?

總結 Day 26 作業

作業題:現實中,一名算法工程師的日常是什麼?

首先,大概看下算法工程師日常做什麼:

算法工程師的日常 星友 LFeng 回答

業務場景分析 需求分析 數據處理 建模 調試 應用 反饋調整 總結報告等

這個總結很精煉。

參考星友 箱子 回答

我感覺算法工程師有相當一部分時間是在處理數據。算法工程師也是爲項目服務,項目也是爲最後的解決方案服務,解決方案也是爲了解決現實生活中的實際問題。爲了讓現實生活中數據呈現出背後隱藏的信息或者趨勢,才需要一個算法來支撐,但是我們能得到的卻是雜亂無章的東西。整理數據,優化數據結構,我感覺也需要很多的工作量。

具體來說,參考星友 北方 回答

借鑑於知乎 1.對接業務方,第一個步驟是去和業務方進行對接,對接的過程中要去了解業務方的需求,業務的真實痛點,很多時候對接的業務方並不瞭解算法能做哪些事情,因此,在對接的過程中業務方會提自己想要的目標是什麼,而此時算法工程師需要憑藉經驗去評估這個方向的業務價值,評估這個方向是否適合算法來做,現在是否是切入的好時機。

2.數據的盤點,對於算法來說,數據的重要性不言而喻,很多時候調半個月的參數,模型的效果都不如引入一份高質量數據來得效果好。阿里算是數據的基礎設施做的很好的一家公司,數據在不同平臺的流轉工作相對方便,另外阿里也有很多基礎數據,然而,實際在工作的過程中還是時常會碰到沒有數據,或者數據質量不佳的情況,在這種情況下一方面需要對數據做出大量預處理的工作,另一方面,需要去思考如何依賴現有數據對模型進行評估,進而去評估業務效果。

3.建模,包括以什麼樣的思路去解決這個問題,預測模型效果增益,特徵抽取,特徵處理,選用何種模型,效果評估,模型迭代,這部分可能是在我的人之中算法工程師的工作,而在實際工作中,這部分工作如果能佔用30%的精力,已經是很高的比例了。

4.模型上線,取決於依賴何種上線方式,有的時候很簡單,可能產出的結果只是一張結果表,業務方下游直接引用即可,有的時候涉及到大量的工程工作,需要考慮模型的性能,複雜度。延時,可解釋性什麼的也會需要去進行考慮。

這四個過程中就是算法的日常工作,精力分配大概是30%,10%,30%,30%,這只是一個毛估估的精力分配比例,有可能一兩週都是處於和業務方開會過程中,也有可能某一個小項目從頭到尾都比較確認,直接當天就能從第一步走到第四步。

這個回答,確實很中肯。

算法工程師需要具備的能力,可以大概參考星友:孫穎潁穎頴頴 的回答:

舉我的例子 一名合格的圖像處理算法工程師 不但要有紮實的計算機基礎 什麼c++和python是必須要熟練使用的 還有一些深度學習的框架如tf torch 還有一些圖像處理的算法 opencv等等 然後就是細分方向了 人臉識別 目標檢測等等 在學校還是以學術復現頂會論文爲主 出學校就是根據自己的方向從事相關的工作吧 解決問題 根據項目要求設計模型 優化模型 在項目上有想法就發發專利和論文 感覺算法工程師重在idea吧 挺難的。

大家一步一步來,慢慢掌握

Day 27 作業題 :遞歸的時間複雜度分析

大家參考下面網址:

https://leetcode-cn.com/explore/orignial/card/recursion-i/259/complexity-analysis/1222/

回答一個問題:如何分析遞歸的時間複雜度?

Day 28 :0-1揹包問題

先總結 Day 27 作業題

遞歸確實能讓代碼變得更加簡潔,讓代碼看起來更加優美,但是稍不留神,就會寫出時間複雜度爲指數級的代碼。所以使用遞歸,必須要留意時間複雜度分析。

昨天推薦的參考網址:

https://leetcode-cn.com/explore/orignial/card/recursion-i/259/complexity-analysis/1222/

裏面主要講到兩類時間複雜度分析,一種以相反順序打印字符串的遞歸,這種時間複雜度的求解較爲直觀,因爲每次問題規模 都會縮減 ,並且每次只打印 個字符,故時間複雜度爲:

但是,以求解裴波那契數列爲代表的遞歸,時間複雜度的求解就不那麼直觀了,文中給出一個很好的求解示意圖:

因爲遞歸的方程:

以求解數列前 4 項爲例,在求解 f(4) 是需要求解出 f(3) 和 f(2),求解 f(3) 時又需要求解 f(2) 和 f(1),以此類推。

整個過程可以繪製爲下圖的二叉樹:

我們知道二叉樹模型的節點個數與層數的關係爲指數次冪,所以遞歸的時間複雜度爲:

如果不做優化,時間複雜度爲指數級的算法基本是難解,或者不是一個真正意義上的可行解, 就已經是一個很恐怖的數字:

所以使用遞歸再次告訴我們:記憶化技術或稱爲緩存技術的重要性。

以上就是兩類典型的遞歸時間複雜度。

Day 28:0-1揹包

0-1 揹包是一個經典的組合優化問題,其中的思想非常重要。今天我們以一個簡單的例子,先來體會 0-1 揹包問題。

有一個最大承重量爲w的揹包,第i件物品的價值爲a1[i],第i件物品的重量爲a2[i],將物品裝入揹包,求解揹包內最大的價值總和可以爲多少?

例子:

a1 = [100, 70, 50, 10], a2 = [10, 4, 6, 12], w = 12, 揹包內的最大價值總和爲 120,分別裝入重量爲4和6的物品,能獲得最大價值爲 120

補全下面代碼,返回求解的最大價值:

def f(a1,a2,w):
    pass

Day 29 :遞歸求解 0-1 揹包問題

本週專題:學習遞歸,所以昨天作業題0-1揹包也是基於遞歸的一類問題,只不過帶有動態規劃,動態轉移方程,這些我們後面會重點講解。

再理解下0-1揹包問題:

有一個最大承重量爲w的揹包,第i件物品的價值爲a1[i],第i件物品的重量爲a2[i],將物品裝入揹包,求解揹包內最大的價值總和爲多少?

例子:

價值數組:a1 = [100, 70, 50, 10],

重量數組:a2 = [10, 4, 6, 12],

揹包最大可載重量:W = 12

結果:揹包內的最大價值總和爲 120,分別裝入重量爲 4 和 6 的物品,能獲得最大價值爲 120

如何計算機編寫代碼求出這類問題,返回求解的最大價值。

分析過程:

如下圖所示:

第一行物品價值

第二行物品重量

我們從最右側開始決策是否裝入重量爲12的物品:

揹包可裝最大重量恰好爲 12

  1. 如果選擇裝入此物品,揹包內物品價值爲 10,並且已經不能再裝入,因此得到一種可行解:價值爲 10

    1. 如果選擇不裝入,我們的視線移動到下一個物品決策上,同樣地我們會面臨裝入還是不裝入的兩個可選擇項:

    如果選擇裝入,創造 50 價值,並且還能最多裝入重量爲6的物品:

    以此類推。

    你看,無論何時,當視線移動到下一個物品時,我們都會面臨裝還是不裝的問題,稱此類問題爲:

    0-1 揹包問題

    以上過程轉化爲如下求解代碼:

    a1 = [100, 70, 50, 10]
    a2 = [10, 4, 6, 12]
    W = 12
    
    
    def f(i, w):
        # 基本情況:
        if w == 0 or i < 0:
            return 0
        elif a2[i] > w:
            return f(i-1, w)
        # 遞歸:
        return max(a1[i] + f(i-1, w-a2[i]),
                   f(i-1, w))
    
    
    r = f(3, W)
    print(r) # 120
    

    基本情況包括:

    1. 揹包可裝載重爲 0 時,表明它不能再裝物品了,自然價值爲 0

    2. i < 0 表明沒有物品可裝入了,自然價值也爲 0

    3. 當 a2[i] > w 時,表明此物品太重,揹包剩餘空間裝載不下,表明此物品不能裝入揹包,我們的視線自動移動到下一個物品,即返回:f(i-1,w)

    遞歸情況:到這裏表明,a2[i] 能裝入揹包,就看我們是否選擇它:0-1 選擇問題:

    裝:a1[i] + f(i-1, w-a2[i]),產生 a1[i] 大小的價值,代價揹包剩餘空間變小,還能裝載 w-a2[i]

    不裝:f(i-1, w)

    決策:選擇較大的

    以上問題遞歸樹的完整示意圖如下:

    Day 29 :複習 0-1 揹包問題

    如果覺得昨天作業不好理解,或者之前拉下作業的星友,藉助Day29的機會彌補一下。已經全都搞定的星友,Day29放鬆一下吧。

    Day 30  :理解遞歸的特例:尾遞歸

    0-1 揹包問題的遞歸解法有些星友理解起來有些困難,所以Day29我們放慢腳步,單獨留出這一天好好理解揹包問題。

    算法的魅力:它會讓你念念不忘,幾天後必有迴響,然後編程水平就會提升一個小小的 level

    今天我去油管上翻閱一下0-1揹包講解視頻,覺得下面這個講解較爲形象,特意再在這裏帖一下:

    圖1(左上角):揹包問題示意圖

    圖2:待求解問題,例子

    圖3:求解表格,這是動態規劃的求解,不是我們這周訓練的遞歸的求解方法。現在這裏提一下動態規劃,後面會重點講到。

    圖4:全部求解完成

    圖5:檢驗價值7如何得來的

    圖6:最後選擇放置到揹包裏的三個物品,綠顏色表示:

    Day 30 尾遞歸

    使用遞歸會使代碼變得非常簡潔,但是遞歸利用不慎,很容易就會出現:stack overflow 棧溢出的問題,這是因爲通常的遞歸也需要消耗在系統調用棧上產生的隱式額外空間,這算是我們使用遞歸所付出的代價。

    但是有一類遞歸非常特殊,它不受此空間開銷的影響。它就是一種特殊的遞歸情況:尾遞歸。

    那麼,滿足哪些條件纔算是尾遞歸呢?下面的兩種代碼示例,哪個是尾遞歸,哪個是一般的遞歸情況呢?

    寫法1:

    def sum:1(ls):
        if len(ls) == 0:
            return 0
        return ls[0] + sum1(ls[1:])
    

    寫法2:

    def sum2(ls):
        def helper(ls, acc):
            if len(ls) == 0:
                return acc
            return helper(ls[1:], ls[0] + acc) 
        return helper(ls, 0)
    

    Day 31:使用遞歸快速冪算法:Pow(x,n)

    總結 Day30 尾遞歸作業

    遞歸調用是遞歸函數中的最後一條指令。並且在函數中應該只有一次遞歸調用。

    大家注意:最後一行語句和最後一條指令的區別:

    下面代碼中sum1函數最後一條語句也是sum1函數,但是最後一條指令顯然是加法操作。所以它不是尾遞歸

    def sum1(ls):
        if len(ls) == 0:
            return 0
        return ls[0] + sum1(ls[1:])
    

    下面函數sum2中的子函數helper的最後一條指令也是helper,所以它是尾遞歸:

    def sum2(ls):
        def helper(ls, acc):
            if len(ls) == 0:
                return acc
            return helper(ls[1:], ls[0] + acc) 
        return helper(ls, 0)
    

    總結:非尾遞歸中,在最後一次遞歸調用之後有一個額外的計算。

    Day 31 作業題

    求 x 的 n 次冪,一般解法時間複雜度爲:O(n),你能使用遞歸寫出 O(logn) 的解法嗎?

    補全如下代碼,返回 x 的 n 次冪:

    def pow(x,n):
        pass
    

    如果你想從零學習算法,不妨加入下面星球,現在是最划算的時候,還提供專門的星友微信交流社羣,每天在星球裏記錄自己的學習過程,學習其他星友的解題分析思路。打卡 300 天退換除平臺收取的其他所有費用。

    長按二維碼,加入我的星球

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