---
# System prepended metadata

title: 王者歸來--演算法

---

# 王者歸來--演算法
備註: 以下程式範例皆有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

