# 2024q1 Homework2 (quiz1+2) contributed by < [devarajabc](https://github.com/devarajabc) > ## 第一週測驗 `1` 參考 [Optimized QuickSort — C Implementation (Non-Recursive)](https://alienryderflex.com/quicksort/),實作非遞迴 (non-recursive; iterative) 的快速排序法 ### 1.解釋上述程式碼的運作原理。 參考 [csotaku0926](csotaku0926), [chloe0919](https://hackmd.io/@Chloexyw/linux2024-homework2) 利用指標陣列 `begin` 和 `end` 來模擬堆疊 從堆疊中取出索引值對應的指標 `begin[i]` 和 `end[i]`並將值存在` L `和 `R` 之中 並將 `pivot` 設定為 `L` > node_t *L = begin[i], *R = end[i]; 示意圖如下 ```graphviz digraph graphname{ node[shape=box] rankdir = LR A[label=4] B[label=1] C[label=3] D[label=5] E[label=2] F[label=7] G[label=8] NULL[label=NULL,shape=plaintext] A->B->C->D->E->F->G->NULL { rank="same"; L[label="L",shape=plaintext,fontcolor=red] pivot[label="pivot",shape=plaintext,fontcolor=blue] pivot->L[color=blue] L->A[color=red]; } { rank="same"; R[label="R",shape=plaintext,fontcolor=red] R->G[color=red]; } } ``` 當 `L` 不等於 `R` 時, 將大於 pivot 的節點加入 `right` ,反之則加入 `left` ```graphviz digraph{ ## rankdir=TD rankdir=LR node [shape=box,color=black] //tp [shape=none,label=pivot,fontcolor=red] A [shape=none,label=left] B [shape=none,label=right] C [shape=none,label=pivot,fontcolor=blue] C->4 B->8->7->5 A->2->3->1 } ``` 接著分別用指標 `begin[i]` 和 `end[i]` 來記錄 `left` 的開頭與結尾、`begin[i+2]` 和 `end[i+2]` 來記錄 `right` 的開頭與結尾,而 `pivot` 則同時用 `begin[i+1]` 和 `end[i+1]` 來記錄 最後將堆疊的索引 `i` 更新為 `i+=2` (取出一層又放回三層) 是意圖如下: :::danger 改進你的漢語表達。 ::: ```graphviz digraph graphname{ node[shape=box] rankdir = LR p [shape=none,label=pivot,fontcolor=blue] p->4 p1[shape=none,label="begin[i+1]",fontcolor=red] p1->4 p2[shape=none,label="end[i+1]",fontcolor=red] p2->4 A[label=8] B[label=7] C[label=5] D[label=2] E[label=3] F[label=1] R[label="R",shape=none,label=right] //NULL[label=NULL,shape=plaintext] right[shape=none,label="right",fontcolor=blue] right->A left[shape=none,label="left",fontcolor=blue] left ->D D->E->F A->B->C { rank="same"; L[label="begin[i+2]",shape=plaintext,fontcolor=red] //pivot[label="pivot",shape=plaintext,fontcolor=blue] //pivot->L[color=blue] L->A[color=red]; } { rank="same"; R[label="end[i]",shape=plaintext,fontcolor=red] R->F[color=red]; } { rank="same"; L1[label="begin[i]",shape=plaintext,fontcolor=red] //pivot[label="pivot",shape=plaintext,fontcolor=blue] //pivot->L[color=blue] L1->D[color=red]; } { rank="same"; R1[label="end[i+2]",shape=plaintext,fontcolor=red] R1->C[color=red]; } } ``` ```graphviz digraph graphname{ node[shape=box] subgraph cluster_0 { //rank="same"; rankdir = LR; style=filled; color=lightgrey; node [style=filled,color=white]; "begin[i+2]" "begin[i+1]" "begin[i]" ; label = "begin"; } subgraph cluster_1 { //rank="same"; rankdir = LR; style=filled; color=lightgrey; node [style=filled,color=white]; "end[i+2]" "end[i+1]" "end[i]" ; label = "end"; } } ``` 可以想像成這樣: ```graphviz digraph graphname{ node[shape=box] rankdir = LR node[shape=record]; map [label="stack |begin[i+2] end[i+2] |begin[i+1] end[i+1] |begin[i] end[i] "]; } ``` 反之當 `L` 等於 `R` 時,代表走訪到 pivot ,也就是「位置正確」的節點,所以使用 `list_add` 來將該節點加入 result 並將 `i--` 。 ### 2.提出改進方案並予以實作 1. 選擇 povit 的方式 2. max_lengh 的大小 ### 3.使用 Linux 核心風格的 List API 改寫上述程式碼,並針對鏈結串列,提出可避免最差狀況的快速排序實作,應設計效能評比的測試程式。 參考 [list.h](https://github.com/sysprog21/lab0-c/blob/master/list.h) ## 第一週測驗 `2` Timsort 的實作,刻意不額外配置記憶體空間 ### 1.解釋上述程式碼的運作原理,提出改進方案並予以實作。 參考 [jimmylu890303](https://hackmd.io/@jimmylu0303/linux2024-homework2#%E6%B8%AC%E9%A9%97%E4%BA%8C) 程式執行可以分成以下幾個階段 1. 將原本的串列的最後一個節點解除環狀佇列,並將它指向 NULL 2. 利用 find_run 將從 list 分割出一個 run ,並將該 run 的開頭的 prev 設為上一個 run 的開頭,示意圖如下 : ```graphviz digraph{ ## rankdir=TD rankdir=LR node [shape=box,color=black] tp [shape=none,label=tp,fontcolor=red] A [shape=none,label=Z,fontcolor=blue] B [shape=none,label=Y,fontcolor=blue] C [shape=none,label=X,fontcolor=blue] C->8-> 9 B->5->6->7 A->1->2->3->4 8->5 [xlabel= " tp->prev",fontcolor=red] {rank=same; 5;8} tp->8 {rank=same; tp;8} 5->1[xlabel= " tp->prev->prev",fontcolor=red] {rank=same; 5;1} } ``` > 上圖修改自 jimmylu0303 3. 當 `stk_size` >= 2 時,利用 `merge_collapse` 來保堆疊上的 run 長度保持平衡 4. 利用`merge_force_collapse` 使 ` stk_size` 長度小於 3 5. 將 linked-list 恢復成 circular doubly-linked list 6. 若` stk_size` 為 2 則需要利用 `merge_final` 合併最後兩個 run 以下是對 timsort 會用到的函式進行細部的解釋 ##### `create_sample` `space` 是指向「一大串可用且連續的記體空間」的指標,藉由 `element_t *elem = space + i` 來配置一段記體空間給 element_t 並用區域變數 `elem` 指向它,隨後使用 `rand()` 來將 `elem->val` 設成一個隨機數,最後利用`list_add_tail(&elem->list, head)`將 element_t 加入鏈接 head 。 ##### `copy_list` 利用 `element_t *copy = space++` 來獲得「指向可使用的記憶體空間」的指標,並在隨後的 `list_for_each_entry` 中不斷將鏈接 `from` 中每個的 `entry` 的資訊如 val, seq 複製到該記憶體空間 , 最後再將該記憶體空間加入鏈接 `to` 之中。 ##### `compare` 比較鏈接 `a` 和鏈接 `b` 的 `val` 並輸出相減的結果,跟 `strcmp` 很像 而 `void *priv` 的功能是,窩不知道 ##### `check_lis` 用來檢查是否有以下三種錯誤情況 1. Wrong order 2. unstable 3. inconsistance #### `run_size` 一開始看不懂為何 `(size_t) (head->next->prev)` 就是 runsize ?,一直到參考了 [ HotMercury](https://hackmd.io/@NeedSleep/linux2024-homework2#-Timsort) 的解釋才知道 run 的被以指標的型態儲存在`head->next->prev` 之中,最後用 (size_t) 來轉換型態並輸出。 ##### `merge` 使用逐對合併 (one-pair-at-a-time) 合併兩個單向的鏈接( null-terminated singly-linked list )。 `tail` 是「指向尾端的指標」的指標,並在 for loop 中不斷更新,最後回傳指向「合併好的鏈結串列 」的指標。 (TODO : 補圖片) ##### `build_prev_link` 將單向鏈結串列恢復成雙向鏈結串列。 但我不懂最後兩行程式碼為何可以讓其回復成雙向鏈結串列 ```c /* And the final links to make a circular doubly-linked list */ tail->next = head; head->prev = tail; ``` ##### `merge_final` 內容與 `merge` 相似 不同之處在於: 1. 使用 Parameter `head` 來當記錄「合併好的鏈結串列」 2. 在函式最後使用 `build_prev_link` 來將單向鏈結串列恢復成雙向鏈結串列 3. 輸出為 void ##### `find_run` > 參考 [ HotMercury](https://hackmd.io/@NeedSleep/linux2024-homework2#-Timsort) 輸出為 pair ,裡面包含了兩個指標 `next` 和 `head` `head` 為指向當前 run 的指標,而 `next` 為指向下一個 run 的指標。 在走訪鏈接的過程中會遇到以下兩種情況: 1. 遇到連續的遞減的節點 -> 計算長度 `len` 並將其反轉,直到遇到比當前節點還大的節點 2. 遇到連續的遞增的節點 -> 計算長度 `len` ,直到遇到比當前節點小的節點-> 將最後一個節點的 `next` 設為 NULL 。 最後透過 ` head->next->prev = (struct list_head *) len`巧妙地將長度 `len` 藏在 `prev` :::danger 程式碼註解以美式英語書寫,不該存在漢語。 用清晰的漢語解釋程式行為後,再搭配程式碼驗證,避免「舉燭」。 ::: ##### `merge_at` 合併 `at` 和 `at->prev` 再用指標 `list` 指向合併的結果且利用 `list->prev = prev` 來維護鏈接的順序同時減少 `stk_size` 的值 (推測是在處理 stack 內的合併) 並利用 `list->next->prev = (struct list_head *) len` 更新合併後的 run 長度,最後再回傳 合併後的 run 。 ##### `merge_collapse` 參考 [維基百科](https://en.wikipedia.org/wiki/Timsort)、 [jimmylu890303](https://hackmd.io/@jimmylu0303/linux2024-homework2#%E6%B8%AC%E9%A9%97%E4%BA%8C) ![Representation_of_stack_for_merge_memory_in_Timsort](https://hackmd.io/_uploads/rJ14oqYT6.svg) >The runs are inserted in a stack. If |Z| ≤ |Y| + |X|, then X and Y are merged and replaced on the stack. In this way, merging is continued until all runs satisfy : > i. |Z| > |Y| + |X| > ii. |Y| > |X|. 在本例子中,X 是 tp , Y 是 tp->prev , Z 是 tp->prev->prev 而要合併的形況分成以下 3 種 1. |Z| <= |Y|+|X| 且 |Z| < |X| ``` YZ 合併 tp->prev = merge_at(priv, cmp, tp->prev); ``` 3. |Z| <= |Y|+|X| 且 |Z| >= |X| ``` XY 合併 tp = merge_at(priv, cmp, tp); ``` 6. |Y| <= |X| ``` XY合併 tp = merge_at(priv, cmp, tp); ``` ##### `merge_force_collapse` 與 `merge_collapse` 不同之處在於 `merge_force_collapse` 會不斷進行以下兩種合併直到`run_size` 小於 3 1. |Z|<|X| ``` YZ合併 tp->prev = merge_at(priv, cmp, tp->prev); ``` 2. 其他 ``` XY合併 tp = merge_at(priv, cmp, tp); ``` 而 `merge_collapse` 只要遇到以下兩種情況就會離開 while loop ``` 1. |Z| > |Y| + |X| 2. |Y| > |X| ``` ### 2.提出改進方案並予以實作 #### 1. `merge` 裡面重複的部分太多,且沒有使用 Galloping mode > 考慮到 A 和 B 都是已排序好的數列,我們可持續改進:首先尋找 B 的首個元素(即)在 A 中的排序位置,從而確定 A 中有一段是小於者,可將這部分元素放回 A。接著,尋找剩餘 A 的首個元素在 B 中的位置,如此反覆進行,直到完成排序。 參考 [yanjiew](https://hackmd.io/@yanjiew/linux2023q1-timsort#Timsort-%E9%81%8B%E4%BD%9C%E6%96%B9%E5%BC%8F) 然而有關「重複部分太多」 的改進方案再參考 [第一次嘗試貢獻 Linux 核心](https://hackmd.io/@yanjiew/linux2023q1-1st_contrib) 所提供的結果後發現小丑竟是我自己。 #### 2. `find_run` 沒有遵守 2 的冪的原則也沒有確保每個run 的長度不小於 minrun 1. 選擇 minrun 的策略是 Timsort 的關鍵,我打算使用 [2024q1 第 1 週測驗題](https://hackmd.io/@sysprog/linux2024-quiz1#%E6%B8%AC%E9%A9%97-2) 裡面提到的方法: >取資料長度的前 6 位,若剩餘位中任何一位被設置,則加 1。 >這主要是為了在處理隨機數列時,使得能夠被 minrun 切分成 2 的冪的 run,以便在合併時達到最佳平衡。 原本看不懂敘述,直到翻閱了 [Wikipedia: Timsort](https://en.wikipedia.org/wiki/Timsort#:~:text=Timsort%20is%20a%20hybrid%2C%20stable,in%20the%20Python%20programming%20language.) 中的描述才知道是取陣列長度的「二進位表示法」的前六位為 minrun ,若剩餘位中任何一位被設置(剩餘位中有任何一位是1),則 minrun 要再加 1。 minrun 具體具體流程如下: 1. 找到前六位 在 while loop 中不斷將 `n` 除以 2 直到其值小於等於 63 (111111,六位皆為1) 2. 檢查剩餘位數是否有被設置(有 1 ) 使用 `n & 1` 來取得最後一位 (LSB) 的值 ,並用 `|` 來更新 `r` 的值 ```c size_t calculate_minrun_naive(size_t n) { size_t r = 0; while (n >= 64) { r |= n & 1; n >>= 1; } return n + r; } ``` :::warning 考慮 [compute_minrun](https://github.com/swenson/sort/blob/24f5b8b13810ad130109c7b56daf8e99ab0fe1b8/sort.h#L123) 的實作,以 CLZ 加速與算,善用 GCC 的 `__builtin_clz` > 了解 ::: 參考 [compute_minrun](https://github.com/swenson/sort/blob/24f5b8b13810ad130109c7b56daf8e99ab0fe1b8/sort.h#L123) 步驟如下: 1. 利用 CLZ 來計算 `top_bit` , size 轉換成二進位後的位數 2. 計算 `shift` ,要向右移的大小,若 size 不足六位則為零 3. 做出 `mask` ,來判斷剩餘位數是否有被設置(`1`) 4. `minrun` = size 的前六位,若有被設置則再加 1 2.改寫 `find_run` >Timsort 為了使合併過程更加均衡,需要儘量控制每個 run 的長度,定義一個 minrun (最小運行長度),用以表示每個 run 的最小長度,若長度太短,就用二分插入排序,將 run 後面的元素插入到前面的 run 裡面。 若對鏈結串列使用 `binary_insert` 的話時間複雜度會很糟糕(TODO:原因補充) >今天一整天都卡在無法用鏈結串列實作出 `binary_insert` 而沒有思考為何要使用 `binary_insert` 以及是否非使用 `binary_insert` 不可,就埋頭亂寫一通,浪費一堆時間,下不為例。 naive:每個 run 的長度固定為 minrun ### 2.將改進過的 Timsort 實作整合進 sysprog21/lab0-c,作為另一個有效的排序命令,應設計效能評比的測試程式,確保測試案例符合 Timsort 設想的資料分布樣式。 --- ## 第二週測驗 `1` 運用 [Linux 核心的 hash table 實作](https://hackmd.io/@sysprog/linux-hashtable) 來完成 LeetCode [105 Construct Binary Tree from Preorder and Inorder Traversal](https://leetcode.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal/description/) >給定由二個由整數構成陣列的前序 (preorder) 和中序 (inorder) 形式,分別是對同一棵二元樹的前序及中序走訪結果,藉此重建此二元樹。 ### 解釋程式碼的運作,撰寫完整的測試程式,指出其中可改進之處並實作 參考 [jimmylu890303](https://hackmd.io/XE_4cW4XQiiWDnE4ILsQqg?view#%E7%AC%AC%E4%BA%8C%E5%91%A8%E6%B8%AC%E9%A9%97%E9%A1%8C) >Hash table 中的元素為 hlist_head ,而 Hash table 中各個 bucket 的節點為 hlist_node 在結構 `hlist_node` 之中, `pprev` 為「指向自己的指標」的指標 ```graphviz digraph G { rankdir = LR; splines = false; node[shape = "record"] list_head[label = "<m>list_head | <n>first"] node_1[label = "<m>hlist_node_1 | {<p>pprev | <n>next}", group = list]; node_2[label = "<m>hlist_node_2 | {<p>pprev | <n>next}", group = list]; node_3[label = "<m>hlist_node_3 | {<p>pprev | <n>next}", group = list]; NULL[shape = plaintext, label = "NULL", group = list] {rank = "min" list_head} list_head -> node_1 -> node_2 -> node_3[ weight = 10, style = invis ] list_head:n -> node_1:m; node_1:p -> list_head:n; node_1:n -> node_2:m; node_2:p -> node_1:n; node_2:n -> node_3:m; node_3:p -> node_2:n; node_3:n -> NULL; } ``` 程式的執行可以分成以下幾個步驟 1. INIT_HLIST_HEAD 2. node_add 3. dfs #### 可改進之處 1. 在原本的程式中,只使用了 `hash = (num < 0 ? -num : num) % size` 當作雜湊函數 故想改使用在 [hash.h](https://github.com/torvalds/linux/blob/master/tools/include/linux/hash.h) 中提到的 $h(K) = K \times 2^w \times A >> (w - p)$ ### 3.在 Linux 核心的 cgroups 相關原始程式碼,找出 pre-order walk 程式碼並探討 --- ## 第二週測驗 `2` LeetCode 146. LRU Cache ### 2.解釋上述程式碼的運作,撰寫完整的測試程式,指出其中可改進之處並實作 本題新增了兩個新的結構 ```c typedef struct { int capacity; int count; struct list_head dhead; struct hlist_head hhead[]; } LRUCache; typedef struct { int key; int value; struct hlist_node node; struct list_head link; } LRUNode; ``` ```graphviz digraph G { rankdir = LR; splines = false; node[shape = "record"] LRUCache[label="<m>LRUCache | <n>capacity | <o>count | <p>dhead | <q>hhead[]"] LRUNode[label="<m>LRUNode | <n>key | <o>value | <p>node | <q>link"] // hlist_head[label = "<m>hlist_head | first", group = list]; // hlist_node[label = "<m>hlist_node | {<p>pprev | <n>next}", group = list]; // hlist_head-> hlist_node [style=invis] LRUCache->LRUNode [style=invis] } ``` > 參考 [jimmylu890303](https://hackmd.io/@jimmylu0303/linux2024-homework2#%E7%AC%AC%E4%BA%8C%E5%91%A8%E6%B8%AC%E9%A9%97%E9%A1%8C) ##### `lRUCacheCreate` ##### `lRUCacheFree` ##### `lRUCacheGet` ##### `lRUCachePut` ### 3.在 Linux 核心找出 LRU 相關程式碼並探討 ## 測驗 第二週測驗 `3` 考慮 `find_nth_bit` 可在指定的記憶體空間找出第 N 個設定的位元 (即 `1` ) ### 2.解釋上述程式碼的運作,撰寫完整的測試程式,指出其中可改進之處並實作 ### 3.在 Linux 核心的 cgroups 相關原始程式碼,找出 pre-order walk 程式碼並探討