以Top-Down思維去解決問題——遞迴

Mysticbinary發表於2024-08-29

目錄
  • 遞迴的基礎
  • 遞迴的底層實現(不是重點)
  • 遞迴的應用場景
  • 程式設計中 兩種解決問題的思維
    • 自下而上(Bottom-Up)
    • 自上而下(Top-Down)
  • 自上而下的思考過程——求和案例
  • 臺階問題 案例
  • 易位構詞生成 案例


遞迴和for迴圈(迭代法)很像,都是透過迴圈去完成一件事。

採用Top-Down思維去設計的遞迴結構,又會比for多一些不同的能力。多什麼能力?

遞迴的基礎

先複習一下遞迴,遞迴的定義:遞迴(英語:Recursion),又譯為遞迴,在數學與電腦科學中,是指在函式的定義中使用函式自身的方法。

image

遞迴的本質就在:遞去歸來 兩個流程中。 初學者剛接觸會有點抽象,下面透過一些案例來認識。

假設需要你實現一個階乘的計算函式,
階乘的定義:
5的階乘是 5*4*3*2*1=120

def factorial(number):
    if number == 1:
        return 1
    else:
        return number * factorial(number - 1)

print(factorial(5))
// 120

遞迴需要考慮三個條件:

  1. 將問題拆分成一個重複使用的子問題;
  2. 注意,這個子問題和問題的規模無關;
  3. 必須包含終止條件 (終止條件即初始狀態)。

遞迴的底層實現(不是重點)

遞迴的底層實現不是本文的重點,瞭解一下就行。

遞迴在程式語言的底層實現通常依賴於呼叫棧(call stack):

  • 呼叫棧

    • 每次函式呼叫時,程式會將函式的引數、區域性變數和返回地址等資訊壓入棧幀。
    • 當遞迴函式呼叫自身時,會建立新的棧幀壓入棧中。
    • 函式執行完畢後,棧幀被彈出,返回控制權給呼叫者。
  • 基線條件

    • 遞迴必須有終止條件,否則會導致棧溢位(stack overflow)。
    • 每次遞迴呼叫都應該向基線條件靠近。
  • 記憶體管理

    • 呼叫棧通常在記憶體的“棧區”分配。棧的大小有限制,過多的遞迴呼叫可能導致棧溢位。
    • 有些語言提供機制來增加棧的大小,但一般不推薦依賴深層遞迴。

總結:

  • 遞迴實現的效率和安全性與具體語言的特性和編譯器的最佳化密切相關。

  • 遞迴的底層實現,就是把相關變數資料(快取)處理後,一層一層的壓入棧,等到了基準條件後,在逐層拿出處理。

計算機眼裡的遞迴:
計算機使用棧來記錄正在呼叫的函式,叫呼叫棧

image

有個區域性變數 number 記錄當前值。

遞迴的應用場景

  • 處理任意多層事情的場景,都可以考慮用遞迴。

  • 當問題和子問題具有遞推關係,比如楊輝三角、計算階乘。

  • 具有遞迴性質的資料結構,比如連結串列、樹、圖。

  • 反向性問題,比如取反操作。

程式設計中 兩種解決問題的思維

這個才是本文重點要學習的。

當面對未來未知的情況時,考慮使用使用自上而下解決問題的思維。

兩種編寫計算函式的方法:

  • 自下而上(Bottom - Up)
    類似迴圈
  • 自上而下 (Top - Down)
    遞迴思想

兩者區別?
在計算函式時,自下而上和自上而下是兩種不同的思維方式和實現策略:

在計算函式時,特別是像階乘這樣的遞迴函式,可以使用兩種主要的實現方式來實現遞迴計算:自下而上(bottom-up)和自上而下(top-down)。這些方法各有優缺點,理解它們有助於選擇適合的實現方式來解決特定的問題。以下是對這兩種實現方式的解釋:

自下而上(Bottom-Up)

自下而上方法,也稱為 迭代方法動態規劃方法,是指從最小的子問題開始逐步構建解決方案,直到解決原始問題。這種方法通常用於動態規劃演算法中,但也可以用於一些簡單的遞迴問題。

實現思路:

  1. 定義最小問題: 從問題的最小子問題開始解決。例如,在計算階乘時,可以從 0!1! 開始。
  2. 逐步構建: 使用迭代或迴圈逐步計算更大的問題的解。
  3. 更新和儲存: 將每個子問題的解儲存起來,以便後續使用。

還是以計算階乘的案例去介紹,自下而上實現方式:

def factorial_bottom_up(n):
    if n < 0:
        raise ValueError("Factorial is not defined for negative numbers")
    result = 1
    for i in range(2, n + 1):
        result *= i
    return result

# 示例用法
print(factorial_bottom_up(5))  # 輸出 120

解釋:

  • 從 1 開始迭代計算 2 到 n 的階乘。
  • 透過逐步乘以每個數字來更新結果,最終得到 n!

自上而下(Top-Down)

自上而下方法也稱為 遞迴方法,是指從解決問題的最上層開始,遞迴地解決較小的子問題。這種方法在處理遞迴問題時非常自然(但可能存在重複計算的子問題,有些可以最佳化)。

實現思路:

  1. 定義主問題: 從問題的最上層開始解決。
  2. 遞迴分解: 將主問題遞迴地分解為較小的子問題。
  3. 基本情況: 定義遞迴的基本情況,以停止遞迴。

還是以計算階乘的案例,展示自上而下實現:

def factorial_top_down(n):
    if n < 0:
        raise ValueError("Factorial is not defined for negative numbers")
    if n == 0 or n == 1:
        return 1
    return n * factorial_top_down(n - 1)

# 示例用法
print(factorial_top_down(5))  # 輸出 120

解釋:

  • n 開始,遞迴呼叫 factorial_top_down(n - 1),直到達到基本情況。
  • 在基本情況時返回 1,逐層返回乘積結果。

總結

  • 自下而上:通常是迭代的方法,逐步構建解決方案,適用於動態規劃和需要避免重複計算的情況。它通常較為高效,尤其是在解決子問題重複計算時。

  • 自上而下:通常是遞迴的方法,直觀地解決問題,但可能會有較高的時間複雜度和空間複雜度,尤其在處理大規模問題時。(可以透過記憶化(Memoization)來最佳化效能)

自上而下的思考過程——求和案例

一般來說,自下而上的實現過程比較好理解,所以這裡多列舉一些自上而下的案例幫助思考,

自上而下的程式設計思考過程:

  1. 把你正在寫的函式想象成是別人實現過的函式。
  2. 辨別子問題。
  3. 看看你在子問題上呼叫函式時會發生什麼,然後以此為基礎繼續。

求和案例
假設我們要寫一個 sum 函式,計算陣列中所有數的和。如果給函式傳入陣列[1,2,3,4,5],那麼它會返回這些數的和15。

我們需要做的第一件事就是想象已經有人實現了 sum 函式。(當然,你可能會有點難以接受,畢竟寫函式是我們自己,怎麼能假設別人寫好了呢! 但可以試著忘掉這一點,先假裝 sum 函式已經實現好了。)

接下來,來辨別子問題。比起科學,這個過程更像是藝術,只要你多練習就能進步。 在這個例子中,可以認為子問題是陣列[2,3,4,5],即原陣列中除第一個數以外的元素。

最後,來看看在子問題上呼叫 sum 函式會發生什麼 ?
如果 sum 函式“已被正確實現”並且子問題是 [2,3,4,5],那麼呼叫 sum([2,3,4,5])時會發生什麼呢?會得到2+3+4+5的和,也就是14。

而要求[1,2,3,4,5]的和,只需向 sum([2,3,4,5])的結果再加上1即可。

請使用程式語言實現一下:

mylist = [1, 2, 3, 4, 5]
def mysum(alist):

    if len(alist) == 1:
        return alist[0]
    else:
		# alist[-1] = alist[len(alist)-1]
        alist[0] += alist[-1]

    return mysum(alist[0:len(alist) - 1])

print(mysum(mylist))
# 15

臺階問題 案例

為什麼需要用遞迴?
image

image

請寫出程式碼:

todo

易位構詞生成 案例

這個是一個很實用的案例,之前想將多個pyload 以不同位置組合成一個整體,就遇到這個難題。

image

請寫出程式碼:

todo

相關文章