# 2022q1 Homework (quiz1) contributed by <`ray90514`> [題目](https://hackmd.io/@sysprog/linux2022-quiz1) ## 測驗一 ```c void map_add(map_t *map, int key, void *data) { struct hash_key *kn = find_key(map, key); if (kn) return; kn = malloc(sizeof(struct hash_key)); kn->key = key, kn->data = data; struct hlist_head *h = &map->ht[hash(key, map->bits)]; struct hlist_node *n = &kn->node, *first = h->first; AAA; if (first) first->pprev = &n->next; h->first = n; BBB; } ``` 這段程式碼的用途是要將 key 加入到 hashtable 裡,而後半段的程式碼是將建立的節點加入給定的 list 。 ![](https://i.imgur.com/7bcWR6h.png) ![](https://i.imgur.com/XlQb0IX.png) ```c struct hlist_node { struct hlist_node *next, **pprev; }; struct hlist_head { struct hlist_node *first; }; ``` 由圖可以看出有四個指標需要修改 `h->first`, `n->pprev`, `n->next`, `first->prev` ,上述程式碼已經修改了兩個,所以 `AAA` 和 `BBB` 是修改 `n` 的兩個指標。 ```c n->pprev = &h->first; n->next = first; ``` ### 延伸問題 程式的運作如下,給定一個 `sum` 及一個數字 `num` ,我們需要看 `sum - num` 是否在陣列裡面,比較快速的作法是對將陣列的數字建成 hashtable ,這樣查找一個數字只需要 $O(1)$ ,確認整個陣列要 $O(n)$ 。這裡有個細節是查找與建立 hashtable 可以同時進行,這樣就一次迴圈就能完成。 ## 測驗二 ```c #include <stddef.h> struct ListNode { int val; struct ListNode *next; }; struct ListNode *deleteDuplicates(struct ListNode *head) { if (!head) return NULL; if (COND1) { /* Remove all duplicate numbers */ while (COND2) head = head->next; return deleteDuplicates(head->next); } head->next = deleteDuplicates(head->next); return head; } ``` 這段程式碼是採用遞迴的方式移除重複的節點的。 ```c head->next = deleteDuplicates(head->next); ``` 這一行是將 `head` 與以 `head->next` 為開頭被移除重複節點的串列接在一起,代表 `head->next` 以前都是不重複的。所以中間那段程式碼是要從 `head` 開始移除重複的節點。 `head` 有重複的條件,因為要比較 `head->next` 的值所以要加上判斷不為空指標, `COND1` 就是 `head->next && head->val == head->next->val` ,如果有重複發生,接下來就是要使用迴圈找出所有重複的節點,所以 `COND2` 與 `COND1` 相同。 ### 延伸問題 #### 使用迴圈修改程式碼。 ```c #include <stddef.h> struct ListNode { int val; struct ListNode *next; }; struct ListNode *deleteDuplicates(struct ListNode *head) { int is_dup = 0; struct ListNode *node = head; struct ListNode **prev = &head; while (node) { if(node->next && node->val == node->next->val) { is_dup = 1; } else if (is_dup) { is_dup = 0; *prev = node->next; } else { prev = &(*prev)->next; } node = node->next; } return head; } ``` ![](https://i.imgur.com/ElSXrAz.png) `prev` 是一個指標的指標,使用 `node` 走訪整個串列,如果 `node` 和 `node->next` 的值不同就移動 `prev` 和 `node`。 ![](https://i.imgur.com/t80cGpX.png) 如果 `node` 和 `node->next` 的值相同就跳過重複的節點,直到找到不同的節點,將 `prev` 所指的 `next` 指標指向該節點。 ![](https://i.imgur.com/Q1PZgOu.png) 重複以上動作直到所有節點都被走訪過。 #### 以類似 Linux 核心的 circular doubly-linked list 改寫 兩個版本都與原本相似,更改了原本對 `NULL` 的判斷,以及對 `prev` 的處理 ```c struct ListNode { int val; struct list_node list; }; ``` 遞迴版本。 ```c struct list_node *deleteDuplicates(struct list_node *head) { static struct list_node *h = NULL; if (head == h) return h; if(!h) h = head; if (head->next != h) { int val = list_entry(head, ListNode, list)->val; int val_next = list_entry(head->next, ListNode, list)->val; if(val == val_next) { /* Remove all duplicate numbers */ while (head->next != h && val == val_next) { head = head->next; val = list_entry(head, ListNode, list)->val; val_next = list_entry(head->next, ListNode, list)->val; } return deleteDuplicates(head->next); } } head->next = deleteDuplicates(head->next); head->next->prev = head; return head; } ``` 迴圈版本,將原本指向前一個節點的 `next` 的指標改成指向前一個節點的指標。 ```c struct list_node *deleteDuplicates(struct list_head *head) { if (!head) return NULL; int is_dup = 0; ListNode *entry; ListNode *safe; struct list_head *prev = head; list_for_each_entry_safe(entry, safe, head, list) { if(&safe->list != head && entry->value == safe->value) { is_dup = 1; } else if (is_dup) { is_dup = 0; prev->next = &safe->list; safe->list.prev = prev; } else { prev = prev->next; } } return true; } ``` ## 測驗三 ```c MMM1 (lru, n, &obj->dhead, dlink) { list_del(&lru->dlink); free(lru); } ``` 因為這裡面用到了 `list_del` ,可以判斷是帶有 `safe` 的 list 巨集,而 `lru` 是 `LRUNode` ,所以 `MMM1` 是 `list_for_each_entry_safe` 。 ```c MMM2 (lru, &obj->hheads[hash], hlink) { if (lru->key == key) { list_move(&lru->dlink, &obj->dhead); return lru->value; } } ``` ```c MMM3 (lru, &obj->hheads[hash], hlink) { if (lru->key == key) { list_move(&lru->dlink, &obj->dhead); lru->value = value; return; } } ``` `lru` 是 `LRUNode` ,而參數的數量為三個,所以 `MMM2` 和 `MMM3` 是 `list_for_each_entry` 。 ```c if (obj->count == obj->capacity) { lru = MMM4(&obj->dhead, LRUNode, dlink); list_del(&lru->dlink); list_del(&lru->hlink); } ``` `MMM4` 會回傳一個 `LRUNode` , 所以可能為 `list_first_entry` 或 `list_last_entry` ,而這段程式碼是在 `LRUCachePut` 裡判斷當空間滿的時後,要挑選一個節點移除,題目是要做 Least Recently Used ,且 `lRUCacheGet` 會將存取到的節點搬到開頭,所以這裡要移除的是尾端的節點,因此 `MM4` 為 `list_last_entry`。 ### 延伸問題 LRU 用串列來實作的話,其中一個做法是,當存取節點時將其搬至開頭或尾端,而需要移除或取代時,就從另外一端進行。 ![](https://i.imgur.com/dyKeFiw.png) ![](https://i.imgur.com/ti0kO1d.png) ![](https://i.imgur.com/iXa8MNk.png) 考慮到存取時的效率,可以將節點放入 hashtable 。本題的程式碼利用同樣的想法去實作 LRU Cache。 ```c typedef struct { int capacity, count; struct list_head dhead, hheads[]; } LRUCache; typedef struct { int key, value; struct list_head hlink, dlink; } LRUNode; ``` `dhead` 是負責處理 LRU 的串列, `hheads[]` 是 hashtable 的部分, `capacity` 紀錄最大的容量, `count` 紀錄現有的容量。這裡的 hash table 使用陣列和串列完成。 `LRUNode` 則儲存需要用的資料與指標。 ```c LRUCache *lRUCacheCreate(int capacity) ``` 建立指定容量的 `LRUCache` ,並初始化。 ```c void lRUCacheFree(LRUCache *obj) ``` 釋放所有`LRUCache` 及所含節點的空間。 ```c int lRUCacheGet(LRUCache *obj, int key) ``` 取得 key 所對應的 value 。先是用 hashtable 直接取得節點,再將該節點移至串列的開頭。 ```c void lRUCachePut(LRUCache *obj, int key, int value) ``` 儲存 key 與對應的 value ,先是在 hashtable 中檢查 key 是否已存在,若存在就直接返回值。若不存在且容量沒滿則要建立一個新節點,若是容量滿了則要取代尾端的節點。最後將這個節點放入串列的開頭,以及 hashtable 對應的位置,還有更新 `count`。 #### 改善 使用 `hlist_head` 跟 `hlist_node` 取代原本的 `list_head` ,將原本 list 的巨集換成 hlist 的。 ```c typedef struct { int capacity, count; struct list_head dhead; struct hlist_head hheads[]; } LRUCache; typedef struct { int key, value; struct list_head dlink; struct hlist_node hlink; } LRUNode; ``` 以下是測試用的程式碼。`min` 是找出陣列最小值的 index , `init` 是將陣列初始化為零。測驗方法是放入與最大容量相同的資料,對這些資料做隨機存取。最後確認這些資料被取代的順序是否為 LRU ,使用時間戳的方法進行確認,有一個陣列 `count` 負責紀錄時間戳,每次存取都會更新,時間戳最小者為要被取代的。 ```c int main(int argc, char *argv[]) { int test_n = 100; int test_m = 10; int size = 16; int *count = malloc(size * sizeof(int)); LRUCache *cache = lRUCacheCreate(size); srand(0); for (int j = 0; j < test_m; j++) { init(count, size); /* put data */ for (int i = 0; i < size; i++) { count[i] = i; lRUCachePut(cache, i, rand()); } /* random access */ for (int i = size; i < test_n; i++) { int key = rand() % size; count[key] = i; lRUCacheGet(cache, key); } /* check the result */ for (int i = 0; i < size; i++) { int key = size + i; lRUCachePut(cache, key, rand()); key = min(count, size); if(lRUCacheGet(cache, key) != -1) { lRUCacheFree(cache); free(count); printf("fail\n"); return 0; } count[key] = test_n + 1; } } printf("sucess\n"); free(count); lRUCacheFree(cache); return 0; } ``` ## 測驗四 ```c while (node) { len++; num = node->num; list_del(&node->link); int left = num, right = num; while ((node = find(LLL, n_size, heads))) { len++; list_del(&node->link); } while ((node = find(RRR, n_size, heads))) { len++; list_del(&node->link); } length = len > length ? len : length; } ``` 題目是要找出陣列中最長序列的長度,而這段程式碼是某個數字找出它所處的序列的長度。 `find` 可以找出第一個參數的數字是否存在,所以可以用來尋找某個數字 n 的 ..., n - 2, n - 1, n, n + 1, n + 2, ... 是否存在,以確定序列的長度。 所以 `LLL` 和 `RRR` 各為一個以 `num` 遞增和遞減的表示式,因為 `left` 和 `right` 初始值為 `num` ,所以可能為 `--left` 和 `++right` 或 `++left` 和 `--right` 。 ### 延伸問題 程式運作大致如下,對給定陣列內的數字建立不重複的 hashtable ,用這個 hashtable 可以快速得知該數字存不存在。 ```c for (int i = 0; i < n_size; i++) { if (!find(nums[i], n_size, heads)) { hash = nums[i] < 0 ? -nums[i] % n_size : nums[i] % n_size; node = malloc(sizeof(*node)); node->num = nums[i]; list_add(&node->link, &heads[hash]); } } ``` 接下來找出陣列內的每個數字所在序列長度的最大值,查找方法如上述所說,查找過的數字要從 hashtable 移除,因為是同一序列,再一次遇到可不必查找。 ###### tags: `linux2022`