Complexity

Lecturer: 22527 Brine

Difinition

演算法的優劣

值得討論的面向

  • 可以從很多面向討論一份程式碼的優劣:
    • 結果的正確性
    • 執行的速度
    • 使用的記憶體大小
    • 可讀性
    • et cetera
  • 在程式執行結果如預期的的條件下,最重要的就是執行時間

你的程式有多快

  • 如何判斷我們的程式執行速度?
  • 如果沒有評測系統呢?
    • 自己來!

執行時間

import time
import random

length = int(input("Length of array: "))
arr = [random.random() for i in range(length)]

t0 = time.time() * 1000

arr = arr.sort()

t = time.time() * 1000

print(f"array sorted in {t - t0:.3f} ms.")

計算執行時間的優點

  • 以下不是專有名詞
    • 真實性
    • 具體性

計算執行時間的優點

  • 沒有一般性
    • 每次執行時間不盡相同
    • 每臺電腦執行時間不盡相同
    • 不同輸入內容執行時間不盡相同
  • 無法比較
    • 在這樣的情況下,比較還有意義嗎?
    • 超級電腦 vs. 馬鈴薯
  • 我們需要一個更公平的衡量方法

以執行步驟衡量

  • 什麼東西不受硬體限制
    • 演算法執行的步驟!
  • 我們假設演算法的執行步驟與輸入資料量大小相關
    • 定義在輸入量為 \(n\) 的情況下,執行步驟數為 \(T(n)\)

數數

  • 如何計算 \(T(n)\)
    • 沒有什麼捷徑,用數的,把每個步驟數出來
  • 什麼是「步驟」
    • 這裡先定義所有基本運算皆為一個「步驟」
    • 宣告、指派、比較……

舉個例子

  • 數數看他有幾個步驟
def summation(arr):
    sum = 0
    for n in arr:
        sum += n
    return sum

舉個例子

  • 數數看他有幾個步驟
def summation(arr):
    sum = 0
    for n in arr:
        sum += n
    return sum
  • 宣告 sum \((1)\)
  • 宣告 n \((1)\)
  • 讓 n 依次當 arr 中每個元素 \((n)\)
  • 將 sum 加上 n \((n)\)
  • 回傳 sum \((1)\)
  • \(T(n) = 2n + 3\)

數數的缺點

  • 很煩
  • 會漏算
  • 有些項其實沒那麼重要?
  • 以剛剛的演算法作為例子
    • \(T(n) = 2n + 3\)
    • 當 \(n = 10\) 的時候,\(3\) 占了總步驟的 \(\frac{3}{23}\)
    • 當 \(n = 7122\) 的時候呢
    • 當 \(n = 271828182\) 的時候呢
  • 當 \(n\) 夠大的時候,我們就不在乎比較小的項數了

來,集合了!

  • 我們不如把在 \(n\) 很大的時候相近的 \(T(n)\) 分在同一個集合
  • 如何定義兩個函數「相近」
    • 在 \(n\) 趨近於 \(\infty\) 時兩者的比值趨近一常數 \(c\)
    • \(eg.\ f(n) = 1n^2 + 2n + 5,\ \ g(n) = 2n^2 + 2n + 5\)
\frac{1n^2 + 2n + 5}{2n^2 + 2n + 5}
= \frac{1}{2}
(as\ n \rightarrow \infty)
  • 可以記作 \(\displaystyle \lim_{n \rightarrow \infty} \frac{f(n)}{g(n)} = \frac{1}{2}\)
  • \(f(n)\) 和 \(g(n)\) 相近
  • 如何幫這些集合取名字?

Big O notation

  • 常見的集合的符號(有別的寫法)
    • 自然數 \(\mathcal N\)
    • 整數 \(\mathcal Z\) 
    • 實數 \(\mathcal R\)
  • 表達複雜度的時候,我們會用 \(\math O\)!
    • \(\mathcal O(f(n))\) 代表所有和 \(f(n)\) 相近的函數的集合
      • 若 \(T(n)\) 和 \(f(n)\) 相近,則 \(T(n) \in \mathcal O(f(n))\)
    • 例子:\(g(n) = n^2 + 3\)
    • \(g(n) \in \mathcal O(n^2)\)
    • \(g(n) \in \mathcal O(n^3)\) ???

相近,倒過來

  • 在剛剛的例子中,把兩式關係倒過來就不一定成立了
  • 回顧一下
    • 兩函數 \(f(n),\ g(n)\)
    • 在 \(n \rightarrow \infty\) 時,若 \(\displaystyle \frac{f(n)}{g(n)}\) 趨近一個常數 \(c\),則 \(f(n)\) 和 \(g(n)\) 相近
  • 如果把 \(f(n)\) 和 \(g(n)\) 交換呢?
    • 若 \(c = 0\) 時代表什麼
      • \(\deg f(n) < \deg g(n)\)
    • 此時 \(g(n)\) 和 \(f(n)\) 不相近!
  • 那不如不要再講相近了,換點別的說法

\(\mathcal O(f(n))\) 的真實定義

  • 存在 \(n_0, c > 0\) 使得:
    • \(\forall n \ge n_0, |T(n)| \le c \times |f(n)|\)
    • 則 \(T(n) \in \mathcal O(f(n))\)
  • 其他記法 / 念法
    • \(T(n) = \mathcal O(f(n))\)
    • 複雜度屬於 \(\mathcal O(f(n))\)
    • 複雜度是 \(\mathcal O(f(n))\)
    • etc.

誰是 \(f(n)\)

  • 根據定義,他可以是任何函數?
    • 不是不行,但沒有必要
    • 我們希望算式越簡潔越好
    • 當個好人 \(f(n)\) 只寫 \(n\) 的最高次項(成長最快的)

技術上來說

  • 回想一下,我們剛寫到 \(n^2 \in \mathcal O(n^3)\)
    • 這是對的
    • 沒有要求抓嚴格上界
    • 你可以每個演算法複雜度都聲稱是 \(\mathcal O(n!)\)
    • r/technicallythetruth

猜數字遊戲

  • 萬年老題
  • 猜一個 \(1 \sim n\) 的數字
    • 我會告訴你的猜測大於等於或小於答案
  • 怎麼猜?
  • 每次猜中間可以刪掉一半的範圍
  • 最多猜 \(\lceil \log n \rceil\) 次
  • \(\lceil \log n \rceil \le \log n + 1\)
  • 這種作法的複雜度:\(\mathcal O(\log n)\)

不只速度

  • 程式使用的空間也可以用相同的方式表示
  • 空間和額外空間

\(\mathcal O\) 的好朋友(們)

  • \(\Theta(f(n))\)
  • 存在 \(n_0, c_l, c_u > 0\) 使得:
    • \(\forall n \ge n_0, 0 \le c_l \times |f(n)| \le|T(n)| \le c_u \times |f(n)|\)
    • 則 \(T(n) \in \Theta(f(n))\)
  • \(\mathcal O\) 規範了函數的上界,\(\Theta\) 則同時規範了上下界!
  • 其實沒那麼重要
  • 其他:\(o, \Omega, \omega\)
Exercise

練習題

練習判斷複雜度

  • 等一下會抽人上來講,剛沒在聽的可以開始翻簡報了
  • 一樣假設所有基本運算是 \(\mathcal O(1)\)
  • 題目難度會從入門到放棄簡單到困難
    • 如果會做然後怕之後抽到太難的可以先自願

輸入輸出

\(\mathcal O(1)\)

n = int(input())

print(n)

For 迴圈

\(\mathcal O(n)\)

n = int(input())

for i in range(1, n, 100):
    print(i)

插入排序

\(\mathcal O(n^2)\)

def insertionSort(arr):
    length = len(arr)
    for i in range(length):
        j = i
        while j > 0 and arr[j] < arr[j - 1]:
            arr[j], arr[j - 1] = arr[j - 1], arr[j]
            j -= 1
        
    return arr

Python 語法

\(\mathcal O(n \log n)\)

print(sorted([int(x) for x in input().split()]))
  • 假設內建排序函式是 \(\mathcal O(n \log n)\)

排序演算法

  • 排序演算法的複雜度最好就是 \(\mathcal O(n \log n)\)
    • 如果不只要排數字的話
    • 排數字的演算法可以是 \(\mathcal O(n \log C)\)
  • 介紹一個 \(\mathcal O(n \log n)\) 的排序演算法

Merge Sort

  • wtf
def mergeSort(arr):
    if len(arr) <= 1:
        return arr
    
    m = len(arr) // 2
    L, R = mergeSort(arr[:m]), mergeSort(arr[m:])

    ans = []
    while len(L) and len(R):
        if L[0] < R[0]:
            ans.append(L.pop(0))
        else:
            ans.append(R.pop(0))

    return ans + L + R

什麼鬼

  • 先不要看程式碼
  • 假設你有兩個一樣大的陣列,把他們合併並排好要多久
    • \(\mathcal O(n)\)
  • merge sort 就是:
    • 把陣列切成兩半
    • 排序左右
    • 合併

最大公因數

\(\mathcal O(\log(a + b))\) ???

def GCD(a, b):
    if a == 0:
      return b
    return GCD(b % a, a)
Application

實際應用與其他

當你寫了一份程式

  • 想要評估他能不能在一秒內完成
    • 把可能會用到的輸入範圍代入
    • 值大約是 \(10^6 \sim 10^7\) 在多數電腦是安全的
    • 其他語言可以高一點,如 C++ 可以到 \(10^8\) 甚至更高

除此之外呢

  • 啪,沒了
  • 跟朋友炫耀
  • 讓你聽起來充滿知性
  • 在近似的時候把剩下的項用很酷的方法寫下來

複雜度代表一切?

  • 從很多方面來看,其實使用複雜度分析並不是完美的
    • 不能完整代表執行步驟數
    • 每個執行步驟不等價
      • 有聽過位元運算嗎
    • 在資料量很大的情況 \(\not =\) 在資料量不大的情況
      • \(5n^2 \text{ vs. } 30 \times n \log n\)
  • 複雜度只是給你估計用的,實際效率還是需要以測量時間確定
Thank you!

謝謝大家

Made with Slides.com