# 2024q1 Homework2 (quiz1+2) >contributed by < [`pao0626`](https://github.com/pao0626) > ## [quiz1](https://hackmd.io/@sysprog/linux2024-quiz1) ### 測驗一 在鏈結串列的資料結構上,實作非遞迴的快速排序法。使用推疊來紀錄每次要處理的資料,進而模擬遞迴呼叫。 #### 解釋程式碼 主要變數: ```c int i = 0; node_t *begin[max_level], *end[max_level]; node_t *result = NULL, *left = NULL, *right = NULL; begin[0] = *list; end[0] = list_tail(list) ``` - `i` 代表堆疊內元素個數。 - `begin` 陣列和 `end` 陣列追蹤每次處理完 `pivot` 後尚未排序的部分,表示區間的頭尾。 - `left` 和 `right` 分別存放每次處理堆疊元素時小於和大於 `pivot` 的節點,以便更新堆疊。 - `result` 用於儲存排序後的結果。 程式初始化時,將 `begin[0]` 和 `end[0]` 設置,表示整個鏈結串列尚未排序,並將其放入堆疊中以便處理。當堆疊不為空時,每次使用 `L` 和 `R` 指標從堆疊中取出待處理的未排序節點區間,可能會出現以下兩種情況: - 情況1: `L` 和 `R` 指標指向同一位置,表示此區間不需要進行排序,因此將其直接加入 `result` 中,然後更新 `i` ,表示將堆疊中的頂元素彈出 - 情況2: `L` 和 `R` 指標指向不同位置,則表示此區間需要進行排序。將 `pivot` 設置為區間的首節點,走訪區間中的節點,根據它們與 `pivot` 的大小關係將它們分別加入 `left` 和 `right` 中,並同步更新鍊結串列的連接順序。 ```c if (L != R) { ... while (p) { node_t *n = p; p = p->next; //CCCC list_add(n->value > value ? &right : &left, n); } ... } else { if (L) list_add(&result, L); i--; } ``` 如果是情況2,對於未排序區間節點的每次操作僅涉及確保 `pivot` 所指向的節點位置正確,即確保在其左邊的節點都小於它,而在右邊的節點都大於它。然後,在彈出堆疊頂部元素後,將這三種區間分別添加到堆疊中以進行後續處理。 ```c begin[i] = left; end[i] = list_tail(&left); //DDDD begin[i + 1] = pivot; end[i + 1] = pivot; begin[i + 2] = right; end[i + 2] = list_tail(&right); //EEEE left = right = NULL; i += 2; ``` 以下以$[4,1,3,5,2]$為例進行圖解: **步驟1.** 初始化,將整個鍊結串列用 `begin` 和 `end` 表示區間後放入堆疊: ```graphviz digraph graphname{ node[shape=record]; rankdir = LR map [label="stack | [4,2] "]; } ``` **步驟2.** 當堆疊非空, `L` 和 `R` 指向堆疊頂元素之區間頭尾節點, `pivot` 指向此區間首元素: ```graphviz digraph structs { node[shape=plaintext]; Pivot [fontcolor="red"]; L [fontcolor="blue"]; R [fontcolor="blue"]; node[shape=box]; struct0 [label= "4"]; struct1 [label= "1"]; struct2 [label= "3"]; struct3 [label= "5"]; struct4 [label= "2"]; { rank="same"; struct0 -> struct1 struct1 -> struct2 struct2 -> struct3 struct3 -> struct4 } Pivot -> struct0; L -> struct0; R -> struct4; } ``` **步驟3.** 使用 `n` 和 `p` 一前一後走訪所有節點: ```graphviz digraph structs { node[shape=plaintext]; Pivot [fontcolor="red"]; n [fontcolor="black"]; p [fontcolor="black"]; node[shape=box]; struct0 [label= "4"]; struct1 [label= "1"]; struct2 [label= "3"]; struct3 [label= "5"]; struct4 [label= "2"]; { rank="same"; struct1 -> struct2 struct2 -> struct3 struct3 -> struct4 } Pivot -> struct0; n -> struct1; p -> struct2; } ``` **步驟4.** 將所有小於和大於 `pivot` 得節點分別暫時用 `left` 和 `right` 紀錄後,更新鍊結串列和堆疊: - 以第一個節點 `1` 為例: ```graphviz digraph structs { node[shape=plaintext]; left [fontcolor="blue"]; Pivot [fontcolor="red"]; n [fontcolor="black"]; p [fontcolor="black"]; node[shape=box]; struct0 [label= "4"]; struct1 [label= "1"]; struct2 [label= "3"]; struct3 [label= "5"]; struct4 [label= "2"]; { rank="same"; struct2 -> struct3 struct3 -> struct4 } Pivot -> struct0; left -> struct1; n -> struct2; p -> struct3; } ``` - 全部處理後會變成: ```graphviz digraph structs { node[shape=plaintext]; left [fontcolor="blue"]; Pivot [fontcolor="red"]; right [fontcolor="blue"]; node[shape=box]; struct0 [label= "4"]; struct1 [label= "1"]; struct2 [label= "3"]; struct3 [label= "5"]; struct4 [label= "2"]; { rank="same"; struct4 -> struct2 struct2 -> struct1 } Pivot -> struct0; left -> struct4; right -> struct3; } ``` - 更新堆疊: ```graphviz digraph structs { node[shape=plaintext]; begin_0 [fontcolor="blue"]; begin_1 [fontcolor="blue"]; begin_2 [fontcolor="blue"]; end_0 [fontcolor="red"]; end_1 [fontcolor="red"]; end_2 [fontcolor="red"]; node[shape=box]; struct0 [label= "4"]; struct1 [label= "1"]; struct2 [label= "3"]; struct3 [label= "5"]; struct4 [label= "2"]; { rank="same"; struct4 -> struct2 struct2 -> struct1 } begin_0 -> struct4; end_0 -> struct1; begin_1 -> struct0; end_1 -> struct0; begin_2 -> struct3; end_2 -> struct3; } ``` ```graphviz digraph graphname{ node[shape=record]; rankdir = LR map [label="stack | [5,5] | [4,4] | [2,1] "]; } ``` **步驟5.** 觀察堆疊情形可以看出節點5和節點4已經處理好了,只要重複**步驟2**繼續處理區間[2.1],直到堆疊為空即可。 - 將堆疊中處理好的節點更新至 `result` 中: ```graphviz digraph graphname{ node[shape=record]; rankdir = LR map [label="stack | [2,1] "]; } ``` ```graphviz digraph structs { node[shape=plaintext]; Pivot [fontcolor="red"]; L [fontcolor="blue"]; R [fontcolor="blue"]; result [fontcolor="black"]; node[shape=box]; struct0 [label= "4"]; struct1 [label= "1"]; struct2 [label= "3"]; struct3 [label= "5"]; struct4 [label= "2"]; { rank="same"; struct0 -> struct3 struct4 -> struct2 struct2 -> struct1 } Pivot -> struct4; L -> struct4; R -> struct1; result -> struct0; } ``` #### 改進方法 - 避免選擇鏈結串列中極值作為 pivot 而導致的 worst case ,嚴格制定 pivot 的選擇方式。 - TODO: 設計效能評比的測試程式,證明能避免"最差"狀況。 #### 測驗問題 - 題目敘述提到鏈結串列結構體的程式碼疑似有錯,應該不需要 `*left` 和 `*right` 。 ```c typedef struct __node { struct __node *left, *right; struct __node *next; long value; } node_t; ``` ### 測驗二 #### Timsort 簡介 其優勢在於對於部分已排序的資料有出色表現,透過找尋資料中已排序的子序列 (run) 來減少合併的次數。但 run 的數量也會影響到效率,因此透過設計 minrun 的長度使其能將資料切分成略小於等於 2 的冪,並用二分插入排序修改某些 run 使其符合條件,就是 Timsort 的關鍵。 Timsort 相較於合併排序,有以下改善。 - [ ] 降低 cache miss 傳統的合併排序非遞迴與遞迴作法都會需要掃過整個序列,而 Timesort 一邊走訪,就一邊找機會合併,類似於 Linux 核心的合併排序實作。 - [ ] 降低記憶體開銷 Timsort 只針對範圍重疊的部份進行合併,且只分配二個要合併的部份中較小那一部份元素數量的記憶體。 - [ ] 降低比較次數 由於二個要合併的數列都是已排序好的,可以使用 Galloping mode 方法。 #### 解釋程式碼 ##### `find_run` ```c static struct pair find_run(void *priv, struct list_head *list, list_cmp_func_t cmp) ``` 用於找尋一個升序的 run ,若降序則反轉成升序。其中有趣的部份是在走訪的過程中會維護 `len` 變數來紀錄 run 的大小,並在最後強制轉型給 `head->next->prev` 儲存。這也反應了為什麼 `run_size` 函式中可以使用 `return (size_t) (head->next->prev);` 來取得 run 大小。函式最後回傳一個配對,其中包含指針 head 指向此 run 的開頭,指針 next 指向下個待處理節點。 ##### `merge_collapse` ```c static struct list_head *merge_collapse(void *priv, list_cmp_func_t cmp, struct list_head *tp) ``` 使用 `stk_size` 紀錄待合併 run ,當超過 2 個就啟動 `merge_collapse` 進行判斷,一旦符合條件則啟用 `merge_at` 進行合併,確保待合併 run 之間長度的平衡,並將結果回傳給 `tp` 指針紀錄。 ##### `merge_at` ```c static struct list_head *merge_at(void *priv, list_cmp_func_t cmp, struct list_head *at) ``` `at` 指標參數會接受一個 `tp` 指標來判斷預合併的位置以及 run 大小,之後用 `merge` 進行合併。 ##### `merge` `mrege` 作用就是合併兩個 run ,與我習慣的寫法不同,採取指標的指標實作。 以下是我習觀的寫法: ```diff static struct list_head *merge(void *priv, list_cmp_func_t cmp, struct list_head *a, struct list_head *b) { - struct list_head *head; - struct list_head **tail = &head; + struct list_head head; + struct list_head *tail = &head; for (;;) { if (cmp(priv, a, b) <= 0) { - *tail = a; - tail = &a->next; + tail->next = a; + tail = tail->next; a = a->next; if (!a) { - *tail = b; + tail->next = b; break; } } else { - *tail = b; - tail = &b->next; + tail->next = b; + tail = tail->next; b = b->next; if (!b) { - *tail = a; + tail->next = a; break; } } } - return head; + return head.next; } ``` ##### 過程圖示 > 參考 <[marvin0102](https://github.com/marvin0102/lab0-c)> 畫圖方法 以題目序列作為範例: ```graphviz digraph list { node [shape="record"]; rankdir = "LR"; n0[label = "head"]; n1[label = "1"]; n2[label = "2"]; n3[label = "3"]; n4[label = "4"]; n5[label = "3"]; n6[label = "2"]; n7[label = "4"]; n8[label = "7"]; n9[label = "8"]; n0->n1; n1->n2; n2->n3; n3->n4; n4->n5; n5->n6; n6->n7; n7->n8; n8->n9; } ``` 執行第一次 `find_run` 。 ```graphviz digraph list { node[shape=plaintext]; "result.head" [fontcolor="blue"]; "result.next" [fontcolor="blue"]; node [shape="record"]; rankdir = "LR" n0[label = "head"]; n1[label = "1", color = red]; n2[label = "2", color = red]; n3[label = "3", color = red]; n4[label = "4", color = red]; n5[label = "3"]; n6[label = "2"]; n7[label = "4"]; n8[label = "7"]; n9[label = "8"]; n0->n1; "result.head"->n1; n1->n2; n2->n3; n3->n4; n4->n5[style = invis]; "result.next"->n5; n5->n6; n6->n7; n7->n8; n8->n9; } ``` 執行完三次 `find_run` 。 ```graphviz digraph list { node[shape=plaintext]; tp [fontcolor="blue"]; "tp->prev" [fontcolor="blue"]; "tp->prev->prev" [fontcolor="blue"]; node [shape="record"]; rankdir = "LR" n1[label = "1"]; n2[label = "2"]; n3[label = "3"]; n4[label = "4"]; n5[label = "3"]; n6[label = "2"]; n7[label = "4"]; n8[label = "7"]; n9[label = "8"]; "tp->prev->prev"->n1; n1->n2; n2->n3; n3->n4; n4->n6[style = invis]; "tp->prev"->n6; n6->n5; n5->n7[style = invis]; tp->n7; n7->n8; n8->n9; } ``` 符合 `merge_collapse` 條件 “`tp->prev->prev`<=`tp->prev`+`tp`,且`tp->prev`<`tp`” ,因此執行 `merge_at` 將 `tp` 和 `tp->prev` 合併。 ```graphviz digraph list { node[shape=plaintext]; tp [fontcolor="blue"]; "tp->prev" [fontcolor="blue"]; node [shape="record"]; rankdir = "LR" n1[label = "1"]; n2[label = "2"]; n3[label = "3"]; n4[label = "4"]; n5[label = "3"]; n6[label = "2"]; n7[label = "4"]; n8[label = "7"]; n9[label = "8"]; "tp->prev"->n1; n1->n2; n2->n3; n3->n4; n4->n6[style = invis]; tp->n6; n6->n5; n5->n7; n7->n8; n8->n9; } ``` 再一次的 `merge_collapse` ,條件為 “`tp->prev`<=`tp` ,所以執行 `merge_at` 將 `tp` 和 `tp->prev` 合併。 ```graphviz digraph list { node[shape=plaintext]; tp [fontcolor="blue"]; node [shape="record"]; rankdir = "LR" n1[label = "1"]; n2[label = "2"]; n3[label = "3"]; n4[label = "4"]; n5[label = "3"]; n6[label = "2"]; n7[label = "4"]; n8[label = "7"]; n9[label = "8"]; "tp"->n1; n1->n2; n2->n6; n6->n3; n3->n5; n5->n4; n4->n7; n7->n8; n8->n9; } ``` 最後透過 `build_prev_link` 重建雙向鏈結串列。 #### 改進方法 - 此程式碼並沒有使用到 Galloping mode 的合併方法。 - 困惑之處:此程式碼實作透過 merge_collapse 函式確保堆疊上的 run 長度保持平衡,這與設計 minrun 長度的方法是不是只能二選一,因為我的理解是 minrun 就代表了每個 run 的固定長度。 - TODO: 將 timsort 優化後整合進 lab0-c 。 ## [quiz2](https://hackmd.io/@sysprog/linux2024-quiz2) ### 測驗一 >參考 [核心基礎設施: hlist_head/hlist_node 結構解析](https://linux.laoqinren.net/kernel/hlist/) >參考 [Linux 核心的 hash table 實作 ](https://hackmd.io/@sysprog/linux-hashtable) 使用 dfs 方法,透過前序走訪結果和中序走訪結果來重建二元樹。需要先將中序走訪結果用雜湊函數儲存,才能確認遞迴參數範圍。 第一次接觸到 c 語言的雜湊函數,詳細內容可見以上兩篇參考文章。 #### 解釋程式碼 > 參考 < [HenryChaing](https://hackmd.io/O_Me7VZOSX2N_yne2I05lQ?view) > 畫圖方法 ##### `hlist_add_head` ```c static inline void hlist_add_head(struct hlist_node *n, struct hlist_head *h) { if (h->first) h->first->pprev = &n->next; n->next = h->first; /AAAA n->pprev = &h->first; h->first = n; } ``` 於雜湊表中找到索引 h ,用 `h->first` 判斷該 bucket 是否已有節點後再插入新節點 n。 假設已有節點 `hlist_node_1` 如下所示: ```graphviz digraph G { rankdir = LR; node[shape = "record"] list_head[label = "<m>list_head | <n>first"] node_1[label = "<m>hlist_node_1 | {<p>pprev | <n>next}"]; node[shape=plaintext]; NULL; list_head -> node_1[style = invis] //不可見箭頭確保順序 list_head:n -> node_1:m; node_1:p -> list_head:n; node_1:n -> NULL; } ``` 插入節點 n 如下所示: ```graphviz digraph G { rankdir = LR; node[shape = "record"] list_head[label = "<m>list_head | <n>first"] node_1[label = "<m>hlist_node_1 | {<p>pprev | <n>next}"]; n[label = "<m>n | {<p>pprev | <n>next}"]; node[shape=plaintext]; NULL; list_head -> n[style = invis] n -> node_1[style = invis] list_head:n -> n:m[color = red]; n:p -> list_head:n[color = red]; n:n -> node_1:m[color = red]; node_1:p -> n:n[color = red]; node_1:n -> NULL; } ``` ##### `order_node` ```c struct order_node { struct hlist_node node; int val; int idx; }; ``` 用來存放雜湊表的 `hlist_node` 節點、序列的值和中序序列所在位置之索引值的結構體。 ##### `find` ```c static int find(int num, int size, const struct hlist_head *heads) { struct hlist_node *p; int hash = (num < 0 ? -num : num) % size; hlist_for_each (p, &heads[hash]) { //BBBB struct order_node *on = container_of(p, struct order_node, node); //CCCC if (num == on->val) return on->idx; } return -1; } ``` 根據雜湊表索引找出對應的鏈結串列串列,再透過 `hlist_for_each` 逐一走訪節點找出對應值的節點,最後返回該節點所存的中序序列索引值。 ##### `雜湊表架構圖示` ```graphviz digraph G { rankdir=LR; node[shape=record]; map [label="<ht0>heads[0]| <ht1>heads[1]|<ht2>heads[2]|<ht3>...| <ht4>... | <ht5>...|<ht6>heads[inorderSize - 1]"]; node[shape=plaintext] in_heads[label=in_heads] null1[label=NULL] null2[label=NULL] subgraph cluster_1 { style=filled; color=lightgrey; node [shape=record]; hn1 [label="<m>hlist_node | {<p>pprev | <n>next}"]; label="order_node 1" } subgraph cluster_2 { style=filled; color=lightgrey; node [shape=record]; hn2 [label="<m>hlist_node | {<p>pprev | <n>next} "]; label="order_node 2" } subgraph cluster_3 { style=filled; color=lightgrey; node [shape=record]; hn3 [label="<m>hlist_node | {<p>pprev | <n>next}"]; label="order_node 3" } in_heads -> map:ht0 map:ht1 -> hn1:m hn1:p -> map:ht1 hn1:n -> hn2:m hn2:n -> null1 hn2:p -> hn1:n map:ht5 -> hn3:m hn3:n -> null2 hn3:p -> map:ht5 } ``` #### 改進方法 - 取 hash 值的方法可以使用參考文獻中提及的黃金比例。 - 改寫 dfs 函式,因為其實沒有必要追蹤 preorder idx ,只要設置成全域變數後逐一遞增即可,如此即可有效減少遞迴函式所需傳入的參數量。 ##### `dfs` ```diff static struct TreeNode *buildTree(int *preorder, int preorderSize, int *inorder, int inorderSize) { struct hlist_head *in_heads = malloc(inorderSize * sizeof(*in_heads)); for (int i = 0; i < inorderSize; i++) INIT_HLIST_HEAD(&in_heads[i]); for (int i = 0; i < inorderSize; i++) node_add(inorder[i], i, inorderSize, in_heads); - return dfs(preorder, 0, preorderSize - 1, inorder, 0, inorderSize - 1, in_heads, inorderSize); + return dfs(preorder, inorder, 0, inorderSize - 1, in_heads, inorderSize); } -static struct TreeNode *dfs(int *preorder, int pre_low, int pre_high, int *inorder, int in_low, int in_high, struct hlist_head *in_heads, int size) +static struct TreeNode *dfs(int *preorder, int *inorder, int in_low, int in_high, struct hlist_head *in_heads, int size) { + static pre_idx = 0; if (in_low > in_high) return NULL; struct TreeNode *tn = malloc(sizeof(*tn)); - tn->val = preorder[pre_low]; + tn->val = preorder[pre_idx]; + pre_idx++; - int idx = find(preorder[pre_low], size, in_heads); + int idx = find(preorder[pre_idx], size, in_heads); - tn->left = dfs(preorder, pre_low + 1, pre_low + (idx - in_low), inorder, in_low, idx - 1, in_heads, size); + tn->left = dfs(preorder, inorder, in_low, idx - 1, in_heads, size); - tn->right = dfs(preorder, pre_high - (in_high - idx - 1), pre_high, inorder, idx + 1, in_high, in_heads, size); + tn->right = dfs(preorder, inorder, idx + 1, in_high, in_heads, size); return tn; } ``` ### 測驗二 ### 測驗三