# 2024q1 Homework2 (quiz1+2) contribute by <`Hualing-Chiu`> ## [第一週測驗題](https://hackmd.io/@sysprog/linux2024-quiz1) ### 測驗一 > 延伸問題: > 1. 解釋上述程式碼的運作原理,提出改進方案並予以實作。 > 2. 使用 Linux 核心風格的 List API 改寫上述程式碼,並針對鏈結串列,提出可避免最差狀況的快速排序實作,應設計效能評比的測試程式。 #### 運作原理 ##### `list_add` 此函式的目的為在鏈結串列前端加入一個新節點,`node_t` 是要被加入的新節點,當節點加入完成後,將 `*list` 指回串列的開頭。 ```c void list_add(node_t **list, node_t *node_t) { node_t->next = *list; *list = node_t; } ``` ##### `list_tail` 此函式目的在於找到串列尾端,這裡使用間接指標來操作,不斷向後更新,因此 `AAAA` 應為 `(*left)->next`。 ```c node_t *list_tail(node_t **left) { while ((*left) && (*left)->next) left = &((*left)->next); return *left; } ``` ##### `list_length` 此函式是在計算整個串列的長度,當判斷 `*left` 不為 `null` 時,不斷將指標向後更新,因此 `BBBB` 應為 `(*left)->next`。 ```c int list_length(node_t **left) { int n = 0; while (*left) { ++n; left = &((*left)->next); } return n; } ``` ##### `quick_sort` 假設原本的鏈結串列為下: 每次都挑選最左邊的節點為 pivot,在進入 `while` 迴圈之前,將 `begin[0]` 及 `end[0]`分別指向串列左端節點及右端節點,進入迴圈後,`L` 指向 `begin[0]`的節點,`R` 指向 `end[0]` 的節點,`p` 則是會向後更新去走訪每個節點。 ```c while (p) { node_t *n = p; p = p->next; list_add(n->value > value ? &right : &left, n); } ``` ```graphviz digraph structs { node[shape=box]; struct0 [label= "3"]; struct1 [label= "5"]; struct2 [label= "4"]; struct3 [label= "1"]; struct4 [label= "2"]; node[shape=plaintext]; pivot; L [fontcolor="blue"]; R [fontcolor="red"]; p { rank="same"; struct0 -> struct1 struct1 -> struct2 struct2 -> struct3 struct3 -> struct4 } pivot -> struct0; L -> struct0; R -> struct4; p -> struct1; } ``` 當`n->value > value` 條件成立時,會將當前指向的節點加到 `right` 的開頭,反之則加到 `left`的開頭。待 `p` 走訪完整個串列後,會將串列分為 `left`、`right`、`pivot`三個部分,且 `begin[0]` 與 `end[0]` 分別指向 `left` 串列的頭跟尾,`begin[1]` 與 `end[1]` 指向 `pivot`,`begin[2]` 與 `begin[2]` 分別指向 `right` 串列的頭跟尾。 ```c begin[i] = left; end[i] = list_tail(left); begin[i + 1] = pivot; end[i + 1] = pivot; begin[i + 2] = right; end[i + 2] = list_tail(right); ``` ```graphviz digraph structs { node[shape=box]; struct0 [label= "2"]; struct1 [label= "1"]; struct2 [label= "3"]; struct3 [label= "4"]; struct4 [label= "5"]; node[shape=plaintext]; pivot; left; right; begin0 [fontcolor="blue"]; end0 [fontcolor="red"]; begin1 [fontcolor="blue"]; end1 [fontcolor="red"]; begin2 [fontcolor="blue"]; end2 [fontcolor="red"]; { rank="same"; struct0->struct1 left->struct0 pivot->struct2 struct3->struct4 right->struct3 } begin0->struct0 end0->struct1 begin1->struct2 end1->struct2 begin2->struct3 end2->struct4 } ``` 當 `L` 與 `R` 從堆疊中取出相同值的情況下,代表子串列只剩下單一元素,即可加入到 `result` 的開頭,就可得到排序好的串列。 ```c } else { if (L) list_add(&result, L); i--; } ``` ### 測驗二 > 延伸問題 > 1. 解釋上述程式碼的運作原理,提出改進方案並予以實作。 > 2. 將改進過的 Timsort 實作整合進 [sysprog21/lab0-c](https://github.com/sysprog21/lab0-c),作為另一個有效的排序命令,應設計效能評比的測試程式,確保測試案例符合 Timsort 設想的資料分布樣式。 #### [Timsort](https://en.wikipedia.org/wiki/Timsort) 運作原理 Timsort 是結合合併排序和插入排序的特點,對排序進行優化,尤其是在已經部分排序的序列,先識別出資料中已排序的子序列( run ),透過不斷合併這些 run 來達成全資料範圍的排序。 接著對 Timsort 程式碼進行探討,以圖示為例: 原始的鏈結串列為雙向鏈結串列 ```graphviz digraph { node [shape=box] graph [rankdir = "LR"]; head -> 1 -> 2 -> 3 -> 8 -> 7 -> 5 -> 6 -> head; head -> 6 -> 5 -> 7 -> 8 -> 3 -> 2 -> 1 -> head; } ``` 在 `timsort` 函式中,首先先將鏈結串列斷開改成單向串列 ```c head->prev->next = NULL; ``` ```graphviz digraph { node [shape=box] graph [rankdir = "LR"]; head->1->2->3->8->7->5->6; 6->5->7->8->3->2->1->head; node[shape=plaintext]; 6->NULL } ``` 接著是 `find_run` 函式,尋找串列中已經排序好的 run,使用 `len` 計算 run 的長度,以下用圖示說明 `find_run` 函式的作法: * 串列是**升序( ascending )** * 首先將 `head` 指向開頭,`next` 指向 `list` 的下一個節點 ```graphviz digraph { node [shape=box] // graph [rankdir = "LR"]; { rank = "same" 1->2->3->8->7->5->6; } node[shape=plaintext]; head->1 list->1 next->2 { rank = "same" 6->NULL } } ``` * 若 `next` 不為空且 `cmp(priv, list, next) <= 0` 成立,則指標持續向後更新,直到找到非升序的串列為止。 ```graphviz digraph { node [shape=box] // graph [rankdir = "LR"]; { rank = "same" 1->2->3->8->7->5->6; } node[shape=plaintext]; head->1 list->3 next->8 { rank = "same" 6->NULL } } ``` * 當找到非升序的節點後,斷開它與前一個節點後回傳 `result.head` 與 `result.next`。 ```graphviz digraph { node [shape=box] // graph [rankdir = "LR"]; { rank = "same" 1->2->3; 8->7->5->6 } node[shape=plaintext]; result_head->1 list->3 result_next->8 { rank = "same" 3->NULL } } ``` * 串列是**降序( descending )** * 新增一個 `prev` 指標實作反轉序列 ```graphviz digraph { node [shape=box] // graph [rankdir = "LR"]; { rank = "same" 8->7->5->6 } node[shape=plaintext]; head->8 list->8 next->7 prev { rank = "same" 6->NULL } } ``` * 把需被反轉的節點給 `prev` ```graphviz digraph { node [shape=box] // graph [rankdir = "LR"]; { rank = "same" 7->5->6 8 } node[shape=plaintext]; head->7 prev->8 list->7 next->5 { rank = "same" // 6->NULL 8->NULL } } ``` * 若 `next` 不為空且 `cmp(priv, list, next) <= 0` 則繼續走訪,否則離開迴圈。 ```graphviz digraph { node [shape=box] // graph [rankdir = "LR"]; { rank = "same" 5->6 7->8 } node[shape=plaintext]; head->5 prev->7 list->5 next->6 { rank = "same" // 6->NULL 8->NULL } } ``` * 離開迴圈後回傳 `result.head` 與 `result.next`,可以發現此段子序列已排序完成。 ```graphviz digraph { node [shape=box] // graph [rankdir = "LR"]; { rank = "same" 5->7->8 6 } node[shape=plaintext]; result_head->5 // prev->7 // list->5 result_next->6 { rank = "same" 8->NULL } } ``` 再來執行 `merge_collapse` 函式,此函式目的為確保堆疊上的 run 長度保持平衡,此判斷條件就是測驗二題目所描述的 $A<=B+C$ ```c n >= 3 && run_size(tp->prev->prev) <= run_size(tp->prev) + run_size(tp) ``` 此外,還多了一個判斷條件為假設尚未合併的 run 有四個以上,若 $A<=B+C$ 或 $B<=C+D$,則 $C$ 和 $B$ 或 $D$ 選長度小的兩者合併。 ```c n >= 4 && run_size(tp->prev->prev->prev) <= run_size(tp->prev->prev) + run_size(tp->prev) ``` 最後,確保堆疊內的 run 長度都達到平衡後,執行 `merge_force_collapse` 函式將剩餘的 run 全部合併,接著把雙向鏈結接上變為雙向串列,即完成 Timsort 排序。 #### 執行輸出 ``` Creating sample ==== Testing timesort ==== Comparisons: 9490 List is sorted ``` ## [第二週測驗題](https://hackmd.io/@sysprog/linux2024-quiz2) ### 測驗一 首先需先了解 preorder 與 postorder 的定義 * preorder ### 測驗二 ### 測驗三