# 2021q1 Homework1 (quiz1) contributed by < `wiasliaw` > ###### tags: `sysprog2021` ## 進度 - [x] 重新回答並解釋運作原理 - [ ] 測試程式使用到 random,請修正「輸出結果相仿」 - [ ] 參考 Optimized QuickSort,並重寫上述 quick sort 程式碼,避免使用遞迴呼叫 - [ ] 請探討 Linux 的 linked list 和上述程式碼的落差,並改寫 linux-list 的 quick sort 範例,避免使用遞迴呼叫 - [ ] 研讀 A Comparative Study of Linked List Sorting Algorithms,思考高效率的 linked list 排序演算法並落實於上述 (3) 的程式碼中 ## 隨堂測驗 ### list_concat ```c= static inline void list_concat(node_t **left, node_t *right) { while (*left) left = &((*left)->next); *left = right; } ``` :::info * 作答 * 從 while 迴圈和函式命名可以知道要讓 left 指向到 list 的最末端,所以選 `left = &((*left)->next)` ::: 將兩個 linked list 串接起來 * 首先看一下原本的 left 跟 right ```graphviz digraph list_concat { rankdir=LR; node [shape=record]; l0 [label="{<data>L0|<ref>}"]; l1 [label="{<data>L1|<ref>}"]; l2 [label="{<data>L2|<ref>}"]; lNULL [label="NULL",shape=plaintext]; left [label="left",shape=plaintext]; r0 [label="{<data>L0|<ref>}"]; r1 [label="{<data>L1|<ref>}"]; r2 [label="{<data>L2|<ref>}"]; rNULL [label="NULL",shape=plaintext]; right [label="right",shape=plaintext]; left->l0; l0:ref:to -> l1:data [arrowtail=dot, dir=both, tailclip=false]; l1:ref:to -> l2:data [arrowtail=dot, dir=both, tailclip=false]; l2:ref:to -> lNULL [arrowtail=dot, dir=both, tailclip=false]; right->r0; r0:ref:to -> r1:data [arrowtail=dot, dir=both, tailclip=false]; r1:ref:to -> r2:data [arrowtail=dot, dir=both, tailclip=false]; r2:ref:to -> rNULL [arrowtail=dot, dir=both, tailclip=false]; } ``` * 第 2 到 3 行: 將 left 指向到 linked list 的尾端 ```graphviz digraph list_concat { rankdir=LR; node [shape=record]; l0 [label="{<data>L0|<ref>}"]; l1 [label="{<data>L1|<ref>}"]; l2 [label="{<data>L2|<ref>}"]; lNULL [label="NULL",shape=plaintext]; left [label="left",shape=plaintext]; r0 [label="{<data>L0|<ref>}"]; r1 [label="{<data>L1|<ref>}"]; r2 [label="{<data>L2|<ref>}"]; rNULL [label="NULL",shape=plaintext]; right [label="right",shape=plaintext]; left->lNULL; l0:ref:to -> l1:data [arrowtail=dot, dir=both, tailclip=false]; l1:ref:to -> l2:data [arrowtail=dot, dir=both, tailclip=false]; l2:ref:to -> lNULL [arrowtail=dot, dir=both, tailclip=false]; right->r0; r0:ref:to -> r1:data [arrowtail=dot, dir=both, tailclip=false]; r1:ref:to -> r2:data [arrowtail=dot, dir=both, tailclip=false]; r2:ref:to -> rNULL [arrowtail=dot, dir=both, tailclip=false]; } ``` * 第 4 行: 將 right 接在 left 的末端 ```graphviz digraph list_concat { rankdir=LR; node [shape=record]; l0 [label="{<data>L0|<ref>}"]; l1 [label="{<data>L1|<ref>}"]; l2 [label="{<data>L2|<ref>}"]; left [label="left",shape=plaintext]; r0 [label="{<data>L0|<ref>}"]; r1 [label="{<data>L1|<ref>}"]; r2 [label="{<data>L2|<ref>}"]; rNULL [label="NULL",shape=plaintext]; right [label="right",shape=plaintext]; left->r0; l0:ref:to -> l1:data [arrowtail=dot, dir=both, tailclip=false]; l1:ref:to -> l2:data [arrowtail=dot, dir=both, tailclip=false]; l2:ref:to -> r0 [arrowtail=dot, dir=both, tailclip=false]; right->r0; r0:ref:to -> r1:data [arrowtail=dot, dir=both, tailclip=false]; r1:ref:to -> r2:data [arrowtail=dot, dir=both, tailclip=false]; r2:ref:to -> rNULL [arrowtail=dot, dir=both, tailclip=false]; } ``` ### quicksort ```cpp= 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; } ``` :::info * 作答 * 從三元運算子可知比 pivot 大的要放進 AAA; 小的要放進 BBB,但是從第 22 行可以知道 left 是接在 list 前面,所以要放比 pivot 小的 node,所以 BBB 應為 `&left`,AAA 應為 `&right`。 * 串接完 left 後,再來需要先接上 pivot,最後才接 right,故選 `list_concat(&result, pivot);list_concat(&result, right)` ::: :::danger 不要用「選擇」的思維作答,選擇題只是便於授課教師批改,你應該設想「如果要我憑空撰寫程式,該怎麼做?」 :notes: jserv ::: 實作 quick sort 演算法將 linked list 排序 1. 第 3 行: 確保 list 不是 NULL,假設原本的 list 如下 ```graphviz digraph quicksort { rankdir=LR; node [shape=record]; list [label="list",shape=plaintext]; l0 [label="{<data>3|<ref>}"]; l1 [label="{<data>1|<ref>}"]; l2 [label="{<data>5|<ref>}"]; l3 [label="{<data>2|<ref>}"]; l4 [label="{<data>4|<ref>}"]; list->l0 l0:ref:to -> l1:data [arrowtail=dot, dir=both, tailclip=false]; l1:ref:to -> l2:data [arrowtail=dot, dir=both, tailclip=false]; l2:ref:to -> l3:data [arrowtail=dot, dir=both, tailclip=false]; l3:ref:to -> l4:data [arrowtail=dot, dir=both, tailclip=false]; } ``` 2. 第 6 到 10 行: * 取出 list 中第一個節點作為 pivot * 剩下的 list 存於變數 `p` ```graphviz digraph quicksort { rankdir=LR; node [shape=record]; list [label="p",shape=plaintext]; l0 [label="{<data>3|<ref>}"]; l1 [label="{<data>1|<ref>}"]; l2 [label="{<data>5|<ref>}"]; l3 [label="{<data>2|<ref>}"]; l4 [label="{<data>4|<ref>}"]; pivot [label="pivot",shape=plaintext]; pivot -> l0; list->l1 l1:ref:to -> l2:data [arrowtail=dot, dir=both, tailclip=false]; l2:ref:to -> l3:data [arrowtail=dot, dir=both, tailclip=false]; l3:ref:to -> l4:data [arrowtail=dot, dir=both, tailclip=false]; } ``` 3. 第 11 行: 建立兩個 linked list,用於儲存與 pivot 的值比較後的結果 ```graphviz digraph quicksort { rankdir=LR; node [shape=record]; left[label="left", shape=plaintext]; right[label="right", shape=plaintext]; lNULL[label="NULL",shape=plaintext]; rNULL[label="NULL",shape=plaintext]; left -> lNULL; right -> rNULL; } ``` 4. 第 12 到 16 行: * 依序取出 list `p` 的 node,並和 pivot 比較 * 較大的放入 right,較小的放入 left ```graphviz digraph quicksort { rankdir=LR; node [shape=record]; pivot[label="pivot", shape=plaintext]; left[label="left", shape=plaintext]; right[label="right", shape=plaintext]; l0 [label="{<data>3|<ref>}"]; l1 [label="{<data>1|<ref>}"]; l2 [label="{<data>5|<ref>}"]; l3 [label="{<data>2|<ref>}"]; l4 [label="{<data>4|<ref>}"]; pivot->l0; left->l1; right->l2; l1->l3[arrowtail=dot, dir=both, tailclip=false] l2->l4[arrowtail=dot, dir=both, tailclip=false]; } ``` 5. 第 18 到 19 行: * 將已經拆開的 left 和 right 也執行 `quicksort` * 以 right 為例重複步驟 1-4,left 以此類推 ```graphviz digraph quicksort { label="Right'"; labelloc="t"; rankdir=LR; node [shape=record]; pivot[label="pivot'", shape=plaintext]; left[label="left'", shape=plaintext]; right[label="right'", shape=plaintext]; l2 [label="{<data>5|<ref>}"]; l4 [label="{<data>4|<ref>}"]; NULL[label="NULL",shape=plaintext]; pivot->l2; left->l4; right->NULL; } ``` 6. 第 21 到 24 行: 將執行完 quicksort 的 left 和 right 與 pivot 串接起來 先執行 Right',會得到新的 right ```graphviz digraph quicksort { rankdir=LR; node [shape=record]; pivot[label="pivot", shape=plaintext]; left[label="left", shape=plaintext]; right[label="right", shape=plaintext]; l0 [label="{<data>3|<ref>}"]; l1 [label="{<data>1|<ref>}"]; l2 [label="{<data>5|<ref>}"]; l3 [label="{<data>2|<ref>}"]; l4 [label="{<data>4|<ref>}"]; pivot->l0; left->l1; right->l4; l1->l3[arrowtail=dot, dir=both, tailclip=false] l4->l2[arrowtail=dot, dir=both, tailclip=false]; } ``` 再來將 left, pivot 和 right 串接起來 ```graphviz digraph quicksort { rankdir=LR; node [shape=record]; pivot[label="pivot", shape=plaintext]; left[label="left", shape=plaintext]; right[label="right", shape=plaintext]; l0 [label="{<data>3|<ref>}"]; l1 [label="{<data>1|<ref>}"]; l2 [label="{<data>5|<ref>}"]; l3 [label="{<data>2|<ref>}"]; l4 [label="{<data>4|<ref>}"]; pivot->l0; left->l1; right->l4; l1->l3[arrowtail=dot, dir=both, tailclip=false]; l4->l2[arrowtail=dot, dir=both, tailclip=false]; l3->l0[arrowtail=dot, dir=both, tailclip=false]; l0->l4[arrowtail=dot, dir=both, tailclip=false]; } ``` 7. 第 25 行: 更新 list ```graphviz digraph quicksort { rankdir=LR; node [shape=record]; left[label="list", shape=plaintext]; l0 [label="{<data>3|<ref>}"]; l1 [label="{<data>1|<ref>}"]; l2 [label="{<data>5|<ref>}"]; l3 [label="{<data>2|<ref>}"]; l4 [label="{<data>4|<ref>}"]; left->l1; l1->l3[arrowtail=dot, dir=both, tailclip=false]; l4->l2[arrowtail=dot, dir=both, tailclip=false]; l3->l0[arrowtail=dot, dir=both, tailclip=false]; l0->l4[arrowtail=dot, dir=both, tailclip=false]; } ``` --- ## random * 從 [random](https://linux.die.net/man/3/random) 的文件可以得知,如果沒有提供 seed,會自動設定 seed 為 1。 > These sequences are repeatable by calling srandom() with the same seed value. If no seed value is provided, the random() function is automatically seeded with a value of 1. * 可以用 `srandom(time(NULL))` 提供 seed,但是 `time()` 回傳的是從 1970 年 1 月 1 號到現在的秒數,所以如果在同一秒執行多次的話還是會發現相同的輸出結果。 * [time() Reference](https://en.cppreference.com/w/c/chrono/time) * [time_t reference](https://en.cppreference.com/w/c/chrono/time_t) :::warning 為何是 1970 年呢?能否從歷史淵源探討這數值的設定? :notes: jserv ::: :::info 在 [Wikipedia](https://en.wikipedia.org/wiki/Unix_time#History) 上由提及最初的定義是從第一版「Unix Programmer's Manual」中,將 Unix 時間定義為「自 1971 年 1 月 1 日起,以六十分之一秒為單位」。這樣設計是為了符合當時 Unix 系統上的 clock rate。但是這樣設計 32 bit 的有號整數只能表示約 2.5 年。 \ ${\frac{2^{32}}{60*60*60*24*365}\approx 2.25...}$ \ 由於範圍有限,Unix 時間重新定義為「以一秒為一單位,並設定 1970 年 1 月 1 日為 0」。 :warning: 除了只從 Wikipedia 擷取字面描述,你嘗試研讀當時硬體規格和推測 clock rate 行為嗎?任何人 (扣除中國人) 都可查閱 Wikipedia 並複製貼上,高等教育不能只做到這樣。追根究底! :notes: jserv ::: * 為了修正這個問題,可以用 [entropy](https://en.wikipedia.org/wiki/Entropy_(computing)) 作為 seed。如何產生一個 entropy,我找到 `getentropy()` 或是 `getrandom()`,可以跟 Linux kernel 取得亂數。 * [初探 Linux kernel 亂數產生器](https://szlin.me/2016/10/05/%E5%88%9D%E6%8E%A2-linux-kernel-%E4%BA%82%E6%95%B8%E7%94%A2%E7%94%9F%E5%99%A8-random-generator/) * [getrandom(2) — Linux manual page ](https://man7.org/linux/man-pages/man2/getrandom.2.html) * [getentropy(3) — Linux manual page](https://man7.org/linux/man-pages/man3/getentropy.3.html) :::warning 請問教授,我知道 `random` 是根據 LCG 實作,「引入其他 Pseudorandom number generator」,是指實作其他的 PRNG 演算法嗎?例如:[The Rust Rand Book](https://rust-random.github.io/book/guide-rngs.html) 上面列出的。 > 是,若能進行數學分析更好 > :notes: jserv ::: --- ## Optimized QuickSort 下方[程式碼](https://alienryderflex.com/quicksort/)出處 ```cpp= bool quickSort(int *arr, int elements) { #define MAX_LEVELS 1000 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 false; 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 true; } ``` :::warning 提及上方程式碼的出處,及其目標 :notes: jserv ::: 為了了解程式運作,我在 `quickSort` 裡面值入 `printf` 以得到值會迴圈後對 arr 的改變。以下為程式碼,以及其對應輸出。 ```c= void print_arr(int *arr, int elements, char c) { printf("Arr(%c): ", c); for (int i = 0; i < elements; i++) { printf("%d ", arr[i]); } printf("\n"); } bool quickSort(int *arr, int elements) { #define MAX_LEVELS 1000 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 false; printf("P:%d L:%d R:%d\n", piv, L, R); print_arr(arr, elements, 'I'); while (L < R) { while (arr[R] >= piv && L < R) R--; if (L < R) arr[L++] = arr[R]; print_arr(arr, elements, 'R'); while (arr[L] <= piv && L < R) L++; if (L < R) arr[R--] = arr[L]; print_arr(arr, elements, 'L'); } arr[L] = piv; beg[i + 1] = L + 1; end[i + 1] = end[i]; end[i++] = L; print_arr(arr, elements, 'F'); } else { i--; } } return true; } int main(int argc, char const *argv[]) { int arr[] = {4, 5, 2, 1, 3}; int n = 5; quickSort(arr, n); return 0; } ``` ```= P:4 L:0 R:4 Arr(I): 4 5 2 1 3 Arr(R): 3 5 2 1 3 Arr(L): 3 5 2 1 5 Arr(R): 3 1 2 1 5 Arr(L): 3 1 2 1 5 Arr(F): 3 1 2 4 5 P:3 L:0 R:2 Arr(I): 3 1 2 4 5 Arr(R): 2 1 2 4 5 Arr(L): 2 1 2 4 5 Arr(F): 2 1 3 4 5 P:2 L:0 R:1 Arr(I): 2 1 3 4 5 Arr(R): 1 1 3 4 5 Arr(L): 1 1 3 4 5 Arr(F): 1 2 3 4 5 ``` 根據輸出以及[網站](https://alienryderflex.com/quicksort/),可以得知 * 第 27 行的 while 迴圈作用在調整陣列,將陣列分成比 pivot 大與比 pivot 小兩個部份。 * `beg` 和 `end` 兩個陣列紀錄了下一次調整陣列的範圍 * pivot 則是取在 `beg` 和 `end` 間的第一個 :::warning TODO: * graphviz 代補 * rewrite quicksort :::