Recursion

遞迴的基本概念就是:

To solve a problem, solve a subproblem that is a smaller instance of the same problem, and then use the solution to that smaller instance to solve the original problem.

最後可以整理出兩個重點:

  1. Each recursive call should be on a smaller instance of the same problem, that is, a smaller subproblem.
  2. The recursive calls must eventually reach a base case, which is solved without further recursion.

簡單來說:

  1. 決定基本情況 (Base Case)
    • 此 base case 即為遞迴的終止條件
  2. 決定一般情況 (General case)
    • 産生遞迴呼叫的指令碼,即遞迴關係式
  3. 將上述兩種情況寫入演算法
    Procedure 遞迴程式名(參數)
    {
    if (Base case)
      return(結果); ……//達到終止條件時結束遞迴,需要時回傳結果
    else
      General case; ……//利用general case執行遞迴呼叫,需要時加上return
    }

議題

  1. 階乘(Factorial;n!)
  2. 費氏數(Fibonacci Number)
  3. 兩數之最大公因數 (Greatest common divisor; G. C. D.)
  4. 二項式係數 (Binomial Coefficient;或稱組合問題 “Combination of n objects”)
  5. Ackerman’s Function
  6. 迴文(palindrome)
  7. 河內塔問題(Tower of Hanoi)
  8. 排列問題(Permutation)

遞迴常見的課題:

  • 寫一個Algorithm (or Program)
  • 追蹤一個Recursion Algorithm (找結果、呼叫次數)

階乘(Factorial;n!)

定義

階乘(n!) = 1 × 2 × 3 × … ×(n-1) × n

解法

  • 數學表示式:
  • pseudocode:
    Input: 所要求算的階乘數値n
    Output: n! 結果回傳
    Procedure Factorial(int n)
      begin
      if (n = 0)
        return 1;
      else
        return (n× Factorial(n-1));
      end
  • Factorial(3) = ? 呼叫了幾次函數? 呼叫了 4 次 (含Factorial(3)),計算結果為 6 ※ Factorial(n) 會被呼叫幾次? 呼叫 n+1 次
  • Iterative Factorial Algorithm 的 pseudocode
    Input: 所要求算的階乘數値n
    Output: n! 結果回傳
    Procedure Factorial(int n)
      begin
      i = 1;
      factN = 1;
      loop (i ≤ n)
        factN = factN * i;
        i = i +1;
      end loop
      return factN;
      end
  • python code
    def factorial(num: int) -> int:
        if num < 0:
            raise ValueError("Number should not be negative.")
    
        return 1 if num in (0, 1) else num * factorial(num - 1)

費氏數(Fibonacci Number)

定義

n 0 1 2 3 4 5 6 7 8 9 10
0 1 1 2 3 5 8 13 21 34 55

解法

  • 數學表示式:
  • pseodocode
    Input: 費氏數値num
    Output: 回傳Fibonacci number結果
    Procedure Fib(int num)
      begin
      if (num is 0 OR num is 1) //Base Case
        return num;
      else
        return (Fib(num-1) + Fib(num-2));
      end
  • Iterative Fibonacci Number Algorithm pseodocode
    Input: 費氏數値num
    Output: 回傳Fibonacci number結果
    Procedure Fib(int n)
    begin
      if (num is 0 OR num is 1)
        return num;
      else begin
        Fa = 0,Fb = 1;
        for(i = 2 to n) = 2 to n)
          Fc = Fa + Fb;
          Fa = Fb;
          Fb = Fc;
        end for;
        return Fc;
      end if;
    end
  • python code
  1. """
    This program calculates the nth Fibonacci number in O(log(n)).
    It's possible to calculate F(1_000_000) in less than a second.
    """
    import sys
    
    def fibonacci(n: int) -> int:
        if n < 0:
            raise ValueError("Negative arguments are not supported")
        return _fib(n)[0]
    
    # returns (F(n), F(n-1))
    def _fib(n: int) -> tuple[int, int]:
        if n == 0:  # (F(0), F(1))
            return (0, 1)
    
        # F(2n) = F(n)[2F(n+1) − F(n)]
        # F(2n+1) = F(n+1)^2+F(n)^2
        a, b = _fib(n // 2)
        c = a * (b * 2 - a)
        d = a * a + b * b
        return (d, c + d) if n % 2 else (c, d)
  2. class Fibonacci:
        def __init__(self, N=None):
            self.fib_array = []
            if N:
                N = int(N)
                self.fib_array.append(0)
                self.fib_array.append(1)
                for i in range(2, N + 1):
                    self.fib_array.append(self.fib_array[i - 1] + self.fib_array[i - 2])
            elif N == 0:
                self.fib_array.append(0)
            print(self.fib_array)
    
        def get(self, sequence_no=None):
            """
            >>> Fibonacci(5).get(3)
            [0, 1, 1, 2, 3, 5]
            [0, 1, 1, 2]
            >>> Fibonacci(5).get(6)
            [0, 1, 1, 2, 3, 5]
            Out of bound.
            >>> Fibonacci(5).get(-1)
            [0, 1, 1, 2, 3, 5]
            []
            """
            if sequence_no is not None:
                if sequence_no < len(self.fib_array):
                    return print(self.fib_array[: sequence_no + 1])
                else:
                    print("Out of bound.")
            else:
                print("Please specify a value")
    
    
    if __name__ == "__main__":
        print("\n********* Fibonacci Series Using Dynamic Programming ************\n")
        print("\n Enter the upper limit for the fibonacci sequence: ", end="")
        try:
            N = int(input().strip())
            fib = Fibonacci(N)
            print(
                "\n********* Enter different values to get the corresponding fibonacci "
                "sequence, enter any negative number to exit. ************\n"
            )
            while True:
                try:
                    i = int(input("Enter value: ").strip())
                    if i < 0:
                        print("\n********* Good Bye!! ************\n")
                        break
                    fib.get(i)
                except NameError:
                    print("\nInvalid input, please try again.")
        except NameError:
            print("\n********* Invalid input, good bye!! ************\n")
    
        import doctest
    
        doctest.testmod()

兩數之最大公因數 (Greatest common divisor; G. C. D.)

定義

省略。

解法

  • 利用輾轉相除法,如下圖所示:
  • pseudocode
    Input: 輸入二個整數A與B
    Output: 傳回A與B的最大公因數
    Procedure gcd(int A, int B)
      begin
      if (A mod B = 0)       //Base Case
        return B;
      else
        return gcd(B, A mod B);
      end
  • PS. 不用考慮 A < B 的問題。因為假如 A < B :gcd(A, B) = gcd(B, A),等於前面多 call 一次遞迴

二項式係數 (Binomial Coefficient;或稱組合問題 “Combination of n objects”)

定義

組合問題的公式如下:

解法

本題解法思路不需要去管,因為最後遞迴關係式式數學家推導出來的:

  • 數學表示式:

  • pseudocode

    Input: 輸入二項式的n與m
    Output: 傳回二項式計算結果
    Procedure Bin(int n, int m)
      begin
      if (n=m or m=0)      //Base Case
        return 1;
      else
        return (Bin(n-1, m) + Bin(n-1, m-1));
      end

Ackerman’s Function

定義

省略。

解法

  • 數學表示式
  • pseudocode
    Input: 輸入Ackerman(M, N)
    Output: 傳回Ackerman(M, N)計算結果
    Procedure Ackerman(M, N)
      begin
      if (M=0)         //Base Case
        return N+1;
      elif (N=0)
        return Ackerman(M-1, 1)
      else
        return Ackerman(M-1, Ackerman(M, N-1))  
      end
  • 追蹤遞迴

河內塔問題(Tower of Hanoi)

定義

河內塔也是個經典的 Recursion 問題。因為此屬簡單問題所以簡單介紹。 河內塔目標是把 n 個盤子從 A 柱移動到 B 柱(如下圖 n = 5),且遵循以下兩個規則:

  1. 一次只能移動一個盤子
  2. 較小的盤子必須在較大的盤子上面

傳說在亞洲某個地方盛傳一個傳說,如果把當地 64 個盤子成功移動到另一個柱子,世界末日即將到來!

解法

因為很簡單所以直接說解法了。 觀察:

  1. n = 1,只需要一次(直接把盤子放到B柱)
  2. n = 2,需要三次(如下圖)
  3. n = 3,需要 3+1+3 次(把 2 個盤子從 A 柱放到 C 柱,移動最大盤從 A 到 B 柱,把 2 個盤子從 C 放到 B 柱)
  • 數學表示式:

    1. Base Case: if n = 1, 移動一次
    2. General Case: when n >=2, 解問題按照下列三步驟:
      1. 遞迴解決子問題n-1移動次數
      2. 移動最大盤
      3. 遞迴解決子問題n-1移動次數
  • Recursive Tower of Hanoi Algorithm pseudocode
    Input: n個盤子,三根柱子(A, B, C)
    Output: 移動過程
    Procedure Towers(int n, char A, char C, char B)
    begin
      if (n > 0)
        Towers(n Towers(n-1, A, B, C); 1, A, B, C);
        print(“Move disk”, n, “from”, A, “to”, C);
        Towers(n Towers(n-1, B, C, A);
    end
  • python code

    def moveTower(height,fromPole, toPole, withPole):
        if height >= 1:
            moveTower(height-1,fromPole,withPole,toPole)
            print("moving disk from",fp,"to",tp)
            moveTower(height-1,withPole,toPole,fromPole)
    moveTower(3,"A","B","C")

時間複雜度

T(n) = T(n-1) + 1 + T(n-1) = 2T(n-1) + 1 = 2[2T(n-2) + 1] + 1 = 4T(n-2) + 3 = ... = =

由上可知一個遞迴演算法會包含兩個部份:

  1. 遞迴呼叫的執行次數 : 2T(n-1)
  2. 非遞迴工作的執行次數 : 1

回到剛剛說世界末日的故事,移動64個盤子需要次,假設每秒可以移動一個盤子,則大概需要5845億年來移動,還沒到那時太陽的壽命已經消耗殆盡了!

Permutation (排列問題)

定義

給予 a,b,c 三個字元,請印出所有的 Permutation。

  • 共有3! = 6 種排列

解法

  • 數學表示式
  • pseudocode
    void perm(list[ ], int i, int n)   // 此函式可產生從 list[i] 至 list[n] 的元素排列
    {
      if (i==n)                        // 當 list[ ] 中的所有元素排列完畢,即:終止條件
      {
        for (j=1; j<=n; j++)
          print(list[j]);              // print list內排列好的結果
      }
    else                               // 當 list[ ] 中的元素尚未排列完畢,即:遞迴關係式
      {
        for(j=i; j<=n; j++)
          {
            swap(list[i], list[j]);    // 讓某個字元當頭(第j個字元當頭)
            perm(list[ ], i+1, n);     // 從第i字元後的其他字元,自己去做排列
            swap(list[i], list[j]);    // 將list陣列還原
          }
      }
    }

Recursion 與 Non-recursion 的比較

上圖說明:

  • 區域變數指的是程式碼而非執行時期的
  • Storage空間指的是程式碼的儲存空間而非執行時期的

Memoization 優化

考慮 Fibonacci 數列:

  1. If n is 0 or 1, return n
  2. Otherwise, return fibonacci(n-1) + fibonacci(n-2)

當 n = 5 時, the computer makes 15 calls:

隨著 n 越來越大,fibonacci(30) 在遞迴時 call 了 fibonacci(2) 超過 50 萬次。 > 所以我們可以實做一個 lookup table 紀錄算過的 fibonacci(input),當然這個 table 要有效率像是 hash table 擁有 查找時間。當然會犧牲一些 space 存放 result(類似快取)。

  • if n == 0 or 1, return n
  • if n in the table, return the table's value for n
  • Otherwise
    • 計算 fibonacci(n-1) + fibonacci(n-2)
    • 儲存 result 在 table
    • return result

如上表所示,隨著 n 越來越大 function call 以指數增加,但是佔用的 space 卻是線性增加。

Bottom-up

有時候改善 recursive algorithm 最有效的方法就是不使用 recursion。

在這個 Fibonacci numbers 例子中,一個 iterative 技巧叫做 bottom-up approach 可以同時節省 time 和 space。當使用 bottom-up approach 時,會先解決子問題然後使用部份解去得出最終解。

  • time complexity:
  • space complexity: since it only remembers three numbers at a time.

以下是pseudocode:

  • if n == 0 or 1, return n
  • Otherwise
    • Create variable twoBehind to remember the result of (n-2) and initialize to 0
    • Create variable oneBehind to remember the result of (n-1) and initialize to 1
    • Create variable result to store the final result and initialize to 0
    • Repeat (n-1) times:

      • Update result to the sum of twoBehind plus oneBehind
      • Update twoBehind to store the current value of oneBehind
      • Update oneBehind to store the current value of result
      • return result

初探 Dynamic programming

上述的 Memoization 和 bottom-up 都是使用了 dynamic programming 策略。

當一個問題具有* *optimal substructureoverlapping subproblems** 特性時,Dynamic programming 則可以被使用。

  • Optimal substructure: the optimal solution to the problem can be created from optimal solutions of its subproblems.
    • fib(5) can be solved with fib(4) and fib(3)
  • Overlapping subproblems: whenever a subproblem is solved multiple times.
    • fib(5) made multiple calls to the typically recursive fib(3) and fib(2)

總結

當以下的問題有任何一個為 當以下的問題有任何一個為 no,則不該使用遞迴來設計演算法 用遞迴來設計演算法:

  1. 問題的演算法或資料結構本身是否就具備 問題的演算法或資料結構本身是否就具備遞迴的特性?
  2. 使用遞迴求解問題是否能 使用遞迴求解問題是否能更簡化或是更易了解?
  3. 使用遞迴求解問題是否能在 使用遞迴求解問題是否能在可接受的時間記憶體空間限制下完成?

參考資料