# 王者歸來--演算法
備註: 以下程式範例皆有javascript和python兩種語言的範例。
## 時間複雜度O(念作Big O,畢格歐)
複雜度取法:
1. 省略係數
2. 取最高的指數
範例:
* 2n^2^種組合,則記為O(n^2^);
* 以2為底的log時,則省略2直接以log表示,例如二分法搜尋法等等的演算法,即為O(log n),另外比較常見的也有O(nlog n);
* 如果為常數,則直接記為O(1)
常見的幾個複雜度的比較(由最複雜到簡單):
O(n!) > O(n^2^) > O(nlog n) > O(n) > O(log n) > O(1)
## 資料結構
### 陣列Array
python的array相關語法
陣列操作的時間複雜度的比較
1. 查詢: 可以直接透過array[index] 取得值,所以是O(1)
2. 新增: 在某index插入值,必須遍歷該array,是O(n)
3. 刪除: 在某index刪除值,必須遍歷該array,是O(n)
```typescript=
const array = [1, 2, 3] // javascript
```
```python=
list = [1, 2, 3] # python
```
### 鏈結(List Node)
重點在於.next連接下一個node,有單向與雙向鏈結表(linked list),查找某項資料時需遍歷整個linked list資料(O(n))。
#### 一個鏈結有2個部分: 資料區(data)和指向區(next)
1. 查詢: 必須遍歷整個list,一路往下找資料,所以是O(n)
2. 新增: 只要重新指向資料即可,是O(1)
3. 刪除: 一樣也是只要重新指向資料即可,是O(1)
#### 程式實例:
https://github.com/ms0223900/algorithm-python-and-js/tree/master/practices/chapter3-linked-node
### 佇列Queue
重點為 ==頭進尾出(先進先出)==
* 新資料插入佇列enqueue: 0 --> [1 | 2 | 3] => [0 | 1 | 2 | 3]
* 讀取佇列dequeue: [0 | 1 | 2 | 3] =讀取=> return 3, [0 | 1 | 2]
#### 程式實例:
https://github.com/ms0223900/algorithm-python-and-js/tree/master/practices/chapter4-queue
### 堆疊Stack
重點為 ==頭進頭出(後進先出)==
遞迴的函示就是用stack的方式堆疊在記憶體中,取得最後一個值,可以實際運行factorial這個範例: [ch_5_5_factorial.py](https://github.com/ms0223900/algorithm-python-and-js/blob/master/practices/chapter5-stack/ch_5_5_factorial.py)
* 資料插入堆疊stack_push: 0 --> [1 | 2 | 3] => [1 | 2 | 3 | 0]
* 讀取堆疊stack_pop: [1 | 2 | 3 | 0] =讀取=> return 0, [1 | 2 | 3]
#### 程式實例:
https://github.com/ms0223900/algorithm-python-and-js/tree/master/practices/chapter5-stack
---
### 2. 雜湊表HashTable
就是鍵值對照表,js或其他語言中為object,python中為dict
每個key值會被"雜湊函數"計算成一個"雜湊值",然後將此雜湊值除以某個數字,所得的"餘數"作為陣列的index,將value存於陣列。
但是一定會有index值重複的情況,此時有2種做法:
* 用鏈結(Linked list)方式串聯value:
```typescript=
// 以下僅為示意而已,不一定是hashtable真正的運作方式
key = sunday
value = 0
key算出來的雜湊值為 12345(假定而已)
12345 % 4 = 1, index 則為1
將value放進array
雜湊array = [empty, {'sunday': 0}]
---
另一個key, value
key = monday
value = 1
key算出來的雜湊值為 12348(假定而已)
12348 % 4 = 0, index 則為0
將value放進array
雜湊array = [{'monday': 1}, {'sunday': 0}]
---
再一個key, value
key = tuesday
value = 2
key算出來的雜湊值為 12362(假定而已)
12362 % 4 = 0, index 則為0
array[0]有值了,所以有衝突! 就不能直接將value放進array!
則用鏈結方式.next串聯值
用()代表串聯值
雜湊array = [
{'monday': 1}({'tuesday': 2}),
{'sunday': 0}
]
```
* 開放定址法
```typescript=
// 以下僅為示意而已,不一定是hashtable真正的運作方式
已經存放值的array
[
{'tuesday': 'T'}.
{'sunday': 'S'},
{'monday': 'M'},
empty,
{'wednesday': 'S'}
]
key = thursday
value = 'T'
key算出來的雜湊值為 12380(假定而已)
12380 % 4 = 0, index 則為0
但是index為0的地方已經被占據,則從0的地方往下找array「空的」地方,找到後塞進array,若是往最後找都找不到時,則從頭開始找。
雜湊array =
[
{'tuesday': 'T'}.
{'sunday': 'S'},
{'monday': 'M'},
{'thursday': 'T'}, // 最後塞在這
{'wednesday': 'S'}
]
```
#### 搜尋值時
用以上的範例來做搜尋
當要找key為'tuesday'時,則用同樣的雜湊函數取得對應的index值,結果為0
搜尋值時,則是array[0].key != 'tuesday' // 不合需求
繼續往下找,array[0].next.key == 'tuesday' // 找到了
找出 {'tuesday': 2}
#### hashtable實際應用
範例:
```python=
# python
myDict = {
'a': 'a',
'b': 10,
}
```
```typescript=
// js
const obj = {
a: 'a',
b: 10,
}
```
---
## 二元樹相關
### 二元樹(btree)
#### 基本觀念:
最上方的節點,為根節點(root node);
每個節點都可以有"最多2個"子節點(child node),分別為.left和.right,代表左節點和右節點;
沒有子節點的節點為"葉節點"(leaf node);
#### 實例如下:
https://codesandbox.io/s/alogorithm-render-xkufd
![](https://i.imgur.com/ZXgxeuZ.jpg)
#### 建立二元樹
==每次都要跟節點比較,比該節點"大"的往右送;比該節點"小"的往左送==
#### 搜索
跟建立的做法差不多,與節點做比較,要搜索的值比現在的節點還大,則往右邊的子節點找,否則往左找。
#### 新增
將新的節點遍歷與比較所有節點,如果比較大則往右,否則往左。
#### 刪除
刪除相對麻煩,除了遍歷並比對數值的相等,還需要補上"空節點"。
#### 儲存方式
可以寫為array(list)或是object的形式
```python=
# list陣列
btree = [10, 5, 20, 3, None, 11, 21] # 沒有按照
# [root, 第一層, 第一層, 第二層, 第二層, 第二層, 第二層]
# listNode鏈結
class Node():
def __init__(self, data=None):
self.data = data
self.left = None
self.right = None
```
1. 陣列
優點: 資料相對簡潔。
缺點: 需要計算index以得知node為哪一層的節點,而且可能會有很多"空節點"。
2. 鏈結
優點: 資料比較直覺,跟二元樹的"結構"差不多。不會有多餘的空節點,且可以用遞迴的方式遍歷所有節點,相當方便。
缺點: 非"plain object",必須經過處理(inorder print等等)才能其中的資料。
#### 程式實例:
https://github.com/ms0223900/algorithm-python-and-js/tree/master/practices/chapter6-binary-tree
---
### 二元搜索法
<!-- 紅黑二元樹? -->
### 堆積樹(Heap Tree)
也是一種二元樹,但是跟上面提到的二元樹不同,當插入新節點時,會從左到右把空節點依序補齊,因此不太會有一般二元樹只偏頗某一邊的情況發生。
插入節點之後,將節點與父節點比較(看是最小還是最大堆積樹)並對調,如果是最小堆積樹,比父節點小的話,就跟父節點對調,反之亦然。
可應用於排序上(從大到小或從小到大),詳見下個章節的介紹與範例。
堆積樹種類:
* 最小堆積樹(最小的為根節點)
* 最大堆積樹(最大的為根節點)
#### 程式實例:
https://github.com/ms0223900/algorithm-python-and-js/tree/master/practices/chapter7_heap
---
## 7種排序法
預設都是最小在陣列的最前面[0]
1. 泡沫
平均: O(n^2^)
每一次迴圈都會把值兩兩做比較,每一次都會把最大值"像是泡沫一樣"推到array最右邊,所以被稱為"泡沫排序"法,而排序上每次只要n-2, n-3...的次數做排序即可(因為每一次都會把一個最大值排到右邊)。
total = (n-1) + (n-2) + ... + 1
2. 雞尾酒
最佳(如果大多排好): O(n)
平均: O(n^2^)
為泡沫排序的改良,透過往右找最大值和往左找最小值來將搜尋範圍漸漸縮小(將最大和最小的設為基準點),如果資料為大多排好的情況,則只要往左和往右找幾次就能排好了。
3. 選擇
平均: O(n^2^)
依序將最小值往array的最前面放。從index為0開始,找"index~最後"之中的哪個值為最小,與前面的值互換,接著index + 1,重複以上步驟,最後把值從小排到大。
4. 插入
平均: O(n^2^)
分為往後和往前遍歷,往後的index逐漸增加,而往前則是在"index~0"之中,"前面"的值如果比較大,就跟現在的值對調。這個方法跟"選擇排序"幾乎是相反的,是把比較大往後互換,最後把值從小排到大。
5. 堆積樹
平均: O(nlogn)
堆積樹的根節點必為最小(或最大)值,因此只要持續將值從堆積樹pop(取根節點並排序堆積樹)出來即可。
6. 快速(Quick Sort)
最佳/平均:O(nlogn)
首先隨機挑一個基準值(pivot),將陣列中的值做比較,如果比基準值還小,則被歸在左半邊,否則就是右半邊。用"遞迴"的方式,再將左半和右半做同一件事,最後將所有值合併在一起,就是小到大的排序了。
7. 合併
平均: O(nlogn)
與"快速排序"法有異曲同工之妙,也是將陣列切開。不過合併排序是從中間切開,將切開後的左半和右半在"合併時"做比較,並且合併為排序好的陣列。如此反覆用"遞迴"的方式,將陣列切到最小,再將每個左半和右半作合併,直到所有都合併完之後,就是已經排序好的陣列了。
#### 程式實例:
https://github.com/ms0223900/algorithm-python-and-js/tree/master/practices/chapter9_sort
---
## 搜尋法
### 數據搜尋
找尋陣列中的相符的數值,此書介紹了幾個方法。
1. 順序搜尋法 O(n)
從0開始,依序往1, 2,...n找相符的值,雖然可能很快就找到我們要的值,不過複雜度平均為O(n)
```python=
arr = [1, 2, 3, 5, 6]
def find(source, target):
for data in range(arr):
if data == source:
return data
return None
```
2. 二分搜尋法 O(logn)
首先此陣列要先"排序",接著不停地找中位數,比較我要找的值比中位數更大或更小,接著不停地比對該值,直到找到該數值為止。
3. 最大/最小搜尋法 O(n)
找最大值或最小值,最簡單的就是,先設定一個初始值(可以用陣列中的第一個),接著從頭依序找尋比較大(或比較小)的值,取代該初始值,找完所有的值之後,該初始值就是最大/最小的值了。
#### 程式實例:
https://github.com/ms0223900/algorithm-python-and-js/tree/master/practices/chapter10_search
## 走迷宮
主要方法為"標記",總共有幾個標記種類:
* 還沒走過(0)
* 已經走過(1)
* 死路(2)
將走過的路堆疊為stack(array),從起點開始,下一格如果不是死路,則標記"下一格"為通道。
但如果下一格無路可走,則代表"此格"為死路,將其標記為死路,並且"倒退"一格(捨棄剛剛走過的路),重複此行為直到找到出口。
#### 程式實例:
https://github.com/ms0223900/algorithm-python-and-js/tree/master/practices/chapter11_maze
---
## 遞迴
### 河內塔
有三個柱子可以擺放中空盤,分別為來源、輔助和目的。我們的任務是要從來源柱子上,從上到下由小到大擺放的中空盤子,全部都移到目的柱。而盤子不能隨意擺放,無論在哪一個柱子,都必須小的在上,大的在下。
解法為: 不管有幾個盤子,每次就是將來源移到輔助,輔助移到目標,如此反覆循環,用遞迴的方式解決此問題。
### 八皇后
* 溯回演算法:
這方法比較好懂,簡單來說就是依序找每一行(row)能擺的queen,但如果上一行放的queen讓該行無法擺放時,則"退回"上一行,重新擺放queen,直到八個皇后都被擺放至棋盤。
* 遞迴的方式:
原理為找每一行(row),依序增加row的值,遞迴式的找下一row可以放的queen,且每一次都會累積(因為是遞迴)下一次的結果,每一row都被互相影響著,如果下一row放的位置無法讓前一行有位置放,則該結果會被取代掉,直到計算出所有能放的queen都擺在棋盤上。
### 碎形
圖形的子單元跟主單元長得一模一樣,用遞迴的方式繪製子單元。常見的像是"遞迴樹",雪花等等。
(這部分因為沒有安裝套件,故暫無範例)
#### 程式實例:
https://github.com/ms0223900/algorithm-python-and-js/tree/master/practices/chapter12_recursive
---
## 圖論(Graph)
### 各種圖形
二元樹也是一種圖形
![](https://i.imgur.com/ZXgxeuZ.jpg)
圖形(每個節點之間有數值,計算點到點之間的最短路徑,例如計算下圖的A到G最短路徑)
![](https://i.imgur.com/LHN6x6q.jpg)
### 廣度優先搜尋
先找完同一層"所有"子節點,再往更深層找。
### 深度優先搜尋
先找下一層的第一個節點,該節點找完所有子節點後,繼續往該節點"更深層"找,直到沒有子節點,才往另一個去找。
### 最短路徑算法
遍歷圖形(Graph)中的所有節點,從起點開始遍歷所有子節點,並找出最小值總和,直到拜訪過所有的節點。有各種算法,其中Bellman ford算法可以算負權重的圖形。
#### 程式實例:
圖形
https://github.com/ms0223900/algorithm-python-and-js/tree/master/practices/chapter13_graph
最短路徑
https://github.com/ms0223900/algorithm-python-and-js/tree/master/practices/chapter14_shortest_path
---
## 貪婪演算法
用來算出"還算滿意"的答案,不一定會是"最好"的答案,會隨著一開始的選擇以及圖形節點順序的不同(權重都一樣),而產生不同的結果
#### 程式實例:
https://github.com/ms0223900/algorithm-python-and-js/tree/master/practices/chapter15_greedy
## 另外解法--動態規劃
除了用貪婪演算來算出"還行"的解答,動態規劃可以算出最佳解答,用的就是二維"表格"的方式。
用表格(二維陣列)來填入計算各種可能,重點是將主問題拆解為子問題,看該欄位還可以放哪些選項(物品),一個欄位不只可以塞一個值,如果還有多餘的"空間",則看最多還能塞多少,最後看此表格的"最右下角"能夠塞到多少的最大值(價值)。
#### 程式實例:
https://github.com/ms0223900/algorithm-python-and-js/tree/master/practices/chapter16_dynamic_plan
---
## 資料加密與資安
### 各種專有名詞
* 竊聽: 傳送過程(路由器、伺服器等等各種管道)中"竊取"資訊。
* 解決方法: 資料加密
* 竄改: 在傳送過程中掉包真正的數據,接收方拿到"不正確的資料"。
* 解決方法: 數位簽章、訊息識別碼
* 詐騙: 駭客偽裝成接收方
* 解決方法: 數位簽章、訊息識別碼
* 拒絕: 傳送方傳送資訊,但接收方表達"沒收到",可能造成糾紛。
* 解決方法: 數位簽章
### 加密
將原始資料加工成難以解讀的資料,就稱為「加密」。
而加密資料的演算法則稱為「金鑰」,即便駭客拿到加密後的資料,沒有「金鑰」通常難以讀取原始資料。接收方需要有金鑰才能夠解密並讀取正確的資料。
### 雜湊(Hash)和SHA
Hash為雜湊函數,可以將不同長度的資料轉換為"固定長度"的"雜湊值",雜湊值有128, 256位元等等不同的位元長度。
#### SHA(Secure Hash Algorithm)
是目前常用的Hash演算法之一,例如常聽到的Hash-256就是,可以將任何大小的資訊轉為256位元的雜湊值,即便資訊之間只差一個字,輸出的雜湊值也是完全不相同,目前還沒被破解。
#### 金鑰密碼系統
1. 對稱: 加密和解密用的是同一個金鑰演算法
最大缺點為,在傳送金鑰時可能被他人竊取,這個傳送金鑰的困難,又稱為"金鑰傳送困難"。
2. 非對稱: 加密和解密用的是不同的金鑰演算法
3. 金鑰種類:
* 公開金鑰: 給傳送方的金鑰,常見的為RSA演算法加密金鑰
* 私密金鑰: 接收方自己的金鑰,不傳給他人
4. 主要運作原理:
接受方擁有公開和私密的金鑰。要傳送資料前,將公開金鑰傳給傳送方,傳送方將資料加密之後傳給接受方,再用私密的金鑰來解密。
5. 可能的威脅: 中間人攻擊
簡單來說,就是駭客用自己的公開金鑰(姑且稱為C)和私密金鑰(C')給傳送方,並從中"攔截"接收方給傳送方的公開金鑰(A),並將原本的A掉包為C,傳送方因為不知道(沒憑證的情況下)是誰給的,所以用C將自己的資料加密。
接著駭客(現在手中有C, C', A這些金鑰)從中攔截加密後的資料,將其用C'解密為原始資料,再用A這個原本的公開金鑰加密資料,將其傳給接收方,接收方用他的私密金鑰一樣可以解密,接收方沒發現問題,因此也不知道資料已經被攔截"偷看過"了。
要解決這個威脅,可以用SSL協定等等的數位憑證,來將金鑰賦予認證,如此一來可以知道是誰發的金鑰,以減少上述的威脅。
#### 認證與憑證
* MAC
在原始文件加密後,再用特定算法附上一段HASH雜湊"鑑別碼"(MAC),成為一個有鑑別碼的加密文件。接收方收到後算出MAC,再和接收方自己算的MAC做比對,如果一致則代表沒被竄改過。
* 數位簽章
特色為「私鑰加密」、「公鑰解密」,與上述的"中間人攻擊"案例的運作原理剛好相反。
傳送方 先在資料中附上數位簽章,接著用私鑰加密。接著傳送公鑰給接收方,再將含有簽章的被加密資料傳給接收方,用該公鑰解密,看是否簽章是否正確,來驗證對方的身分。
* 數位憑證
https憑證就是使用這個方法。特色為需要先向「認證機構(CA)」申請憑證,你必須提供你的個人訊息(例如姓名、組織等等)和公開金鑰給該機構,接著該機構會將你所有的訊息+公開金鑰用該機構的"私密金鑰"加密,做成一個含有該機構數位簽章的"數位憑證",將其發送給你,這就是你/你的服務在網路上的"身分證"。
傳送方傳送"數位憑證"給對方,而接收方拿到該數位憑證之後,接著向CA查證之後,再用CA的公開金鑰解密,取得傳送方的公開金鑰,接著就可以相對安全地信任作資料的傳輸了。
相關文章: https://progressbar.tw/posts/98
---
## 人工智慧(深度學習)相關演算法--KNN演算法
重點在於"算出特徵數值",主要用到了"迴歸"與"統計",算出平均值和近似值。
### 計算特徵
常用於推薦的演算法,算法是與每個特徵值的距離來看,越近表示越相似則越推薦。公式為畢氏定理的算法,例如,有兩項特徵值分別為a, b,則((a2 - a1)^2^ + (b2 - b1)^2^)^1/2^,算出與目標最相近的。
### 分群算法
將一堆點做分群,用的是計算特徵的算法做延伸。一開始先設定好分N群,並"隨機挑選"群集的中心點,接著計算"群集中每個點到中心點"最短距離,"重新計算"該中心點的新位置來做"重新分群",直到每個群集的每個點已經不再變化,表示最佳的分群已經完成。
![image alt](https://miro.medium.com/max/1050/1*2jUdohA3MZ1u0utGmsBTRw.gif)
#### 程式實例:
https://github.com/ms0223900/algorithm-python-and-js/tree/master/practices/chapter18_knn
---
## 面試常見演算法考題
* 質數:
先將原數字開根號並取其最小整數,檢查其因數是否有大於1的因數即可,如果沒有大於1的因數則表示該數字為"質數"。
* 回文:
將字串轉為陣列,從前面和從後面各拿一個字,比對看是否一樣,直到剩下0~1個字。
* 最大公因數:
* 逼近法: 從2開始一個個找是否為兩數的因數,直到該因數等於其中比較小的數字。
* 輾轉相除法(歐幾里得法): 兩數字先比較,除數為比較小的(假如為A),被除數為比較大的(B)。接著,兩數相除所得的餘數再作為除數,而原本比較小的數字(A)則是作為被除數,如此相除直到"餘數為0",取最後一個被除數就是兩者的最大公因數。
* 最小公倍數:
為兩數相乘 除以 兩數的最大公因數。
* 雞兔同籠:
可以用數學的二元一次方程式來解,也可以用程式直接一個個逼近來算。
* 挖金礦:
同動態規劃的問題。
#### 程式實例:
https://github.com/ms0223900/algorithm-python-and-js/tree/master/practices/chapter19_interviews