# 2024q1 Homework2 (quiz1+2) contributed by < [youjiaw](https://github.com/youjiaw/lab0-c) > ## 第 1 週測驗題 ### 測驗 `1` 鏈結串列結構體如下: ```c typedef struct __node { struct __node *left, *right; struct __node *next; long value; } node_t; ``` ```graphviz digraph { node [shape="record"]; rankdir = "LR" subgraph cluster_node { label = "node_t"; node_t [label="<v>value|<n>next|{<l>left|<r>right}"]; } } ``` <!-- 對應的操作函式有以下五個: * `void list_add(node_t **list, node_t *node_t)` * `node_t *list_tail(node_t **left)` * `int list_length(node_t **left)` * `node_t *list_construct(node_t *list, int n)` * `void list_free(node_t **list)` --> #### 1. 補完程式碼,使其得以符合預期運作 **`*list_tail`** * 經由迴圈不斷將 `left` 指向下一個節點,最後回傳鏈結串列的最後一個節點,因此 `AAAA` 為 `(*left)->next`。 ```diff node_t *list_tail(node_t **left) { while ((*left) && (*left)->next) - left = &(AAAA); + left = &((*left)->next); return *left; } ``` **`list_length`** * 與前者相同,使用迴圈逐一走訪每個節點來計算長度,因此 `BBBB` 同為`(*left)->next`。 ```diff int list_length(node_t **left) { int n = 0; while (*left) { ++n; - left = &(BBBB); + left = &((*left)->next); } return n; } ``` **`quick_sort`** * 下方程式碼會將鏈結串列中,大於 `pivot` 的節點加到 `right`,小於等於 `pivot` 的節點加到 `left`,因此 `CCCC` 為 `p->next`。 ```diff while (p) { node_t *n = p; - p = CCCC; + p = p->next; list_add(n->value > value ? &right : &left, n); } ``` * 走訪所有節點後,將 `left`, `pivot` 及 `right` 的範圍資訊存放在 `begin` 及 `end`。 * 因此,`DDDD` 為 `list_tail(&left)`,`EEEE` 為 `list_tail(&right)`。 ```diff begin[i] = left; - end[i] = DDDD; + end[i] = list_tail(&left); begin[i + 1] = pivot; end[i + 1] = pivot; begin[i + 2] = right; - end[i + 2] = EEEE; + end[i + 2] = list_tail(&right); ``` #### 2. 解釋程式碼的運作原理,提出改進方案並予以實作 #### 運作流程: 1. 將長度為 count,值為 0 至 count-1 的陣列進行 shuffle。 1. 依照陣列的值,依序建立鏈結串列的節點。 1. 進行非遞迴的快速排序法,最後檢查排序結果。 其中,非遞迴的快速排序法是用堆疊模擬遞迴呼叫。 * 一開始,用來記錄範圍的指標 `L` 與 `R` 分別為鏈結串列的開頭與結尾, `pivot` 為開頭節點,用來走訪節點的 `p` 則是 `pivot->next`。 * 將 `value` 設為 `pivot` 的值,並把 `pivot->next` 指向 NULL。 ```graphviz digraph g_1 { node [shape=box, fontsize=15, fontcolor=black]; 2 -> 5 [style=dashed]; 5 -> 3 -> 4 -> 1; value [shape=plaintext, fontsize=15, label="value",fontcolor=cadetblue]; v [label="2", color=cadetblue, fontcolor=cadetblue]; value -> v [style=invis]; value -> v [style=invis]; v -> 2[style=invis]; node [shape=plaintext, fontsize=15]; pivot [fontcolor=red]; L [fontcolor=blue]; R [fontcolor=blue]; pivot -> 2 [color=red]; L -> 2 [color=blue]; R -> 1 [color=blue]; p -> 5 ; { rank=same; v 4 5 3 2 1; }; { rank=same; value pivot L R;}; } ``` * 用指標 `n` 指向 `p` 並逐一走訪節點。 * 若 `n->value` > `value`:將此節點新增至 `right`。 * 若 `n->value` <= `value`:將此節點新增至 `left`。 ```graphviz digraph g_3 { node [shape=box, fontsize=15, fontcolor=black]; 1; 2; 4 -> 3 -> 5; node [shape=plaintext, fontsize=15]; left [fontcolor=blue]; pivot [fontcolor=red] right [fontcolor=blue]; left -> 1; pivot -> 2; right -> 4; // {rank=same; left 1;} // {rank=same; right 3 4 5;} } ``` * 最後,將 `left`, `pivot` 及 `right` 的範圍記錄在 `begin` 與 `end`。 * 迴圈會不斷排序 `right`,直到剩下一個節點,並將其加到 `result` 的開頭,再向左邊進行排序,因此 `result` 中的節點最後會是由小排到大。 #### 改進方案: :::info 我的想法為: * `begin` 與 `end` 是否真的需要兩倍的空間? * 目前是假設 malloc 總是成功,因此需要加入檢查的程式碼。 * 使用雙向鏈結串列。 ::: **決定 `begin` 與 `end` 的空間** 經過測試,在有 shuffle 的狀況下,list 的長度每增加十倍,`begin` 與 `end` 的最大深度只增加不到一倍。 但是若有 worst case 出現,就會需要 **2 * n - 1** 的空間,以下為 n = 10 時的 worst case,此時 `begin` 與 `end` 的最大深度為 19。 ```c int arr[] = {0,2,4,6,8,9,7,5,3,1}; ``` **檢查 malloc 是否成功** 參照 [tools/perf/util/intlist.c](https://elixir.bootlin.com/linux/latest/source/tools/perf/util/intlist.c#L18) 的程式碼,加入 malloc 的檢查。 :::info 查閱 Linux 核心程式碼時,發現 [scripts/kconfig/lxdialog/checklist.c](https://elixir.bootlin.com/linux/latest/source/scripts/kconfig/lxdialog/checklist.c#L21) 並沒有在 malloc 後進行檢查,這是為什麼? ::: **使用雙向鏈結串列** 雙向鏈結串列可以改善以下的函式: * `*list_tail(node_t **left)` 的時間複雜度由 O(N) 變為 O(1)。 * `quick_sort(node_t **list)` 的 `end` 可以移除。 :::warning TODO: 改為雙向鏈結串列 ::: #### 3. 使用 Linux 核心風格的 List API 改寫上述程式碼。 於 commit [0286652](https://github.com/youjiaw/linux2024-homework/commit/028665250ff73d965896de6102e8273615b785cf) 改寫。 --- ### 測驗 `2` #### 1. 補完程式碼,使其得以符合預期運作 **`*merge`** `merge` 會把兩個已排序的鏈結串列,合併成一個新的已排序鏈結串列,用 `*head` 與 `**tail` 紀錄。 * 一開始鏈結串列是空的,所以將 `**tail` 初始化為 `&head`。 ```diff struct list_head *head; - struct list_head **tail = AAAA; + struct list_head **tail = &head; ``` * 接著比較 `a` 與 `b` 的第一個節點,將比較小的節點加到新的鏈結串列尾部,因此 `BBBB` 為 `&a->next`,`CCCC` 則為 `&b->next`。 <!-- :::spoiler `merge` 程式碼 --> ```diff if (cmp(priv, a, b) <= 0) { *tail = a; - tail = BBBB; + tail = &a->next; a = a->next; if (!a) { *tail = b; break; } } else { *tail = b; - tail = CCCC; + tail = &b->next; b = b->next; if (!b) { *tail = a; break; } } ``` <!-- ::: --> **`build_prev_link`** 在走訪所有節點的同時建立 `prev`,走訪到尾部就會跳出迴圈,因此最後要將頭尾相連。 <!-- :::spoiler `build_prev_link` 程式碼 --> ```diff - DDDD = head; - EEEE = tail; + tail->next = head; + head->prev = tail; ``` <!-- ::: --> **`timsort`** Timsort 最後會確認是否有剩下的節點,如果有就會做 `merge_final`,因此 `FFFF` 為1。 ```diff - if (stk_size <= FFFF) { + if (stk_size <= 1) { build_prev_link(head, head, stk0); return; } merge_final(priv, cmp, head, stk1, stk0); ``` #### 2. 解釋程式碼的運作原理,提出改進方案並予以實作 #### 3. 將改進過的 Timsort 實作整合進 [sysprog21/lab0-c](https://github.com/sysprog21/lab0-c) --- ## 第 2 週測驗題 ### 測驗 `1` #### 1. 補完程式碼,使其得以符合預期運作 **`hlist_add_head`** 把節點加入到開頭位置,要將新節點的指標指向"原本的開頭節點"和"指向原本開頭節點的指標"。 ```diff if (h->first) h->first->pprev = &n->next; - n->next = AAAA; + n->next = h->first; n->pprev = &h->first; h->first = n; ``` **`node_add`** 依照節點的值,對 `size` 取餘數得到相對應的 hash 值,並呼叫 `hlist_add_head` 將該節點加到該雜湊表中,即 `&heads[hash]`。 ```diff int hash = (val < 0 ? -val : val) % size; - hlist_add_head(&on->node, DDDD); + hlist_add_head(&on->node, &heads[hash]); ``` **`find`** `find` 的目的是找出節點在中序的位置,因此取得 `hash` 值後,會用迴圈找尋對應的雜湊表 `&heads[hash]` 是否有符合該節點值的 `order_node`,並返回該節點的 index。 ```diff 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, BBBB) { - struct order_node *on = CCCC(p, struct order_node, node); + hlist_for_each (p, &heads[hash]) { + struct order_node *on = list_entry(p, struct order_node, node); if (num == on->val) return on->idx; } return -1; } ``` #### 2. 解釋程式碼的運作原理,提出改進方案並予以實作 #### 運作流程: 1. 建立一個與中序長度相同的雜湊表,並初始化指向第一個節點的指標 `first` 為 NULL。 2. 依照中序的順序,將每個節點加到對應的雜湊表,分為兩個步驟。 1. 將節點的值(val)與其在中序的位置(idx)紀錄在 `order_node` 結構中。 2. 取得該節點的 `hash` 值後,更改節點與對應雜湊表的指標,將節點加在雜湊表的開頭。 3. 使用 `dfs` 建構二元數。 1. 依照前序的順序,使用 `find` 找出該節點在中序的 `idx`。 2. 使用遞迴依次建構左、右子樹,直到節點均被走訪完。 #### 3. 在 Linux 核心的 cgroups 相關原始程式碼,找出 pre-order walk 程式碼並探討 --- ### 測驗 `2` #### 1. 補完程式碼,使其得以符合預期運作 **`hlist_del`** 刪除傳入的節點 `n`。 ```diff void hlist_del(struct hlist_node *n) { struct hlist_node *next = n->next, **pprev = n->pprev; *pprev = next; if (next) - EEEE = pprev; + next->pprev = pprev; } ``` **`lRUCacheFree`** 此函式會走訪所有節點並將其刪除,由於 `pos` 是 `list_head` 結構, `list_entry` 的 `member` 應傳入 `link`。 ```diff struct list_head *pos, *n; list_for_each_safe (pos, n, &obj->dhead) { - LRUNode *cache = list_entry(pos, LRUNode, FFFF); - list_del(GGGG); + LRUNode *cache = list_entry(pos, LRUNode, link); + list_del(&cache->link); free(cache); } ``` **`lRUCacheFree`** #### 2. 解釋程式碼的運作原理,提出改進方案並予以實作 #### 運作流程: #### 3. 在 Linux 核心找出 LRU 相關程式碼並探討 --- ### 測驗 `3`