斐波那契數列的計算在程式設計中是一個經典問題,其計算效率的提升一直是演算法最佳化的重點。傳統的遞迴或迭代方法,時間複雜度較高,尤其在計算較大項數時,效率明顯下降。本文介紹的矩陣乘法結合二進位制分解的演算法,能有效降低時間複雜度至 O(log n),顯著提升計算效率。同時,文章也探討了不同實作版本的效能差異,例如使用查表法、記憶化技術等最佳化技巧,並分析了各種最佳化策略的優缺點,讓讀者對斐波那契數列的計算有更全面的理解。

使用矩陣乘法高效計算斐波那契數列

斐波那契數列是一種經典的數學序列,其定義為:每個數字都是前兩個數字的和(1, 1, 2, 3, 5, 8, 13, …)。在電腦科學中,計算斐波那契數列是一個常見的問題,尤其是在探討演算法效率時。

矩陣表示與斐波那契數列

我們可以使用矩陣乘法來高效地計算斐波那契數列。定義一個2x2的矩陣 $A = \begin{bmatrix} 1 & 1 \ 1 & 0 \end{bmatrix}$,則有: [ A^n = \begin{bmatrix} F(n+1) & F(n) \ F(n) & F(n-1) \end{bmatrix} ] 其中,$F(n)$ 表示第 $n$ 個斐波那契數。

內容解密:

  • 矩陣 $A$ 的定義使得其 $n$ 次方的右上角元素恰好是第 $n$ 個斐波那契數。
  • 這種表示方法允許我們利用矩陣乘法的性質來計算斐波那契數列。

二進位制分解與快速計算

為了高效計算 $A^n$,我們採用二進位制分解的方法。這種方法的核心思想是將 $n$ 表示為二進位制數,然後利用矩陣乘法的結合律來減少計算次數。

例如,要計算 $A^{23}$,首先將 23 表示為二進位制:$23 = 10111_2$。然後,反轉這個二進位制數得到 11101。接著,計算 $A^{2^0}, A^{2^1}, A^{2^2}, A^{2^3}, A^{2^4}$,並將對應於 1 的矩陣相乘:$A^{2^0} \times A^{2^2} \times A^{2^3} \times A^{2^4}$。

def mul(A, B):
    a, b, c, d = A
    e, f, g, h = B
    return a*e+b*g, a*f+b*h, c*e+d*g, c*f+d*h

def fibIII(n):
    A = [1,1,1,0]  # 斐波那契矩陣
    X = [1,0,0,1]  # 單位矩陣
    s = str(bin(n))[2:][::-1]  # 將 n 轉換為二進位制並反轉
    for i in range(len(s)):
        if s[i] == '1':
            X = mul(X, A)
        A = mul(A, A)
    return X[1]

內容解密:

  • mul(A, B) 函式用於計算兩個 2x2 矩陣的乘積。
  • fibIII(n) 函式透過二進位制分解的方法計算第 $n$ 個斐波那契數。
  • n 轉換為二進位制並反轉,以便根據二進位制位的值決定是否將對應的 $A^{2^i}$ 累乘到結果矩陣 X 中。
  • 在每次迭代中,將 A 自乘以計算下一個冪次。

為什麼這種方法更高效?

這種方法的時間複雜度遠低於直接遞迴或迭代計算斐波那契數列。對於計算第 $n$ 個斐波那契數,主要的開銷在於計算 $A^n$。透過二進位制分解,我們將計算次數從 $O(n)$ 降低到 $O(\log n)$,因為我們只需要進行 $\log_2 n$ 次矩陣乘法。

不同實作版本的比較

文中提供了多個版本的 fib 函式實作,包括 fibIIIfibIIfibI。這些實作在基本思路上是一致的,但具體實作細節有所不同。比較這些實作的執行效率,可以發現 fibI 的效能最佳。

內容解密:

  • fibIIIfibIIfibI 的主要區別在於矩陣乘法的實作方式和變數的使用。
  • fibI 直接展開了矩陣乘法的運算,避免了函式呼叫的開銷,因此效率最高。

斐波那契數列的計算最佳化

斐波那契數列是一種廣泛應用於數學、電腦科學和物理學等領域的數列。它的計算方法有很多種,但不同的實作方式會對效能產生巨大的影響。本篇文章將探討幾種不同的斐波那契數列計算方法,並分析其優缺點。

初始實作與最佳化需求

最初的斐波那契數列計算函式 fibIfibII 展示了遞迴和迭代兩種基本方法。然而,這些方法存在效率問題,尤其是當 n 很大時。為瞭解決這個問題,我們需要對演算法進行最佳化。

矩陣運算與公式推導

透過分析斐波那契矩陣,可以推匯出兩個重要的公式:

  • fib(2*k) = fib(k)*(2*fib(k+1)-fib(k))
  • fib(2*k+1) = fib(k+1)**2 + fib(k)**2

這些公式為最佳化斐波那契數列的計算提供了理論基礎。

最佳化實作:fibJJfibJ

利用上述公式,我們可以實作更高效的斐波那契數列計算函式 fibJJfibJ。其中,fibJ 透過使用查表法(lookup table)將計算時間從3158秒降低到5秒。

程式碼解析:fibJ

def fibJ(n): 
    if n < 18:
        return [0,1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597][n]
    if (n%2) == 0:
        k = n//2
        f = fibJ(k)
        g = fibJ(k+1)
        return f*(2*g-f) 
    k = (n-1)//2
    f = fibJ(k)
    g = fibJ(k+1)
    return g*g + f*f 

內容解密:

  • 使用查表法處理小於18的 n 值,以避免不必要的遞迴計算。
  • 根據 n 的奇偶性,使用不同的公式計算斐波那契數。
  • 遞迴呼叫 fibJ 來計算 fg,然後根據公式傳回結果。

進一步最佳化:fibK 與記憶化技術

為了進一步提高效能,我們引入了記憶化技術(memoization),實作了 fibK 函式。記憶化技術透過儲存已經計算過的值,避免了重複計算,從而大幅提高了計算效率。

程式碼解析:fibK

def fibK(n, dict = {}): 
    if n < 18:
        return [0,1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597][n]
    if (n%2) == 0:
        k = n//2
        if k not in dict:
            dict[k] = fibK(k, dict)
        A = dict[k]
        if (k+1) not in dict:
            dict[k+1] = fibK(k+1, dict)
        B = dict[k+1]
        return 2*A*B-A*A
    else:
        k = (n-1)//2
        if (k+1) not in dict:
            dict[k+1] = fibK(k+1, dict)
        A = dict[k+1]
        if k not in dict:
            dict[k] = fibK(k, dict)
        B = dict[k]
        return A*A + B*B

內容解密:

  • 使用字典 dict 儲存已經計算過的斐波那契數,以避免重複計算。
  • 根據 n 的奇偶性,使用不同的公式計算斐波那契數,並利用儲存的值進行計算。

函式設計的藝術:單一任務與多功能函式的取捨

在程式設計中,如何有效地使用函式是一門藝術。大部分時候,我們希望建立的函式能夠專注於完成單一任務,避免過於複雜的多功能函式。然而,在某些情況下,將多個相關任務合併到一個函式中也可能是合理的。

多功能函式的例子:影像處理

以下是一個將彩色影像和灰階影像處理合併為一個函式的例子:

WIDTH = 512
HEIGHT = 512

class ImageFrame:
    def __init__(self, colors, wd=WIDTH, ht=HEIGHT, colorFlag=False):
        self.img = PhotoImage(width=wd, height=ht)
        for row in range(ht):
            for col in range(wd):
                num = colors[row*wd + col]
                if colorFlag:
                    kolor = '#%02x%02x%02x' % (num[0], num[1], num[2])  # 彩色
                else:
                    kolor = '#%02x%02x%02x' % (num, num, num)  # 灰階
                self.img.put(kolor, (col, row))
        c = Canvas(root, width=wd, height=ht); c.pack()
        c.create_image(0, 0, image=self.img, anchor=NW)
        printElapsedTime('displayed image')

內容解密:

  • colorFlag 引數用於控制是否處理為彩色或灰階影像。
  • colorFlagTrue 時,影像被處理為彩色;否則,被處理為灰階。
  • 這種設計提高了程式碼的複用性,但也增加了理解和維護的複雜度。

單一任務與多功能函式的取捨

在上述例子中,將彩色和灰階處理合併為一個函式雖然減少了程式碼的重複,但也引入了一個額外的引數 colorFlag,使得函式的理解和除錯變得更加困難。

優缺點分析:

  1. 合併為一個函式

    • 優點:減少了程式碼的重複,符合 DRY(Don’t Repeat Yourself)原則。
    • 缺點:引入額外的引數,增加了理解和除錯的難度。
  2. 分開為多個函式

    • 優點:每個函式功能單一,易於理解和除錯。
    • 缺點:可能導致程式碼重複,需要在多個地方進行相同的修改。

最佳實踐

Ward Cunningham 的一句名言精闢地總結了這個問題:「If you don’t have a good understanding of the code, you’re not going to be able to make it better.」這意味著,無論選擇哪種方案,開發者都需要對程式碼有深刻的理解。

在實際開發中,我們應該根據具體情況進行權衡:

  • 如果程式碼足夠簡單,並且合併後不會顯著增加複雜度,那麼合併為一個函式是合理的。
  • 如果程式碼較為複雜,或者未來可能需要對不同功能進行不同的修改,那麼分開為多個函式會更有利於維護。

函式設計的藝術:長度、簡潔性與可讀性

在程式設計的世界裡,函式(Function)是建構程式的基本單立。一個好的函式設計不僅能提高程式碼的可讀性,也能讓維護和擴充變得更加容易。那麼,到底該如何設計一個好的函式呢?

函式的長度:越短越好?

Brian Kernigham 和 P.J. Plauger 曾經提到,他們所寫的函式中位數是 15 行,平均值是 19 行。這個數字告訴我們,函式的長度並不是越短越好,而是應該根據實際需求來設計。一般來說,一個函式不應該超過螢幕所能顯示的行數(約 38 行),但更重要的是追求可讀性,而不是簡單地追求短小。

以 Sudoku 解題為例

以下是一個檢查 Sudoku 解是否正確的函式範例,共 34 行:

def solutionIsCorrect(matrix):
    #
---
Build lists of rows and columns.
    rows = [[]] * MAX
    cols = [[]] * MAX
    for r in range(MAX):
        for c in range(MAX):
            rows[r].append(matrix[r][c].value)
            cols[c].append(matrix[r][c].value)
    #
---
Build list of blocks.
    block = []
    for n in range(MAX):
        block.append([])
    for n in range(MAX):
        for r in range(blockHeight):
            for c in range(blockWidth):
                row = (n//blockWidth)*blockHeight+r
                col = (n%blockHeight*blockWidth) +c
                block[n].append(matrix[row][col].value)
    #
---
Check all rows for all n digits.
    for r in rows:
        for n in range(1, MAX+1):
            if {n,} not in r: 
                return False
    #
---
Check all columns for all n digits.
    for c in cols:
        for n in range(1, MAX+1):
            if {n,} not in c:
                return False
    #
---
Check all blocks for all n digits.
    for b in block:
        for n in range(1, MAX+1):
            if {n,} not in b:
                return False
    return True 

內容解密:

  1. 初始化行與列的列表:使用 rowscols 來儲存 Sudoku 矩陣中的每一行和每一列的值。
  2. 建立區塊列表:將 Sudoku 矩陣劃分為多個區塊,並將每個區塊的值儲存在 block 列表中。
  3. 檢查每行、每列和每個區塊:遍歷 rowscolsblock,確保每個數字從 1 到 MAX 都出現一次。
  4. 傳回檢查結果:如果所有檢查都透過,則傳回 True,否則傳回 False

這個函式雖然稍長,但它的每個部分都相對簡單,且有清晰的註解說明。這種設計使得程式碼易於理解和維護。

為什麼不將小部分拆成獨立函式?

在這個範例中,每個部分都相對簡單,不太需要進一步拆分獨立的函式。將這些部分放在一起,有助於保持程式碼的連貫性和可讀性。這裡選擇了內聚(Cohesion)而非解耦(Decoupling),因為這樣可以減少複雜度。

一行函式的價值

有時候,我們會寫一些只有一行的函式,例如:

def triangleHasNotConverged(count, A, B, C): 
    return (count < MAX_TRIANGLE_COUNT and
            SMALLEST_TRIANGLE_SIZE < max(B.dist(C), A.dist(B), A.dist(C)))

這類別函式雖然簡單,但它提供了更好的可讀性。相比於直接寫出複雜的布林表示式,使用一個具有描述性的函式名稱可以讓程式碼更容易理解。

如何選擇合適的函式設計?

考慮到計算兩點之間的距離,我們有三種不同的實作方法:

方法 1:兩個獨立函式

def distance2D(x,y):
    assert len(x) == len(y) == 2
    return sqrt( (x[0]-y[0])**2 + (x[1]-y[1])**2 )

def distance3D(x,y):
    assert len(x) == len(y) == 3
    return sqrt( (x[0]-y[0])**2 + (x[1]-y[1])**2 + (x[2]-y[2])**2 )

方法 2:使用迴圈的單一函式

def distance(x,y):
    assert len(x) == len(y) and len(x) in {2,3}
    total = 0
    for n in range(len(x)):
        total += (x[n]-y[n])**2
    return sqrt( total )

方法 3:使用列表推導式的單一函式

def distance(x,y):
    assert len(x) == len(y) and len(x) in {2,3}
    return sqrt(sum([(x[n]-y[n])**2 for n in range(len(x))]))

方法 4:無迴圈的單一函式

def distance(x,y):
    if len(x) == len(y) == 2:
        return sqrt((x[0]-y[0])**2 + (x[1]-y[1])**2)
    if len(x) == len(y) == 3:
        return sqrt((x[0]-y[0])**2 + (x[1]-y[1])**2 + (x[2]-y[2])**2)
    # 此處應新增錯誤處理,例如:
    else:
        raise ValueError("Unsupported dimension for distance calculation")

內容解密:

  • 方法 1 明確區分了 2D 和 3D 距離計算,但需要維護兩個函式。
  • 方法 2方法 3 使用了迴圈或列表推導式,使函式更通用,但稍微犧牲了可讀性。
  • 方法 4 直接展開計算,避免了迴圈,但需要手動處理不同維度的情況,並且需要適當的錯誤處理機制。

在實際應用中,我們應該根據具體需求選擇最合適的方法。如果預期不會擴充功能,使用簡單直接的方法即可;若需要更高的靈活性,則可以選擇更通用的實作方式。