# 2024q1 Homework2 (quiz1+2) contributed by < `kevinzxc1217` > ## 第一週測驗題 ### 測驗一 ```c node_t *list_tail(node_t **left) { while ((*left) && (*left)->next) left = &(AAAA); return *left; } ``` 這裡一開始我的想法會是填 `left -> next` ,不斷的往下走訪,不過要考慮到 `left` 是指標的指標,所以這裡的 `AAAA` 要填 `(*left) -> next` 。 ```c int list_length(node_t **left) { int n = 0; while (*left) { ++n; left = &(BBBB); } return n; } ``` 這裡要計算串列的長度,一樣是希望透過指標的指標去走訪整個串列,所以這裡的 `BBBB` 要填 `(*left) -> next` 。 ```c while (p) { node_t *n = p; p = CCCC; list_add(n->value > value ? &right : &left, n); } ``` ![1](https://hackmd.io/_uploads/Syg-2Jwap.png) 希望 `p` 不斷的往下移動,則 `CCCC` 要填 `p -> next` 。 ![2](https://hackmd.io/_uploads/S1fgpkDTp.png) ![4](https://hackmd.io/_uploads/B1aPTyPpa.png) ![5](https://hackmd.io/_uploads/rJLkyxwT6.png) 這裡主要是在分類指標 `n` 所指向的值 `n->value` 是否大於 `pivot` ,若大於的話分類在 `right` ,否則分類在 `left` 串列。 ```c begin[i] = left; end[i] = DDDD; begin[i + 1] = pivot; end[i + 1] = pivot; begin[i + 2] = right; end[i + 2] = EEEE; ``` ![a1](https://hackmd.io/_uploads/rywJWew66.png) ![a2](https://hackmd.io/_uploads/B1mo-lvTT.png) 這裡的 `end` 指的是 `left` 串列上的尾端,可以呼叫函式 `list_tail(node_t **left)` ,其中要注意該函式接收的參數需要是指標的指標,所以這裡的`DDDD` 要填 `list_tail(&left)` ; 而 `EEEE` 要填 `list_tail(&right)` 。 ```c if (L != R) { ... i += 2; } else { if (L) list_add(&result, L); i--; } ``` 該函式主要是用到了 `begin` 和 `end` 的 `index` 來取代quick sort的遞迴呼叫,不同的 `index` 就對應著不同的遞迴階段,其中 `i+=2` 就對應遞迴的再次呼叫本身函式,而 `i--` 就對應遞迴的 `return` 。 #### 使用 Linux 核心風格的 List API 改寫上述程式碼 >[程式連結](https://github.com/kevinzxc1217/linux2024_hw2/blob/main/quick_sort/quick_sort.c) ```c typedef struct __node { struct list_head list; long value; } node_t; ``` 將 `node_t` 內各個 node 的連結改成 List API 內部的 `list_head` 型態。 ```c void *list_construct(struct list_head *head, int n){ if(!head) return false; node_t* node = malloc(sizeof(node_t)); list_add_tail(&node -> list, head); node -> value = n; } ``` `node_t` 結構類似於 lab-0 內的 `element_t` ,可以透過 `list_add_tail` 的方式接上所新增的節點。 ```c struct list_head *list_tail(struct list_head *head) { struct list_head *tail = head -> prev; return tail; } ``` 由於改成環狀鏈結串列,故可以直接從 `head` 的前一個找到最尾巴的節點。 ```c static bool list_is_ordered(struct list_head *head) { struct list_head *cur = head -> next; while(cur != head && cur-> next != head){ node_t *n_cur = list_entry(cur,node_t,list); node_t *n_next = list_entry(cur->next,node_t,list); if(n_cur->value < n_next->value) return false; cur = cur -> next; } return true; } ``` 檢查是否有照順序排序,鏈結串列上的節點,主要是透過 `list_entry()` 的函式找到其 `value` 值,再去做比對。 ```c void quick_sort(struct list_head *list) { int n = list_length(list); int pivot_value; int i = 0; int max_level = 2 * n; struct list_head *begin[max_level], *end[max_level]; struct list_head *result = list_new(), *left = list_new(), *right = list_new(); begin[0] = list; while (i >= 0) { struct list_head *L = begin[i]; if (L ->next !=list_tail(L)) { struct list_head *pivot = list_new(); struct list_head *cur = L ->next; int max = list_length(L) -1; int min = 0; int cnt = rand() % (max - min + 1) + min; struct list_head *p = cur; while(cnt){ p = p->next; cnt--; } if(cur == p) cur = cur->next; pivot_value = list_entry(p,node_t,list)->value; list_move_tail(p, pivot); while (!list_empty(L)) { cur = cur -> next; list_move_tail(cur -> prev, list_entry(cur -> prev,node_t,list)->value > pivot_value ? right : left); } list_splice_tail_init(left,begin[i]); begin[i+1] = list_new(); begin[i+2] = list_new(); list_splice_tail_init(pivot,begin[i+1]); list_splice_tail_init(right,begin[i+2]); i += 2; } else { if (!list_empty(L)) list_splice_tail_init(L, result); i--; } } list_splice_tail_init(result,list); } ``` 這裡改寫時有幾點要注意: - 在創建新鏈結串列時應使用 `list_new()` ,已確保有分配到一個 `head` 空間。 - 原先傳入的 `**list` 是指標的指標,這裡改寫成傳入 `*list` 指標,有部分程式會受影響。 - 原本程式碼內是使用 `begin` 和 `end` 兩個指標,由於改成環狀鏈結串列,改寫後僅使用 `begin` ,可用 `list_tail()` 代替需要找尋 `end` 的程式。 - 原先的 quick_sort 程式所使用到的鏈結串列並沒有 `head` ,所以在找第一個節點時要注意。 - 這裡將鏈結串列賦予時,應使用 `list_splice()` 函式,才能確保完全合併,不能僅使用等號做賦予。 ```c int max = list_length(L) -1; int min = 0; int cnt = rand() % (max - min + 1) + min; struct list_head *p = cur; while(cnt){ p = p->next; cnt--; } if(cur == p) cur = cur->next; ``` 其中這段程式是在目前的鏈結串列中隨機挑選一個節點當作 pivot ,與每次挑選第一個節點或是最後一個節點當作 pivot 來比較,可減少 worse case 發生。這裡要注意的是程式內 `cur` 所指向的節點和 `p` 所指向的節點要不同,不然後續將 `p` 所指向的節點移至 `pivot` 串列時, `cur` 便會跟著指向,我們希望 `cur` 仍然指向原先的 `list` 串列。 ### 測驗二 ```c 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 = AAAA; for (;;) { /* if equal, take 'a' -- important for sort stability */ if (cmp(priv, a, b) <= 0) { *tail = a; tail = BBBB; a = a->next; if (!a) { *tail = b; break; } } else { *tail = b; tail = CCCC; b = b->next; if (!b) { *tail = a; break; } } } return head; } ``` 因為 `**tail` 為指標的指標,一開始僅有 `*head` ,故 `AAAA` 所指的位址應為 `&head` 。 其中 `*tail = a` 是將 `head` 指向 `a` 所指向的串列,而 `tail` 是指向新加入串列所指向的地址,故 `BBBB` 為 `&a->next` 而 `CCCC` 類推為 `&b->next`。 ```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; } ``` 這段程式碼主要是在建構迴圈雙向鏈結串列 ,簡單來看,如果沒有需要新增 `list` 的話,只需要將傳入的 `head` 和 `tail` 兩者的 `prev` 和 `next` 互指即可形成迴圈雙向鏈結串列,所以其中 `DDDD` 為 `tail->next` ,而 `EEEE ` 為 `head->prev` 。中間部分主要是將 `list` 新增到 `tail` 後面,且形式為迴圈雙向鏈結串列。 ```c void timsort(void *priv, struct list_head *head, list_cmp_func_t cmp) { stk_size = 0; struct list_head *list = head->next, *tp = NULL; if (head == head->prev) return; /* Convert to a null-terminated singly-linked list. */ head->prev->next = NULL; do { /* Find next run */ struct pair result = find_run(priv, list, cmp); result.head->prev = tp; tp = result.head; list = result.next; stk_size++; tp = merge_collapse(priv, cmp, tp); } while (list); /* End of input; merge together all the runs. */ tp = merge_force_collapse(priv, cmp, tp); /* The final merge; rebuild prev links */ struct list_head *stk0 = tp, *stk1 = stk0->prev; while (stk1 && stk1->prev) stk0 = stk0->prev, stk1 = stk1->prev; if (stk_size <= FFFF) { build_prev_link(head, head, stk0); return; } merge_final(priv, cmp, head, stk1, stk0); } ``` 在這段程式碼中,其中 `find_run` 函數主要用是將鏈結串列依據規則分成不同的 `run` ,用 `tp` 去連接各個整理後的 `run` ,最後再 `merge` 起來。 ![b1](https://hackmd.io/_uploads/rkKK6StT6.png) 假設這次初始狀態的鏈結串列 ![b2](https://hackmd.io/_uploads/r1wJ0BFaa.png) 這是剛進去`rnu` 函式後,初始化的鏈結串列 ```c if (cmp(priv, list, next) > 0) { /* decending run, also reverse the list */ struct list_head *prev = NULL; do { len++; list->next = prev; prev = list; list = next; next = list->next; head = list; } while (next && cmp(priv, list, next) > 0); list->next = prev; } else { do { len++; list = next; next = list->next; } while (next && cmp(priv, list, next) <= 0); list->next = NULL; } ``` 這段程式主要是進行分割不同的 run ,並且依據其升序或降序去做反轉。 ![b5](https://hackmd.io/_uploads/r1Ic7LFTT.png) ![b6](https://hackmd.io/_uploads/ByjaQItaa.png) 這裡主要是將 `4->3->1` 鏈結串列反轉成 `1->3->4`,經過上述程式一輪後,鏈結串列會如上圖所示。 ![B3](https://hackmd.io/_uploads/By61MUFp6.png) ![B3](https://hackmd.io/_uploads/HyoFfLK6a.png) 跑完整個 `find_run` 函式後,會發現原本的 `4->3->1` 鏈結串列反轉成 `1->3->4` 了,並且將 `next` 指標指向下一次需要進去 `find_run` 整理的鏈結串列。重複以上動作最後可以得到多個經過升序排列好不同的 run 。 `merge_collapse()` 該函式主要檢查堆疊頂端的 3 個 run 是否滿足以下原則: 1. A 的長度要大於 B 和 C 的長度總和。 2. B 的長度要大於 C 的長度。 當不符合這些條件時,函式會決定是否進行合併。 `merge_force_collapse()` 不檢查 `merge_collapse()` 的原則,直接將不同的 run 進行合併,直到剩餘 2 個 run。 `merge_final()` 將最後 2 個 run 進行合併,形成完全排序好的鏈結串列。 ![5](https://hackmd.io/_uploads/SyzMtUK6p.png) ## 第二週測驗題 ### 測驗一 ```c struct hlist_node { struct hlist_node *next, **pprev; }; struct hlist_head { struct hlist_node *first; }; static inline void hlist_add_head(struct hlist_node *n, struct hlist_head *h) { if (h->first) h->first->pprev = &n->next; n->next = AAAA; n->pprev = &h->first; h->first = n; } ``` 這裡由於 `n->next`的資料型態是指標,而`n->pprev` 的資料型態是指標的指標 `&h->first` ,故可以推敲出 `AAAA` 應填 `h->first` 。 ```c #define container_of(ptr, type, member) \ ((type *) ((char *) (ptr) - (size_t) & (((type *) 0)->member))) #define hlist_for_each(pos, head) \ for (pos = (head)->first; pos; pos = pos->next) 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); if (num == on->val) return on->idx; } return -1; } ``` 這裡是在尋找該鏈結串列上,是否有值和該函式的 `num` 一樣,對應`hlist_for_each()` , `BBBB` 是傳入該函式的 `&heads[hash]` ;而 `*on` 是希望將目前指標的 `val` 是什麼,所以 `CCCC` 是 `container_of` 。 ```c static inline void node_add(int val, int idx, int size, struct hlist_head *heads) { struct order_node *on = malloc(sizeof(*on)); on->val = val; on->idx = idx; int hash = (val < 0 ? -val : val) % size; hlist_add_head(&on->node, DDDD); } ``` 根據 `hlist_add_head()` 函式, `DDDD` 應填入 `struct hlist_head *h` 型態,故為 `&heads[hash]` 。 ```c 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) { if (in_low > in_high || pre_low > pre_high) return NULL; struct TreeNode *tn = malloc(sizeof(*tn)); tn->val = preorder[pre_low]; int idx = find(preorder[pre_low], size, in_heads); tn->left = dfs(preorder, pre_low + 1, pre_low + (idx - in_low), 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); return tn; } ``` 這裡參考的演算法是先從前序找第一個位置當做 root ,該 root可以在中序內分割成兩個陣列,root 左邊為左子樹,右邊為右子樹;再將找到的左子樹陣列和右子樹在前序內做分割陣列,完成第一次。後續再將切分完的左子樹的第一個位置當作 root 重複以上步驟,直到都切成單一元素,再將右子樹重複以上步驟,直到切成單一元素,即可完成。 而 `dfs` 使用 `pre_low` 和 `pre_high` 以及 `in_low` 和 `in_high` 透過設置不同的上下界,來替代上述提到的分割陣列,使其完成建樹。 ```c 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); } ``` 假設一開始的前序中序為: 前序 3,9,20,15,7 中序 9,3,15,20,7 經過第一次的分割,前序中序變成: 前序 3[9][20,15,7] 中序 [9]3[15,20,7] ```graphviz digraph BST { node [fontname="Arial" ]; l1 [ label = "3" ]; l21 [ label = "9" ]; l22 [ label = "15,20,7" ]; l1 -> { l21 l22 }; } ``` 由於左子樹切割到只剩餘一個節點時,會換右子樹重複做分割的動作,都切到剩餘單一元素即可完成建樹: 前序 20,[15],[7] 中序 [15],20,[7] ```graphviz digraph BST { node [fontname="Arial" ]; l1 [ label = "3" ]; l21 [ label = "9" ]; l22 [ label = "20" ]; l31 [ label = "15" ]; l32 [ label = "7" ]; l1 -> { l21 l22 }; l22 -> { l31 l32 }; } ``` ### 測驗二 ```c typedef struct { int capacity; int count; struct list_head dhead; struct hlist_head hhead[]; } LRUCache; ``` **LRUCache** 內部的資料結構 `capacity` : 紀錄該 cache 的容量,能放多少值 `count` : 紀錄 cache 當前所使用的容量 `dhead` : 紀錄所有的鏈結串列 `hhead` : 紀錄各個 key 的對應的鏈結串列 ```c typedef struct { int key; int value; struct hlist_node node; struct list_head link; } LRUNode; ``` **LRUNode** 內部的資料結構 `key` : 該 node 所對應的 hash table 上的 key 值 `value` : 該 node 所存放的值 `node` : 與該串列上其他 node 連結的鏈結 `link` : 與 dhead 連結的鏈結 ```graphviz digraph LRUcache { node [shape=record]; rankdir = LR; subgraph cluster_a { label = LRUCache; struct0 [label="capacity"]; struct1 [label="count"]; struct2 [label="dhead | {<prev>prev | <next>next}"]; struct3 [label="<head>hhead[0] | <head1>hhead[1] | ..."]; } subgraph cluster_b { label = LRUNode1; struct6 [label="link | {<prev>prev | <next>next}"]; struct7 [label="node | {<prev>pprev | <next>next}"]; } subgraph cluster_c { label = LRUNode2; struct10 [label="link | {<prev>prev | <next>next}"]; struct11 [label="node | {<prev>pprev | <next>next}"]; } subgraph cluster_d { label = LRUNode3; struct12 [label="link | {<prev>prev | <next>next}"]; struct13 [label="node | {<prev>pprev | <next>next}"]; } struct2:next -> struct6; struct6:next -> struct10; struct10:next -> struct12; struct3:head -> struct7; struct3:head1 -> struct13; struct7:next -> struct11; } ``` ```c void hlist_del(struct hlist_node *n) { struct hlist_node *next = n->next, **pprev = n->pprev; *pprev = next; if (next) EEEE = pprev; } ``` 該函式主要是刪除指定的 node ,其中 `**pprev` 指向的是 `n` 的前一個 node 的 `next` 指標,所以 `*pprev = next` 這段是將 `n` 前一個 node 的 `next` 指向 `n` 的下一個 node 。而剩下部分就是希望 `next` 的前一個指向 `n` 的前一個,所以 `EEEE` 為 `next->pprev` 。 ```c void lRUCacheFree(LRUCache *obj) { struct list_head *pos, *n; list_for_each_safe (pos, n, &obj->dhead) { LRUNode *cache = list_entry(pos, LRUNode, FFFF); list_del(GGGG); free(cache); } free(obj); } ``` 這裡的 `pos` 型態是 `list_head` ,而 `LRUNode` 內對應 `list_head` 型態的結構是 `link` ,在 `list_entry` 中, `FFFF` 要填入和 `pos`同個型態,所以是 `node` 。 而 `void list_del(struct list_head *entry)` 這裡是希望傳入的是 `list_head` 的形式 , 所以是 `&cache->link` 。 ```c int lRUCacheGet(LRUCache *obj, int key) { int hash = key % obj->capacity; struct hlist_node *pos; hlist_for_each (pos, &obj->hhead[hash]) { LRUNode *cache = list_entry(pos, LRUNode, HHHH); if (cache->key == key) { list_move(IIII, &obj->dhead); return cache->value; } } return -1; } ``` 該 LRU cache 首先使用傳入的 `key` 值去算出在要找的值在 hash table 陣列上的哪個串列中,若沒有的話回傳 -1 ,有的話則將該節點的 `value` 回傳。 而其中因為 `pos` 是 `hlist_node` 的型態,所以 `HHHH` 是 `node` ; 因為 `void list_move(struct list_head *entry, struct list_head *head)` 所以 `IIII` 要填入的是 `*list_head` 的型態,所以是 `&cache->link` 。 ```c void lRUCachePut(LRUCache *obj, int key, int value) { LRUNode *cache = NULL; int hash = key % obj->capacity; struct hlist_node *pos; hlist_for_each (pos, &obj->hhead[hash]) { LRUNode *c = list_entry(pos, LRUNode, JJJJ); if (c->key == key) { list_move(KKKK, &obj->dhead); cache = c; } } if (!cache) { if (obj->count == obj->capacity) { cache = list_last_entry(&obj->dhead, LRUNode, link); list_move(&cache->link, &obj->dhead); hlist_del(&cache->node); hlist_add_head(&cache->node, &obj->hhead[hash]); } else { cache = malloc(sizeof(LRUNode)); hlist_add_head(&cache->node, &obj->hhead[hash]); list_add(&cache->link, &obj->dhead); obj->count++; } cache->key = key; } cache->value = value; } ``` 這段程式碼主要是到雜湊表內放入新的節點,如果該節點存在,則將該節點透過 `list_move` 函式放到 `dhrad` 所指向的串列最前面;若不存在,則檢查該 `LRUCache` 是否已滿,若已滿,則透過 `list_last_entry` 函式找到該鏈結串列的最後一個節點,將其放到值放在 `dhead` 串列的第一個節點上,並且透過 `hlist_del` 該函式刪除 `hhead` 串列上的節點,再將新的值插入在第一個節點上;若未滿,則將其插入在 `hhead` 鏈結串列和 `dhead` 鏈結串列的第一個節點上。 其中這裡的 `pos` 是 `hlist_node` 的型態,所以 `JJJJ` 是 `node` ,而 `KKKK` 需要是 `*list_head` 的型態,所以是 `&c->link` 。 ### 測驗三 ```c static inline unsigned long __ffs(unsigned long word) { int num = 0; #if BITS_PER_LONG == 64 if ((word & AAAA) == 0) { num += 32; word >>= 32; } #endif if ((word & 0xffff) == 0) { num += 16; word >>= 16; } if ((word & 0xff) == 0) { num += 8; word >>= 8; } if ((word & 0xf) == 0) { num += 4; word >>= 4; } if ((word & 0x3) == 0) { num += 2; word >>= 2; } if ((word & 0x1) == 0) num += 1; return num; } ``` 這裡主要是去計算傳入的 `AAAA` 可以根據規律判斷為 `0xffffffff` 。 ```c static inline void __clear_bit(unsigned long nr, volatile unsigned long *addr) { unsigned long mask = BIT_MASK(nr); unsigned long *p = ((unsigned long *) addr) + BIT_WORD(nr); *p &= BBBB; } ``` 該函數主要是希望將指定的位元清成 0 ,所以 `BBBB` 是 `~mask` 。 ```c #define FIND_NTH_BIT(FETCH, size, num) \ ({ \ unsigned long sz = (size), nr = (num), idx, w, tmp; \ \ for (idx = 0; (idx + 1) * BITS_PER_LONG <= sz; idx++) { \ if (idx * BITS_PER_LONG + nr >= sz) \ goto out; \ \ tmp = (FETCH); \ w = hweight_long(tmp); \ if (w > nr) \ goto found; \ \ nr -= w; \ } \ \ if (sz CCCC BITS_PER_LONG) \ tmp = (FETCH) & BITMAP_LAST_WORD_MASK(sz); \ found: \ sz = min(idx * BITS_PER_LONG + fns(tmp, nr), sz); \ out: \ sz; \ }) ``` 其中 `hweight_long()` 該函式主要是去計算這 8 個位元中有多少個 1 ,而程式內的 `for` 迴圈是在找第 n 個位元 1 放在哪裡,所以這裡的 `CCCC` 應填入 `%` 。