# Data Structures and Algorithms - 資料結構與演算法
## TL;DR
學習資料結構與演算法的筆記整理
# 深度優先搜索與廣度優先搜索 DFS and BFS on Tree and Graph
## 重點整理
* 以 `while` loop 與 Queue 實現 BFS
* 以 Recursion 實現 DFS
* 在 Graph 上多考慮 Redundancy 問題
* Tree 的解題重點通常在於在每一個 node 上要做什麼事情,還有要使用 Bottom-up 還是 Top-down
## Implementing BFS on Binary Tree
### 邏輯
1. Add the starting node (`root` node) to the queue
2. while queue is not empty
1. 利用變數 `currentNode` 指向正在操作的 node (to mark which node you are operating on) - `dequeue()`
2. Visit the `currentNode`
3. Check `currentNode` 是否有左子節點與右子節點,有的話 Add them to the queue
```
if (currentNode.left) {queue.push(currentNode.left)}
if (currentNode.right) {queue.push(currentNode.right)}
```
```
const queue = []
queue.push(root)
while (queue.length > 0) {
const current = queue.shift()
// Visit the current node
if (current.left) {queue.push(current.left)}
if (current.right) {queue.push(current.right)}
}
```
## Implementing DFS on Binary Tree
使用 Recursion 比較適合,利用 Iteration 來建立 DFS 時,Preorder, Inorder, Postorder 三種方法會很不一樣,並不只是換指令的順序而已。
### 邏輯
1. 定義一個 helper function `traverse()`,接受一個參數 `currentNode`
1. Base case: `currentNode === null`
2. Recursive case:
* Visit the `currentNode`
* 以左子節點與右子節點作為參數調用 recursive function
> 在 Recursive Calls 之前、之中、之後 visit `currentNode` 決定是 Preorder, Inorder, 還是 Postorder
2. Invoke the helper function with the starting node (root node) as the input parameter
```
const traverse = (current) => {
if (current === null) {return}
// Proorder
traverse(current.left)
// Inorder
traverse(current.right)
// Postorder
}
```
## Implementing BFS on Graph
概念基本上與 BFS on Tree 類似,但是 Graph 需要多考慮 Redundancy 的問題,也就是同一個 Vertex 可能被重複走訪,需要透過一個 set to keep track of the vertex that has been visited
#### 邏輯
1. Add the starting node to the queue
2. Add the starting node to the set
3. While queue is not empty
1. 利用變數 `currentNode` 指向正在操作的 node (to mark which node you are operating on) - `dequeue()`
2. Visit the `currentNode`
3. Loop through all `neighbors`
4. Check `neighbors` 是否有被走訪過
5. 沒有的話 add them to the queue, add them to the set
```
const queue = []
const visited = new Set()
queue.push(start)
visited.add(start)
while (queue.length > 0) {
const current = queue.shift()
// Visit the current node
const neighbors = graph[current]
for (let i = 0; i < neighbors.length; i++) {
if (!visited.has(nieghbors[i])) {
queue.push(neighbors[i])
visited.add(neighbors[i])
}
}
}
```
## Implementing DFS on Graph
概念基本上與 DFS on Tree 類似,但是 Graph 需要多考慮 Redundancy 的問題,也就是同一個 Vertex 可能被重複走訪,需要透過一個 set to keep track of the vertex that has been visited。
### 邏輯
1. 定義一個 helper function `traverse()`,接受一個參數 `currentNode`
1. Base case: `currentNode` has been visited
2. Recursive case:
* Visit the `currentNode`
* **Add the `currentNode` to the set**
* 以 `neighbors` 作為參數調用 Recursive Function
2. Invoke the helper function with the starting node as the input parameter
```
const visited = new Set()
const traverse = (current) => {
if (visited.has(current)) {return}
// Visit the current node
visited.add(current)
const neighbors = graph[current]
for (let i = 0; i < neighbors.length; i++) {
traverse(neighbors[i])
}
}
```
DFS on Graphs 也可以利用 `while` loop 與 Stack 來實現,將 BFS on Graph 的步驟中的 Queue 改成 Stack 即可。
# 回溯法 Backtracking
## 回溯法的邏輯
簡單來說,**Backtracking 就是回到上一個選擇的狀態**。逐一嘗試各種可能 (Enumeration of Candidates 列舉各種可能性, enumeration 列舉),在嘗試過程中若發現路徑不為解或已窮盡所有可能,退回前一步驟,重新嘗試其他可能。
## 解題重點
以我目前的解題經驗來看,**Backtracking 通常搭配遞迴函數實現窮舉。窮舉的邏輯是一個樹狀結構,窮舉的過程是 DFS on Tree**。
### 如何分析一個窮舉過程
建立窮舉邏輯的樹狀結構時,思考窮舉過程的三個元素:
* 路徑 (path) - 已經做的選擇
* 選擇 (options) - 目前可以做的選擇
* 終止條件 (base cases) - 遞迴終止的條件
**窮舉的邏輯主要分為兩個層面**
* 第一個層面 (做選擇) - 調用 Recursion,選擇後的狀態調用 Recursion 代表做了一個選擇,往下一個選擇移動。
* 第二個層面 (窮舉各種選擇) - 利用 Iteration 窮舉當下的所有選擇。Backtracking 會與 Iteration 在同一個 level 發生,在每一個 Iteration 裡
1. 做選擇
2. 調用遞迴
3. 取消選擇
簡而言之,做選擇就是在樹狀結構上執行 DFS,做完選擇,狀態改變,掉用遞迴,前往下一個狀態。
```
let result = []
const backtrack = (path, options) => {
if (base case) {
result.push(path)
return
}
for (option in options) {
path.push(option) // 做選擇
backtracking(path, options) // 調用遞迴
path.pop() // 取消選擇
}
}
```
# 排列組合 Permutation and Combination
**排列組合的解題邏輯基本上靠窮舉,窮舉的邏輯是一個樹狀結構而窮舉的過程就是 DFS on Tree,所以可以透過 DFS 搭配 Backtracking 的概念走訪窮舉邏輯的樹狀結構得到所有的可能。DFS 靠 Recursion 實現**。
用 Recursion 來實現窮舉的時候,需要解決的問題是如何建立 Recursion。有三個元素可以幫助思考如何建立 Recursion 來窮舉。
* 路徑 (path) - 已經做的選擇
* 選擇 (options) - 目前可以做的選擇
* 終止條件 (base cases) - 遞迴終止的條件
## 解題步驟
1. 以上述三點建立窮舉的樹狀邏輯 - 每個步驟 (節點) 的狀態要標示清楚是關鍵
2. **透過樹狀邏輯得到 Recursion 函數需要的參數與 Base Case**
3. 再利用模板確認細部邏輯
## 排列 Permutation
### 定義
從給定個數的元素中取出指定個數的元素進行排列。元素相同但順序不同為不同的排列。(順序不同算不同)
### 邏輯
因為是排列,先取 1 再取 2 與先取 2 再取 1 是不一樣的,所以除了路徑中已經被選過的元素不能再選以外 (通常排列不考慮同一元素可以重複使用),所有選擇都在被考慮範圍。若選項中有重複的元素,在同一個 Level 相同元素的選項是被視為相同的,不需重複選取。
### 模板
使用模板前先考慮三個問題:
* 是不是排列?
如果是,要考慮順序。先取 1 再取 2 與先取 2 再取 1 是不一樣的。每次都從頭 loop through 所有選擇。
```
for (let i = 0; i < options.length; i++) {
// 做選擇
}
```
* 同一元素可不可以重複使用?
若是不能重複同一元素,用一個 `used` array 紀錄這個 index 有沒有用過。做決定時 `used[i] = true`,在 backtrack 時 `used[i] = false`。`if (used[i]) {continue}` 跳過這個選擇。
* options 裡面有沒有重複的元素?
若有重複元素,在同一個 level 遇到重複的元素應視為相同的選擇,要忽略。**先將 options 排序後**,當 `i > 0 && !used[i - 1] && nums[i] === nums[i - 1]` 時 `continue`(**`used[i - 1] === false` 因為有 Backtracking**,`used[i - 1] === true` 會發生在下一個 Level 的選擇但元素與上一個 Level 被選擇的元素一樣,此時 `options[i]` 是可以選擇的)
* 檢查 Backtracking
[LeetCode 47 花花答案參考](https://zxi.mytechroad.com/blog/searching/leetcode-47-permutations-ii/)
```
options = options.sort((a, b) => a - b)
dfs(state, path, used) {
if (state === 終止條件) {遞迴終止}
for (let i = 0; i < options.length; i++) {
if (used[i]) {continue}
used[i] = true
path.push(options[i])
dfs(state + options[i], path, used)
path.pop()
used[i] = false
}
}
```
## 組合 Combination
### 定義
從給定個數的元素中取出指定個數的元素,不考慮排序。元素相同但順序不同依然為相同的組合。(順序不同算相同)
### 邏輯
因為是組合,先取 1 再取 2 與先取 2 再取 1 是一樣的,所以前面選過的元素後面不要再考慮。同一個元素是否可以重複選取會影響接下來可做的選擇。若選項中有重複的元素,在同一個 Level 相同元素的選項是被視為相同的,不需重複選取。
### 模板
* 是不是組合?
如果是,不考慮順序。先取 1 再取 2 與先取 2 再取 1 是一樣的。定義一個 `start` 參數,loop through 選擇的時候從 `start` 開始,避免重複的組合。
```
for (let i = start; i < options.length; i++) {
// 做選擇
}
```
* 同一元素可不可以重複使用?
如果可以重複使用同一元素,要做下一個選擇的 `start` 不變。若是不能重複同一元素,要做下一個選擇時 `start + 1`。
* options 裡面有沒有重複的元素?
若有重複元素,在同一個 level 遇到重複的元素應視為相同的選擇,要忽略。**先將 options 排序後**,當 `i > start && options[i] === options[i - 1]` 時 `continue`
* 檢查 Backtracking
```
options = options.sort((a, b) => a - b)
dfs(state, path, start) {
if (state === 終止條件) {遞迴終止}
for (let i = start; i < options.length; i++) {
path.push(options[i])
dfs(state + options[i], path, i)
path.pop()
}
}
```
## 範例
### 39. Combination Sum
Time Complexity:
為 n 叉樹的節點數,n 為 the size of candidates array
樹高為 target / min value in candidates,考慮每次都加最小值
節點數 = 1 + n + n ^ 2 + .... + n ^ (target / min)
等比級數和公式 = a0(1 - r ^ (n)) / 1 - r
所以節點數為 (1 - n ^ ((target / min) + 1)) / 1 - n
Space Complexity:
O(target / min)
### 46. Permutations
Time Complexity:
1 + n + n(n - 1) + ... + n! (應該是這樣)
Space Complexity:
O(n!)