# 2021q1 Homework1 (quiz1)
contributed by < `Nahemah1022` >
###### tags: `linux2021`
- [x] 解釋程式運作原理
- [x] 引入其他 [Pseudorandom number generator](https://en.wikipedia.org/wiki/Pseudorandom_number_generator)
- [x] 參考 [Optimized QuickSort](https://alienryderflex.com/quicksort/),避免使用遞迴呼叫
- [ ] 改寫 [linux-list](https://github.com/sysprog21/linux-list) 的 quick sort 範例,避免使用遞迴呼叫
- [ ] [A Comparative Study of Linked List Sorting Algorithms](https://pages.mtu.edu/~shene/PUBLICATIONS/1996/3Conline.pdf),思考高效率的 linked list 排序演算法,並落實於程式碼中
---
## 一、解釋程式運作原理
### 1. 回顧 QuickSort 演算法
- 演算法流程
1. 選定一 pivot
2. 與其他所有 element 並比較其大小,並分別排至其左右
3. 對左、右兩 partitions 再執行上述演算法
4. 已經分割至最小單位後結束
- 分析:
- Divide & Conquer,將問題不斷分割為性質相似的子問題
- 每次選中的 pivot 是影響效能的關鍵
- 若選擇的 pivot **接近中位數**,則能夠將問題分割為等 size 的子問題,能夠在最小的遞迴深度內將子問題並行處理
- 反之則會使結構歪斜,即失去 Divide 的效果
### 2. 程式碼解讀
```cpp
static inline void list_add_node_t(node_t **list, node_t *node_t) {
node_t->next = *list;
*list = node_t;
}
static inline void list_concat(node_t **left, node_t *right) {
while (*left)
left = &((*left)->next);
*left = right;
}
void quicksort(node_t **list)
{
if (!*list)
return;
node_t *pivot = *list;
int value = pivot->value;
node_t *p = pivot->next;
pivot->next = NULL;
node_t *left = NULL, *right = NULL;
while (p) {
node_t *n = p;
p = p->next;
list_add_node_t(n->value > value ? &right : &left, n);
}
quicksort(&left);
quicksort(&right);
node_t *result = NULL;
list_concat(&result, left);
list_concat(&result, pivot);
list_concat(&result, right);
*list = result;
}
```
1. `list_add_node_t`:將 `node_t` 插入原有 `list` 中的頭
- 參數意義解析
- `list`:指向原有 linked list 頭的 indirect pointer
- `node_t`:欲插入 linked list 中的節點
- 功能解析
- 先將 `node_t` 連接至原有 linked list 頭
- 再將頭修改為新節點 `node_t`
2. `list_concat`:將兩 linked list 串接
- 參數意義解析
- `left` 原有 linked list 中的任一節點之 indirect pointer
- `right` 欲串接 linked list 的頭
- 功能解析
- 利用迴圈尋找 `left` 的末端位置,並停留在末端的 next 節點
- 將其內容修改為 `right`,即完成串接
:::success
**問題探討:指標的指標**
```graphviz
digraph structs {
rankdir=LR
node [shape=record];
struct1 [label="{value |<n1> next node address}|<a1>node address|direct pointer address"];
struct2 [label="{value |<n2> next node address}|<a2>node address|direct pointer address"];
struct3 [label="{value |<n3> next node address}|<a3>node address|direct pointer address"];
struct1:n1->struct2:a2[arrowhead=vee, tailclip=true, arrowsize=1];
struct2:n2->struct3:a3[arrowhead=vee, tailclip=true, arrowsize=1];
}
```
- **direct pointer 使用上的限制:**
以此函式為例,若將傳入的參數改為 direct pointer `node_t *left` 並在迴圈中使用 `left = left->next` ,待迴圈行至串列尾端時須提前留意該位置的下一個 node address 是否為 `Null` ,若等到 `left == Null` 時才將 `left` assign 為 `right` ,也已經無法回頭修改到前一個 node 的 next 。
- **indirect pointer 如何避開此限制:**
上述問題發生的主要原因在於 **direct pointer 傳值不傳址**,因此無法同時修改到前者的 next 以及後者的 value,但兩者其實是源自於同一個指標變數,因此可以透過題目中的 indirect pointer `node_t **left` 傳址的性質來一次修改到兩者,即可避免在 singly linked list 中時常路過了就無法再回頭修改的難題。
:::
3. quicksort
- 將串列的頭選為 pivot ,以其 value 大小為準將串列切割為兩部份,分別存放置 `*left` 與 `*right` 中
- 對 `*left` 與 `*right` 再次遞迴上述演算法
- 當串列小至不可再分割時 return
- 最後將 `*left`、`pivot` 與 `*right` 串接即完成
---
## 二、引入其他 [Pseudorandom number generator](https://en.wikipedia.org/wiki/Pseudorandom_number_generator)
### 1. 問題與原因探討
若多次執行測驗中的測試程式,會發現每次生成的亂數結果相仿,其原因在於 `random()` 函式本身的 [Pseudorandomness](https://en.wikipedia.org/wiki/Pseudorandomness) 特性,函式的 sequence of numbers 產生於 [deterministic](https://en.wikipedia.org/wiki/Deterministic_system) 的演算法,根據一個亂數種子來產生其對應的數序。
而在測試程式中並沒有給定亂數種子,按照 [random](https://linux.die.net/man/3/random) 中的說明 default 值為 `1` ,因此每次產生的數序皆相同。
### 2. 解決方式
修改為如下程式碼,利用 `srandom()` 設置亂數種子,其值為 `<time.h>` 中的 `time(NULL)`
```cpp
int main(int argc, char **argv) {
srandom(time(NULL));
size_t count = 20;
node_t *list = NULL;
while (count--)
list = list_make_node_t(list, random() % 1024);
...
}
```
如此一來程式在被執行時,將會以當下的系統時間作為亂數種子,利用系統時間不會重複出現的特性,即可在每次執行時產生相異的數序
:::warning
**隱憂:程式在相同系統時間下被執行複數次**
- 系統時間戳記的單位是用微秒,因此若程式在同一個微秒被執行兩次,其內容產生的亂數將會相同(此情況容易發生在 Multi-threaded 的程式中)
:::
---
## 三、[Non-Recursive Optimized QuickSort](https://alienryderflex.com/quicksort/)
### 1. 程式解析
```c=
// quickSort
//
// This public-domain C implementation by Darel Rex Finley.
//
// * Returns YES if sort was successful, or NO if the nested
// pivots went too deep, in which case your array will have
// been re-ordered, but probably not sorted correctly.
//
// * This function assumes it is called with valid parameters.
//
// * Example calls:
// quickSort(&myArray[0],5); // sorts elements 0, 1, 2, 3, and 4
// quickSort(&myArray[3],5); // sorts elements 3, 4, 5, 6, and 7
#define MAX_LEVELS 1000
bool quickSort(int *arr, int elements)
{
int piv, beg[MAX_LEVELS], end[MAX_LEVELS], i = 0, L, R;
beg[0] = 0;
end[0] = elements;
while (i >= 0) {
L = beg[i];
R = end[i] - 1;
if (L < R) {
piv = arr[L];
if (i == MAX_LEVELS - 1)
return NO;
while (L < R) {
while (arr[R] >= piv && L < R)
R--;
if (L < R)
arr[L++] = arr[R];
while (arr[L] <= piv && L < R)
L++;
if (L < R)
arr[R--] = arr[L];
}
arr[L] = piv;
beg[i + 1] = L + 1;
end[i + 1] = end[i];
end[i++] = L;
} else {
i--;
}
}
return YES;
}
```
程式用 `int beg[], end[]` 兩個 `array` 來模擬 recursive 版本 quick sort 中 stack 堆疊的情形,用來紀錄每一層切割形成區段的開始與結束點
- `if` 區段即是 recursive 版本中的 Divide 行為(程式 `24` 行)
1. 選擇區段的開頭作為 pivot
2. 由最右端開始,找出區段中最後一個小於 pivot 的值(R)
3. 由最左端開始,找出區段中第一個大於 pivot 的值(L)
4. 將 R、L 對調,把區段範圍縮小至 L 到 R 並重複上方演算法
5. 當 R、L 相遇時代表 Divide 已完成,將 pivot 插入中間
6. 將左右兩 partitions 端點位置 push 進 stack 中
- `else` 區段即是 recursive 版本中的 Merge 行為(程式 `42` 行)
1. 程式的資料結構為 `array`,順序正確即可無須做 concatenate
2. 因此直接將 stack pop(`i--`)即可
### 2. 應用在 singly linked list 中
> [GitHub](https://github.com/Nahemah1022/Linux2021-quizzes/tree/main/quiz1)
```c=
#define MAX_LEVELS 1000
void q_sort(queue_t *q)
{
char *pivot;
int i = 0;
list_ele_t *beg[MAX_LEVELS] = {NULL}, *end[MAX_LEVELS] = {NULL}, *junc = NULL;
beg[0] = q->head;
end[0] = q->tail;
while (i >= 0) {
list_ele_t *lh = beg[i], *rh = beg[i], *lt = beg[i], *rt = beg[i];
if (beg[i] != end[i]) {
pivot = beg[i]->value;
list_ele_t *p = beg[i];
while (p != end[i]) {
p = p->next;
if (strcasecmp(p->value, pivot) < 0) {
lt = (lh != beg[i]) ? (lt->next = p) : (lh = p);
} else {
rt = (rh != beg[i]) ? (rt->next = p) : (rh = p);
}
}
lt->next = beg[i];
beg[i]->next = rh;
rt->next = NULL;
beg[i + 1] = beg[i];
end[i + 1] = beg[i];
beg[i] = lh;
end[i] = lt;
beg[i + 2] = rh;
end[i + 2] = rt;
i += 2;
} else {
if (beg[i] != junc)
beg[i]->next = junc;
junc = beg[i];
i--;
}
}
q->tail = q->head = beg[0];
while (q->tail->next)
q->tail = q->tail->next;
}
```
- `if` 區段同樣負責做 Divide
1. 將區段開頭(`beg[i]`)設為 pivot
2. `lh`、`lt`、`rh`、`rt` 分別用來紀錄左、右 partition 區段的頭尾
3. 將左、pivot、右 三個區段的端點 push 進 stack(三元樹)
- `else` 區段同樣負責做 Merge
1. 將區段彼此間 linked list 結構串連
2. 將 stack pop (`i--`)
:::warning
在這之前,我曾經嘗試將上述 [non-recursive quick sort]((#1-程式解析)) 的演算法邏輯複製過來,實做出了以下程式,雖然能夠輸出正確的結果,但效能相當不理想,甚至可能比普通的 insertion sort 還要差。
```c=
void q_sort(queue_t *q)
{
char *pivot;
int i = 0;
list_ele_t *beg[q->size], *end[q->size];
list_ele_t *l, *r, *pr;
beg[0] = q->head;
end[0] = q->tail;
while (i >= 0) {
l = beg[i];
r = end[i];
if (l && r && l != r) {
pivot = l->value;
while (l != r) {
pr = r = l;
while (pr != end[i]) { // find the last one smaller than pivot
pr = pr->next;
if (strcasecmp(pr->value, pivot) < 0)
r = pr;
}
if (r != l) { // found
l->value = r->value;
l = l->next;
}
while (strcasecmp(l->value, pivot) <= 0 &&
l != r) { // find the first one bigger than pivot
l = l->next;
}
if (l != r) {
r->value = l->value;
}
}
l->value = pivot;
beg[i + 1] = l->next;
end[i + 1] = end[i];
end[i++] = l;
} else {
i--;
}
}
}
```
我檢討其原因後發現問題出在兩者使用的資料結構,不同於[上方](#1-程式解析)程式使用的 `array` ,本處使用的 `singly linked list` **無法直接造訪前一個 index 的 node**,因此在無法任意修改資料結構的環境下,要作到上述演算法中 *由最右端開始,找出區段中最後一個小於 pivot 的值* ,**勢必要從頭開始找到尾** 並紀錄最小者(即上方程式的 `16` 行),才能夠確保其為 *最後一個*,然而這會使效能遽減,會讓演算法複雜度直接增加 n 個 order。
重新思考上方 [Non-Recursive Quicksort](#1-程式解析) 程式的核心精神其實是在於 **用 stack 去紀錄切割而成的區間端點**,而非左右交換的 partition 演算法,因此我將 partition 的手段從左右交換改回單向造訪並比較,最後才實做出了 performance 正常的 [optimized-non-recur-linkedlist-qsort.c](https://github.com/Nahemah1022/Linux2021-quizzes/blob/main/quiz1/optimized-non-recur-linkedlist-qsort.c) 程式。
:::
---
## 四、[linux-list](https://github.com/sysprog21/linux-list) 中 quick sort 的不同
---
## 五、[A Comparative Study of Linked List Sorting Algorithms](https://pages.mtu.edu/~shene/PUBLICATIONS/1996/3Conline.pdf)