# 2024q1 Homework2 (quiz1+2) contributed by < `LindaTing0106` > ## 第一週測驗題 ### 測驗 1 此函式目的在於找到串列的尾端,故在 `*left` 和 `(*left)->next` 不等於 null 的情況下, `left` 應該要等於 `&(*left)->next` ,也就是 `*left` 下一個節點的地址。 ```c node_t *list_tail(node_t **left) { while ((*left) && (*left)->next) left = &(AAAA); return *left; } ``` 此函式是在計算整個串列的長度,所以 `while` 中只需要判斷 `*left` 當下的節點是否為 null ,如有指向節點,則可以持續往下一個節點,並計算長度。 則 `left` = `&(*left)->next`。 ```c int list_length(node_t **left) { int n = 0; while (*left) { ++n; left = &(BBBB); } return n; } ``` 此處 `CCCC` 填入 `p->next` ,因為此函式需要去遍歷串列的每個節點,並根據他們大於或是小於 pivot 的值,決定應該插入 `right` 或是 `left` 。 ```c while (p) { node_t *n = p; p = CCCC; list_add(n->value > value ? &right : &left, n); } ``` 原本想法是因為 begin 和 end 儲存一段串列的頭與尾,像是前面的 ```c begin[0] = *list; end[0] = list_tail(list); ``` 所以 `DDDD` = `list_tail(left)`, `EEEE` = `list_tail(right)`。 後來發現是傳進去的型態問題, `node_t *list_tail(node_t **left)` 傳入的型態應該為 pointer of pointer of node_t ,故將答案改為 `DDDD` = `list_tail(&left)`, `EEEE` = `list_tail(&right)`。 ```c begin[i] = left; end[i] = DDDD; begin[i + 1] = pivot; end[i + 1] = pivot; begin[i + 2] = right; end[i + 2] = EEEE; ``` #### 延伸問題: ##### 解釋上述程式碼的運作原理。 上述的快速排序法是使用非遞迴的方式實作。 還沒進入迴圈之前, `begin[0]` 和 `end[0]` 分別會指向串列的頭與尾。 ```graphviz digraph First { #rankdir = LR; node[shape=box]; {rank="same"; 4; 1 ;3 ;5 ; 2 ; 7;} 4 -> 1 -> 3 -> 5 -> 2 -> 7; node[shape=none]; {rank="same"; 4; 1 ;3 ;5 ; 2 ; 7;NULL} 7->NULL subgraph subgraph_name{//子图 rankdir = BT; node[shape=none]; "begin[0]" -> 4; "end[0]" -> 7; } } ``` 先選定 4 作為 pivot ,並且將 pivot 從串列移除,之後開始比較 pivot 與串列各個節點之間的大小,將小於 pivot 的節點 `list_add` 進 `left` ,大於 pivot 的節點 `list_add` 進 `right`。 ```graphviz digraph pivot_is_4 { node[shape=box]; 4; node[shape=none]; pivot; pivot->4 } ``` ```graphviz digraph move_p { #rankdir = LR; node[shape=box]; {rank="same"; 1 ;3 ;5 ; 2 ; 7;} 1 -> 3 -> 5 -> 2 -> 7; node[shape=none]; {rank="same"; 1 ;3 ;5 ; 2 ; 7;NULL} 7->NULL subgraph subgraph_name{//子图 rankdir = BT; node[shape=none]; p -> 3; n -> 1; } } ``` 排列好大小後,利用 `begin[i]` 和 `end[i]` 指標指向串列的頭跟尾。 `begin[i]` 和 `end[i]` 指向的是比 pivot 小的串列, `begin[i+2]` 和 `end[i+2]` 指向的是比 pivot 大的串列, `begin[i+1]` 和 `end[i+1]` 則指向 pivot 。 ```graphviz digraph begin_end { rankdir = LR subgraph subgraph_name{ node[shape=box]; 7->5; } subgraph subgraph_name{ node[shape=box]; 2->3->1; } subgraph subgraph_name{ node[shape=box]; 4; } node[shape=none]; right->7; left -> 2; "begin[0]"->2 "end[0]"->1 "begin[1]"->4 "end[1]"->4 "begin[2]"->7 "end[2]"->5 } ``` 由於 `i += 2;` 所以先從上一回合比 pivot 大的串列開始執行上述動作。 將 7 當作 pivot 所以在比大小的時候, 將節點 5`list_add` 進 `left`,而`right` 只能指向 NULL 。 ```graphviz digraph pivot2 { rankdir = TB subgraph subgraph_name{//子图 node[shape=box]; 7; } node[shape=none]; "begin[3]"->7 "end[3]"->7 pivot->7 } ``` ```graphviz digraph round2 { #rankdir = LR subgraph subgraph_name{//子图 node[shape=box]; 5; } subgraph subgraph_name{//子图 node[shape=none]; NULL; } node[shape=none]; left->5; "begin[2]"->5 "end[2]"->5 right->NULL "begin[4]"->NULL "end[4]"->NULL } ``` 再下一回合 `i = 4` 時,因為 `begin[4]` 和 `end[4]` 都指向 NULL ,所以進到else中,並且 `i--` ,來到 `i = 3` ,由於 `begin[3]` 和 `end[3]` 都指向節點 7,故將 7 `list_add` 進 `result` ,且 `i--` , `i = 2` 時也是相同操作,將 5 `list_add` 進 `result` 。重複以上步驟,最終 `result` 則會為一個遞增串列,以上及為程式碼的運作原理。 ##### 提出改進方案並予以實作。 從上述程式碼可以看出他永遠都是選擇第一個節點作為 pivot ,但當最差情況發生時的時間複雜度會來到 O(n²) ,像是 `[1, 2, 3, 4, 5]` 則每次選到的 pivot 都為最左邊的數字,如此 `left` 都為 NULL 而 `right` 為除了 pivot 以外的節點。如此每次選擇 pivot 後,都要遍歷剩下的節點,那麼就可以使用 Median of Medians Algorithm 的方法去規避最差情況發生。 ##### 使用 Linux 核心風格的 List API 改寫上述程式碼 [程式碼](https://github.com/LindaTing0106/linux2024-homework2/blob/main/QuickSort/QuickSort.c) 在程式碼中引入 list.h 函式,並將 node_t 改為串列連接。 ```diff +#include "list.h" typedef struct __node { - struct __node *left, *right; - struct __node *next; + struct list_head list; long value; } node_t; ``` 將程式碼內部用到 node_t 的函式,都改寫為 list_head 的型態。 由於改為 list_head 的型態,故可以將 list_tail 的函式刪除,直接使用 list->prev 的方式表示最尾端的端點,故也將 end 移除。但因為要保存每次分治的串列,故這裡的 begin 需要配置 list_head 的空間。 ```diff - begin[0] = *list; - end[0] = list_tail(list); + for (int be = 0; be < max_level; be++){ + begin[be] = list_new(); + } + begin[0] = list; while (i >= 0) { - node_t *L = begin[i], *R = end[i]; + struct list_head *L = begin[i]->next, *R = begin[i]->prev; ``` 此外將原本很多單純用 = 賦予值的部分,改為運用 `list_splice_init` 、 `list_move` 。 ##### 並針對鏈結串列,提出可避免最差狀況的快速排序實作,應設計效能評比的測試程式。 ### 測驗 2 `AAAA` 填入 `&head` ,則可以透過指標的指標進行編寫。 ```c struct list_head *head; struct list_head **tail = AAAA; ``` 當此處的priv不為 NULL ,則會將型態轉為整數的指標,使用*取值後再加 1 。初步認為這裡應是對應到最後算出比較次數的計算。 ```c int compare(void *priv, const struct list_head *a, const struct list_head *b) { if (a == b) return 0; int res = list_entry(a, element_t, list)->val - list_entry(b, element_t, list)->val; if (priv) *((int *) priv) += 1; return res; } ``` 在比較完兩個節點的大小後,使用 `*tail` 將較小的節點連接到 head ,而後應該要將 `tail` 指向此節點的 next ,讓下次比較大小時,較小的節點可以繼續接在 `*tail` 處,也就是讓尾端的 next 可以放入下一個節點的地址。 故 `BBBB` 應該填入 `&(*tail->next)` , 但因為要填入最精簡的程式碼,故寫成 `&a->next` 。 而 `CCCC` 只是換成在如果節點 b 比 a 的值小的情況,所以可以直接填入 `&b->next` 。 ```c if (cmp(priv, a, b) <= 0) { *tail = a; tail = BBBB; ``` 從此段程式碼來看, `list` 最終會指向 NULL ,而需要補上 `DDDD` 和 `EEEE` 才能使整個串列變成雙向環狀鏈結串列,所以需要將 `list` 的前一個節點,也就是 `tail` 的下一個節點變為 `head` ,並且 `head` 的上一個節點也應該要為 `tail` 。故 `DDDD` 填入 `tail->next` ,而 `EEEE` 為 `head->prev` 。 ```c static void build_prev_link(struct list_head *head, struct list_head *tail, struct list_head *list) { tail->next = list; do { list->prev = tail; tail = list; list = list->next; } while (list); /* The final links to make a circular doubly-linked list */ DDDD = head; EEEE = tail; } ``` 在這段程式碼中我們可以觀察到 `stk_size` 此變數,一開始是設為 0 ,而在後面找 run 的迴圈裡,此變數也會更著增加,從這裡我們可以判斷 `stk_size` 是表示著切了幾個 run 的個數,並且在各個合併的函示中, `stk_size` 的數量也會減少。 而在 `build_prev_link(head, head, stk0);` 此函式中,我們可以得知是在將最後合併出來,只剩下一個 run 的串列修正為雙向環狀鏈結串列,故 `FFFF` 填入 1。 ```c void timsort(void *priv, struct list_head *head, list_cmp_func_t cmp) ``` #### 延伸問題: ##### 解釋上述程式碼的運作原理。 在上述程式碼內,並無設定 minrun 的部分,假設被傳入 `find_run` 的 list 。 ```graphviz digraph find_run { rankdir = LR; node[shape=box]; 5->4->3->6->1; } ``` 因為 5 > 4 的關係想辦法讓這段 run 反轉。 ```graphviz digraph de { rankdir = LR; node[shape=box]; 5; 4->3->6->1; node[shape=none]; 5-> NULL; list->4 next->3 head->4 prev->5 } ``` 最後回傳回主程式遞增的 run ( 稱 run1 ) ,以及另一個還沒被排序過的串列。 ```graphviz digraph de2_break { rankdir = LR; node[shape=box]; 5;4;3; 6->1; node[shape=none]; 3->4->5-> NULL; list->3 next->6 head->3 prev->4 "result.head"->3 "result.next"->6 } ``` `tp` 會將排序過的 run 的 `head` 串接在一起,經過剛剛的 `find_run` ,現在 run 的數量等於 1 ,接著進入 `merge_collapse` ,但因為 run==1 ,所以直接跳出。 再跑一次迴圈後,剛剛剩下來沒有被排序的串列,經過 `find_run` 的處理後, `result.head` 的指針會指向處理好的 run ,在這裡程式是用 `result.head->prev` 指向 run1 。 ```graphviz digraph run2 { rankdir = LR; node[shape=box]; 1;6; node[shape=none]; 1->6->NULL "result.head"->1 "result.next"->NULL } ``` 之後再次進到 merge_collapse 函式,根據是否滿足上面提到的原則,來決定要不要進行合併。 ##### 提出改進方案並予以實作。 明顯可以發現上述程式碼中並沒有包含 minrun 的實作,故應將 minrun 加入程式的實作。 minrun 的大小為,將總資料長度轉為二進位後的前 6 位數字,若 6 位數字後不為 0 ,則加 1。 ## 第二週測驗題 ### 測驗 1 ```graphviz digraph hashTable { rankdir= "LR"; node [shape= "record"]; node2 [label= "<0>|<1>|<2>|<3>|<4>"]; node3 [label= "<0>val = 3|idx = 1"]; node4 [label= "<0>val = 9|idx = 0"]; node2:<0>->node4; node2:<1>->node3; node [shape="none"]; node2:<2>->NULL; } ``` 這段重建二元樹的程式碼,是先從 `TreeNode *buildTree` 開始,首先程式會先創一個空的 hash table ,並且利用 `void node_add` 將 `inorder` 的節點放入 hash table 中,其中節點資訊包含了數值和位置。 ```graphviz digraph hashTable { rankdir= "LR"; node [shape= "record"]; node2 [label= "<0>|<1>|<2>|<3>|<4>"]; node3 [label= "<0>val = 3|<1>idx = 1"]; node4 [label= "<0>val = 9|<1>idx = 0"]; node2:<0>->node4; node2:<1>->node3; node3[fontcolor=red]; node [shape="none"]; node2:<2>->NULL; } ``` 之後在 `TreeNode *dfs` 中,會利用指標 `*tn` 去存取目前在前序中最前面的節點的數值,當作此子樹的頭,以及利用這個數值在 `int find` 中找尋其在中序裡的位置。 ```graphviz digraph tnleft { node [shape= "record"]; node1 [label="<0>3|<1>9|<2>20|<3>15|<4>7"]; node [shape="none"]; pre_low->node1:1; pre_high->node1:1; } ``` ```graphviz digraph tnleft { node [shape= "record"]; node1 [label="<0>9|<1>3|<2>15|<3>20|<4>7"]; node [shape="none"]; in_low->node1:0; in_high->node1:0; } ``` 其中 `tn->left = dfs(preorder, pre_low + 1, pre_low + (idx - in_low), inorder, in_low, idx - 1, in_heads, size);` ,會持續遞迴出左子樹的函式。 `pre_low + 1` 做為下輪的 `int pre_low` 是因為第一個已經被當作頭了,所以 `pre_low` 就往前一格。 `pre_low + (idx - in_low)` 做為下輪的 `int pre_high` ,則是利用 `idx - in_low` 可以算出左子樹有的節點數量,所以用 ``` pre_low + 左子樹有的節點數量 ```,得出後面 `pre_low + (idx - in_low)` 格的節點都是左子樹的節點。 `idx - 1` 做為下輪的 `int in_high` 是因為在中序中,頭左邊的所有節點都是左子樹的節點。 ```graphviz digraph tnleft { node [shape= "record"]; node1 [label="<0>3|<1>9|<2>20|<3>15|<4>7"]; node [shape="none"]; pre_low->node1:2; pre_high->node1:4; } ``` ```graphviz digraph tnleft { node [shape= "record"]; node1 [label="<0>9|<1>3|<2>15|<3>20|<4>7"]; node [shape="none"]; in_low->node1:2; in_high->node1:4; } ``` 而 `tn->right = dfs(preorder, pre_high - (in_high - idx - 1), pre_high, inorder, idx + 1, in_high, in_heads, size);` , `pre_high - (in_high - idx - 1)` 做為下輪的 `int pre_low` 是因為 `(in_high - idx - 1)` 可以得出在前序中右子樹的尾到右子樹的頭中間的節點數量,故用 `pre_high` 減去,可得右子樹的頭的節點的位置。 `idx + 1` 做為下輪的 `int in_low` ,則是因為在中序中頭的右側即全為右子樹節點。 ### 測驗 2 `LRU Cache` 是將一種快取的實做方式,當有新資料進來時,將快取中最後一次被存取的部分給替代掉。 需要寫一般串列和 hash table 的串列,來去串接節點是因為可以利用 hash table 提升我們在取值時的速度,如果只有一般串列,那我們必須遍歷 `count` 個節點找尋我們要的節點。而需要一般串列是因為我們這樣才有哪個節點是最後被使用的紀錄。 ```c typedef struct { int capacity; // 其LRUCache的大小 int count; // 目前使用的數量 struct list_head dhead; // 串接所有的節點 struct hlist_head hhead[]; // 用 hash table 的方式儲存節點 } LRUCache; ``` `LRUCache *lRUCacheCreate(int capacity)` 會根據其容量大小來為其配置記憶體大小。 `void lRUCacheFree(LRUCache *obj)` 利用傳入的指標,將其指向的快取給刪除,程式裡面用到 `list_entry` 函式,來取得快取中的串列的物件,再將其各個刪除。應該也可以使用 `hlist_del` 將 hash table 中串列的節點移除。 `int lRUCacheGet(LRUCache *obj, int key)` 程式碼中會先遍歷對應的 hash table 中的串列,如果有找到對應的值,則將 dlist 中該節點,搬去串列的最前方,並回傳該值,如果找不到,則回傳 `-1` 。 `void lRUCachePut(LRUCache *obj, int key, int value)` 是希望將給定的值放入快取中,首先會利用 key 求出 hash 值,再利用 hash 值來檢查是否此 key 值已經使用過了。 - 如果**使用過了**,則將此節點移動至串列最前方並且將 value 改為新的值。 - 如果**沒有使用過**,則比對傳進來的快取是否已經滿了。 - 如果**空間滿了**,則將最後一個使用的節點移至串列的最前方,並將其在 hash 中對應的位置刪除,並且再將其放入 hash table 中。 - 如果**空間沒有滿**,則新配置一個空間,並將節點加入串列,也加入 hash table 中,並將 count++ 。 這段程式碼相當為將 &cache->node move 至 &obj->hhead[hash] ,既然都有 list_move 了,也可以實作出 hlist_move 。 ```c hlist_del(&cache->node); hlist_add_head(&cache->node, &obj->hhead[hash]); ``` ### 測驗 3 `find_nth_bit` 在指定的記憶體空間找出第 N 個設定的位元。 `__ffs(unsigned long word)` 是用來找一段 long 中,最高的位元是哪一位,程式碼當中用了多個判斷式。 其中原理為,當 word & 0xf 為 0 則表示在 4 位元內 word 的值都為 0 ,故 word 必大於 $2^4$ ,以此類推。 ```c if ((word & 0xf) == 0) { num += 4; word >>= 4; } ``` `find_nth_bit` 的程式碼運作原理為,先比較要找的數字有無超過限制的 `size` ,並檢查 `size` 的設置是否符合規範, `GENMASK(h, l)` 是將 `h` 到 `l` 間的位元變為 1 , 故 `val` 為 `addr` `h` 到 `l` 間的值,如果 `val` 有值,則利用 `fns` 找到為 1 的最高位元並回傳。