# 演算法 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)。