---
# System prepended metadata

title: 演算法 Algorithm
tags: [知識百寶袋]

---

# 演算法 Algorithm

### 演算法的簡單定義
輸入+演算法=輸出
Input+Algorithm=Output

```
假設：我今天要在2和3之間得到6，那我就要在2和3之間，加上一個*號，就可以讓算式設計成2*3=6
為了得到另外一個東西，我輸入一個東西，這個這可以就叫做演算法
```

`假設：我今天要得到一杯蘋果汁，中間可能經過果汁機，或是用手榨，最後得到一杯蘋果汁，那果汁機或用手，這處理的過程就可以稱作為演算法。`


```
假設：我今天要煮泡麵加一顆蛋，我們輸入水、泡麵、泡麵調味包、一顆蛋，這碗泡麵的演算法可能會類似：

1.滾水
2.泡麵放進滾水中
3.加入調料包
4.打入一顆蛋
5.煮至熟
```

### 時間複雜度(Time Complexity)：評估演算法好壞的工具
評估演算法好壞的工具，最基本的兩個指標是：
- 這個演算法有多快
- 需要用到的記憶體有多少

### 大O符號
我們通常用大O符號(Big O notation)來記錄時間複雜度的快慢

```
假設：今天想要看高年級實習生這部電影，那我可以
1. 打Grab到BGC出租片店，來回車程需要20分鐘
2. 從網路上下載，一部片需要花8分鐘
->那我可能會先選擇2的方案
```

```
假設：今天想要看高年級實習生、幸福綠皮書、Love Rosie
1. 打Grab到BGC出租片店，來回車程需要20分鐘
2. 從網路上下載，一部片需要花8分鐘，所以三部片共耗時24分鐘
```

| | 方案1 | 方案2 |
| -------- | -------- | -------- |
| 租1部片| 打Grab20分鐘|網路下載8分鐘|
| 租10部片| 打Grab20分鐘|網路下載80分鐘|
| 租n部片| 打Grab20分鐘|網路下載8Xn分鐘|

1. 去出租店：所要取得的時間不受想看的電影片數而影響。也就是不管輸入多少需求(input)，輸出(output)所需要的時間不會因此增加。
->O(1)
2. 網路下載：所要取得的時間受想看的電影片數而影響。也就是輸入多少需求(input)，輸出(output)所需要的時間會因這個多少，而增加。
->O(n)

大O符號，是用來描述一個演算法在輸入n個東西時，總執行時間和n的關係
實務上，我們只會紀錄最高次方的那一項，並忽略其所有的係數

### 演算法有多快不是以秒衡量，而是以步驟次數

### 常見的六種時間複雜度與演算法
- O(1)：陣列讀取
- O(n)：簡易搜尋
- O(log n)：二分搜尋
- O(n^2)：選擇排序
- O(nlogn)：合併排序
- O(2^n)：費波那契數列

#### O(1)：陣列讀取
- 代表：不管input多少東西，程式都會在同一時間跑完

fruits=[蘋果,芭樂,葡萄,木瓜] ----->陣列
蘋果 ----->元素
0 ----->索引值

fruits=[蘋果,芭樂,葡萄,木瓜]
這時，我們如果想要知道：水果列這個陣列中任何一個編號所對應到的水果，都只需要把這個索值對應的元素一起印出來，就知道對應的水果是什麼了。

```
n=0;
print(fruits[n]);
>> "蘋果"
```

陣列讀取時，因為我們已經知道櫃子的索引值，不管放入的n是多少，程式都可以在"一個步驟"就到達n所對應到編號的櫃子，並取出該元素->O(1)

#### O(n)：簡易搜尋
- 代表：input n個東西，程式就會跟著輸入的n等比例的增加
例如：n=3，程式就會在3個步驟完成

以fruits=[蘋果,芭樂,葡萄,木瓜]這個陣列來舉例：
例如我目前都不知道各個櫃子中，放的是什麼水果，如果我要知道哪個是木瓜，最壞的方法：就會是，一個櫃子、一個櫃子開，開到是木瓜的櫃子。

- 通常程式步驟的時間複雜度會是用程式執行會碰到的最壞狀況 (Worst Case) 來表示，詳細例子我們可以在下面看到。

```
fruits=[蘋果,芭樂,葡萄,木瓜];
for fruit in fruits:
    if(fruit == "木瓜"){
    print("找到木瓜了！");
    break;
}else{
    print("這裡沒有木瓜T_T");    
}
```

如果木瓜在第0號櫃子，一個步驟就可以找到他，如果在第3號櫃子，四個步驟才可以找到他。
因此Worst Case是我們剛好要花n個步驟才會找到->O(n)

#### O(log n)：二分搜尋
- 代表：input的數量為n時，執行的步驟數是log n
log n=x 意思是 n=2^x (log以2為底)

假設想要在字典中，找到一個單字，這個字開頭為X，我們可以用前面提過的「簡易搜尋」的邏輯，從A開始找找找；但也可以翻到字典後面，從w字母開頭的字，再繼續找。

假設有一長串有大有小排序好的數字們，我們要在其中找一個特定數字，一樣可以從第一個開始往後檢查。
但更聰明的方法是：先檢查最中間的數字，如果中間的數字比我們要找的數字大/小，我們要找的數量就只剩原本的一半

假設我們今天有七個櫃子
number = [3,14,18,24,30,47,51];
今天要找47這個數字

1. 簡易搜尋：
步驟一：打開第一個櫃子，不是
步驟二：打開第二個櫃子，不是
步驟三：打開第三個櫃子，不是
步驟四：打開第四個櫃子，不是
步驟五：打開第五個櫃子，不是
步驟六：打開第六個櫃子，是

要使用六個步驟才找到47


2. 二分搜尋法
第一步驟：打開第四個櫃子，24 ->比47小，那可以往後找
第二步驟：打開第五個櫃子，不是
第三步驟：打開第六櫃子，是

兩個方法的時間複雜度
1. 簡易搜尋：最壞情況需要7個步驟(要找51)
2. 二分搜尋法：

| 要找的數字 | 3 | 14 |18 |24 |30 |47 |51 |
| -------- | -------- | -------- | -------- | -------- | -------- | -------- | -------- |
| 需要步驟數| 3 | 2 | 3 |1 |3 |2 |3 |

有n個櫃子時，二分搜尋法每在進行一個步驟時，就可以扣除掉一半的可能性，每次都能減少一半，因此二分搜尋法最糟最糟也只需要以2為底的log n 個步驟就可以完成


```
Numbers = [5,17,33,41,55,61,80]
Find = 55
​
索引值
low = 0
high = len(Numbers) - 1
​
while low <= high:
    mid = (low + high) // 2
    if Numbers[mid] > Find:
        high = mid - 1
    elif Numbers[mid] < Find:
        low = mid + 1
    else:
        break
​
print(mid)
```
#### 選擇排序法(Selection Sort)
- 只要重複執行兩個步驟
1. 找最小值
從「未排序好的數字」中找到最小值
2. 丟到左邊
把最小值丟到「未排序好的數字」的最左邊，把它標示成已排序好

從n個還沒排序好的數字中找到最小值，需要n個步驟
最常見的找最小值的方法：我們先設陣列的第一個數字是「目前的最小值」，然後往後把陣列的數值一個一個讀取

如果讀取的下個數比最小值大，就什麼都不用做
而讀取到的數字比「目前的最小值」小，就把「目前的最小值」換成這個數。
重複這個方法把所有陣列的數都讀過一遍，就能確保目前的最小值為整個數列的最小值。

#### O(n logn)：合併排序(Merge Sort)
- 代表執行時間會隨著以二為底的log n乘上再n成長
- 最常見的例子是合併排序法(Merge Sort)與快速排序法(Quick Sort)

基本的步驟可以分為「拆分」及「合併」：
拆分：步驟數為 n-1
1. 把大陣列切一半，分為兩個小陣列
2. 把切好的兩個小陣列再各自切一半
3. 重複步驟2直到每個小陣列都只剩一個元素
合併：步驟數為 n * log n
1. 排序兩個只剩一個元素的小陣列並合併
2. 把兩邊排序好的小陣列合併並排序成一個陣列
3. 重複步驟2直到所有小陣列合併成一個大陣列

每回合的合併需要花：O(n)
總共需要回合數：O(log n)

合併排序法總共的步驟數為 n-1 + n log n。
時間複雜度，即為 O( n log n)。