---
# System prepended metadata

title: '[資演] 1- 時間複雜度與空間複雜度'
tags: [資演]

---

# [資演] 1- 時間複雜度與空間複雜度
###### tags: `資演`

在進入到正題之前，我們先考察一下演算法的定義（出自[維基百科](https://zh.wikipedia.org/wiki/%E7%AE%97%E6%B3%95)）：
> 在數學和電腦科學之中，演算法（algorithm）指一個被定義好的、計算機可施行其指示的**有限步驟或次序**，常用於計算、數據處理和自動推理。演算法是**有效**方法，包含一系列**定義清晰的指令**，並可於**有限的時間及空間**內清楚的表述出來。

在這邊我們特別注意幾個關鍵字：有限的步驟和次序、有效、定義清晰的指令，以及有限的時間及空間。

我們要怎麼衡量一個演算法的好壞呢？要衡量一個演算法的好壞，最基本的兩個指標是這個演算法需要多少計算**時間**，以及他需要用到的儲存**空間**有多少，也就是**時間複雜度**和**空間複雜度**。
那麼，這些複雜度我們要怎麼量測呢？例如在leetcode上提交一個程式碼，並成功編譯後，會顯示出你的程式碼的執行時間和使用的記憶體：

![](https://hackmd.io/_uploads/r15tw68zc.png)

但是，比較常見的情況是，我們希望在執行程式碼之前就先評估演算法的複雜度，再決定要不要採用這個演算法。另外，在不同的機器上執行相同的程式，執行速度也會有差異。因此，我們需要有一個共同的標準來評估演算法的效能。這種時候，就必須使用一些**複雜度分析**的技巧了。

## 時間複雜度
一個演算法所需要的時間，我們通常會用這個演算法所需要的**步驟數**來衡量。這個步驟數要怎麼計算呢？

舉例來說，我們現在要尋訪一個長度為 $n$ 的list，這個list的名稱為`my_list`：
```
for i in range(n):
    print(my_list[n])
```
每次`print`都算是一個步驟的話，這段code需要花費幾個步驟才能完成呢？這很容易看出來，因為`my_list`一共有 $n$ 個element，所以總共需要花費 $n$ 步才能完成尋訪。

再舉一個例子，現在我們有一個長度為 $n$、已經由小到大排列好的list，我們叫它`sorted_list`。我們想使用binary search在裡面檢查一個元素是否存在，**最多**需要花費幾個步驟？

我們先複習一下binary search。以下是binary search的pseudocode：
```
left = 左邊的邊界
right = 右邊的邊界
while (左比右小，代表邊界還正常) {
    mid = 左右邊界的中間點
    if (數列中間點的數值大於 target) {
      右半邊可以不用看了，因此把右邊邊界移到中間
    } else if (數列中間點的數值小於 target) {
      左半邊可以不用看了，因此把左邊邊界移到中間
    } else {
      找到答案了！
    }
}
```

:::spoiler 用Python寫出來是這個樣子的
    def binary_search(sorted_list, target):
        #設置選取範圍的指標
        left = 0
        right = len(sorted_list) - 1
        while left <= right:
            mid = (left + right) / 2        #取中間索引的值
            if sorted_list[mid] < target:   #若搜尋值比中間的值大，將中間索引+1，取右半
                left = mid + 1
            elif sorted_list[mid] > target: #若搜尋值比中間的值小，將中間索引+1，取左半
                right = mid - 1
            else:                           #若搜尋值等於中間的值，則回傳
                return mid
        return -1


:::

在binary search中，需要花最多時間的狀況，是要查找的element不存在的情況。這種時候需要花費多少步驟呢？假設`sorted_list`的長度 $n = 8$，並且要查找的element=比所有`sorted_list`裡面的element還要大：
* 第一次查找
    `left = 0`
    `right = 7`
    `mid = 3`
    --> `target > mid`
    --> 選擇右半邊
* 第二次查找
    `left = 4`
    `right = 7`
    `mid = 5`
    --> `target > mid`
    --> 選擇右半邊
* 第三次查找
    `left = 5`
    `right = 7`
    `mid = 6`
    --> `target > mid`
    --> 選擇右半邊
    --> `target > sorted_list[7]`
    --> **找不到**

我們可以發現，如果`sorted_list`的長度是8，尋找一個element所需要花費的最大步驟就是3次：
$$
2^3 = 8
$$或者是
$$
\log _2 8 = 3
$$也就是說，binary search的複雜度是$\log n$。

## Big-O notation
剛才我們粗略介紹了一下時間複雜度的概念，現在我們要來正式定義它。

複雜度分析的notation有三種：
* $Ο$ (Big-O)
    演算法時間函式的上限（upper bound）。
* $\Omega$ (Omega)
    演算法時間函式的下限（lower bound）。
* $\Theta$ (Theta)
    演算法時間函式的上限與下限，也就是 $O =\Omega=\Theta$ 的時候。

舉例來說，前面提到的尋訪演算法，最多需要$n$步，所以它的複雜度為 $O(n)$。另一方面，binary search的複雜度是$O(\log(n))$。你可能會想問，如果要找的element剛好在第一次查找的`mid`的位置，那就只需要一步，也就是說，binary search是一個$\Omega(1)$的演算法？然而這樣說是不對的！參考[這篇文章](https://stackoverflow.com/questions/9459507/complexity-of-binary-search)，正確的說法應該是：
* 對於最佳狀況，你最少需要$\Omega(1)$次查找，最多也只需要$O(1)$次查找，所以binary search的最佳狀況複雜度是$\Theta(1)$
* 對於最差狀況，你最少需要$\Omega(\log n)$次查找，最多也只需要$O(\log n)$次查找，所以binary search的最差狀況複雜度是$\Theta(\log n)$
    
也就是說，最佳狀況和最差狀況必須**分開檢視**，不能混為一談！

## 複雜度的計算
事實上剛才介紹的這組 big-O notation都是屬於一種漸進符號（asymptotic notation），嚴謹一點的定義可以參考[這篇文章](http://alrightchiu.github.io/SecondRound/complexityasymptotic-notationjian-jin-fu-hao.html)，在這邊我們只介紹粗略的計算方式。

首先，在計算big-O時，我們只看增長最快的那項，並且忽略一切常數factor。例如 $3x^3 + 4x^2 + 2x + 4$，我們可以說它等於$O(x^3)$。至於增長速度怎麼看呢？很明顯的，對於多項式，項次越大增長速度越快，複雜度也就越高。增長最快的是$O(a^n)$，其中$a$是常數，也就是所謂的指數增長。接著是多項式$O(n^k)$，其中$k>1$。再來是線性$O(n)$。$O(\log n)$ 增長速度又比線性再更慢。可以把big-O notation想成是一種分等級的方式，指數增長是一種複雜度等級，再來多項式又是一種複雜度等級...等。

![](https://hackmd.io/_uploads/HJu5eDOf5.png)


Big-O notation 有幾個重要的數學性質：

* 加法
若 $f_1(n)$ 為 $O(g(n))$ 且 $f_2(n)$ 為 $O(g(n))$，則 $f_1(n) + f_2(n)$ 也是 $O(g(n))$。
意思是說，兩個屬於相同複雜度等級的函數相加，也會是那個相同的複雜度等級。
* 常數factor
若 $f_1(n)$ 為 $O(g(n))$，則對於任意常數$k$乘以 $f_1(n)$ 也是 $O(g(n))$。
意思是說，乘以常數不會改變一個函數的複雜度等級。
* 乘法
若 $d(n)$ 為 $O(f(n))$，而 $e(n)$ 為 $O(g(n))$，則$d(n)\cdot e(n)$ 為 $O(f(n) \cdot g(n))$。
意思是說，兩個函數相乘的複雜度，相當於其個別複雜度的相乘。

更多 big-O notation 的數學性質可以參考[這份講義](https://www.cs.mcgill.ca/~blanchem/250/2017/Lecture12_handouts.pdf)。

## 空間複雜度
空間複雜度和時間複雜度的計算方式相同。通常空間複雜度和時間複雜度是可以trade off的，也就是說，我們可以用高一點的空間複雜度來換取低一點的時間複雜度，反之亦然。在記憶空間相對便宜的現代，我們更傾向於用以空間換取時間。一個很好的例子是排序演算法。排序演算法有很多種，每種各有其優缺，這就是因為不同的演算法做了不一樣的trade off。