# 2024q1 Homework2 (quiz1+2) contributed by < [gawei1206](https://github.com/gawei1206) > ## 第一周測驗題 ### 測驗一 #### `list_tail()` / `list_length()` ```c node_t *list_tail(node_t **left) { while ((*left) && (*left)->next) left = &(*left)->next; //AAAA return *left; } int list_length(node_t **left) { int n = 0; while (*left) { ++n; left = &(*left)->next; //BBBB } return n; } ``` `list_tail` 和 `list_length` 都需要從頭逐一走訪所有節點,`*left` 指向 `list` 的第一個節點,透過`left = &(*left)->next` 更改 `left` 持續走訪下一個節點 #### `q_sort()` 以非遞迴的方式來實作快速排序法,透過 `begin` 與 `end` 作為堆疊,紀錄串列的範圍,過程中判斷 `begin[i]` 與 `end[i]` 是否相等,如果相等就用 `list_add` 將 `pivot` 加入到 `result` 的開頭 ```c if (L) list_add(&result, L); ``` 若不相等,透過`begin[i]` 取出一段串列,並把第一個節點設為 `pivot` ,走訪串列時將節點的 `value` 與 `pivot` 比較,小於等於 `pivot` 的節點加入 `left` 串列中,大於 `pivot` 的節點加入 `right` 串列中 ```c while (p) { node_t *n = p; p = p->next; //CCCC list_add(n->value > value ? &right : &left, n); } ``` 將一段串列分成 `left` 與 `right` 後,更新到 `begin` 與 `end` 中 ```c begin[i] = left; end[i] = list_tail(&left); //DDDD begin[i + 1] = pivot; end[i + 1] = pivot; begin[i + 2] = right; end[i + 2] = list_tail(&right); //EEEE ``` #### 以圖形化觀察變化 第一輪: 以測驗一的例子來看,第一個點作為 `pivot` ,以 `p` 開始走訪每一個節點,將各個節點分配至 `left` 與 `right` 之中 ![list](https://hackmd.io/_uploads/SJC0N0taa.png) 分配完成後,以 `begin[i]` 、 `end[i]` 指向 `left` 開始與結束的位址, `begin[i+2]` 、 `end[i+2]` 指向 `right` 開始與結束的位址, `begin[i + 1]` 、 `end[i + 1]` 則指向 `pivot` ![list](https://hackmd.io/_uploads/BkYggAYa6.png) ![list](https://hackmd.io/_uploads/S13MXRYpa.png) ![list](https://hackmd.io/_uploads/ry31m0tTp.png) 第二輪: 這時 `i = 2`,取出 `begin[2]` 紀錄的串列,並重複第一輪的操作 ![list](https://hackmd.io/_uploads/rywTykqp6.png) ![list](https://hackmd.io/_uploads/B1H5Jyqpa.png) ![list](https://hackmd.io/_uploads/rkNXlycpT.png) 第三輪開始: 這時 `i = 4` , `begin[4]` 指向 `NULL` , `i--` 接下來 `i = 3` 、 `i = 2` 、 `i = 1` 時 `begin[i] == end[i]` ,將節點加入到 `result` 中,完成右半部的排序 ![list](https://hackmd.io/_uploads/HktOjMcp6.png) 最後剩下 `begin[0]` 、 `end[0]` 中有元素,重複上面的動作 ![list](https://hackmd.io/_uploads/HJ6i3M5Tp.png)![list](https://hackmd.io/_uploads/r1r6hz5p6.png)![list](https://hackmd.io/_uploads/SJy1pG5p6.png) 再將這些節點加入 `result` 中,完成左半部的排序 ![list](https://hackmd.io/_uploads/BJEwaM5a6.png) #### 改善程式碼 - 使用第一周作業用到的 `element_t` 結構,透過雙向鏈結串列可以更快的存取最後一個節點,避免像函式 `list_tail` 一樣需要從頭走訪一次 - 在快速排序中 `pivot` 的選擇會影響執行的效率,選擇當前最大或最小的 `value` 作為 `pivot` 是最差的情況,可以透過 median of three 或 median of median 來改善這種情況 ### 測驗二 ## 第二周測驗題 ### 測驗一 以DFS的方法重建一棵樹,首先以 `node_add` 這個函式將節點加入雜湊表中 ```c for (int i = 0; i < inorderSize; i++) node_add(inorder[i], i, inorderSize, in_heads); ``` #### `node_add` 找出此節點應該存放的開頭位址,並以 `hlist_add_head` 將他加入鏈結串列的開頭 ```c int hash = (val < 0 ? -val : val) % size; hlist_add_head(&on->node, &heads[hash]); //DDDD ``` #### `hlist_add_head` 從[Linux 核心的 hash table 實作](https://hackmd.io/@sysprog/linux-hashtable)可以看到 `hlist_node` 的結構與使用方式 Linux 核心的 hash table 實作中,用以處理 hash 數值碰撞的 `hlist_node` 的結構: ```c struct hlist_node { struct hlist_node *next, **pprev; }; ``` 再來可以知道知到 `next` 指向相鄰的節點本身,而 `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; } ``` ```c static inline void hlist_add_head(struct hlist_node *n, struct hlist_head *h) { if (h->first) h->first->pprev = &n->next; n->next = h->first; //AAAA n->pprev = &h->first; h->first = n; } ``` 所以在 `hlist_add_head` 中要插入一個節點時,會先判斷 `hlist_head` 是否已經有儲存節點,若存在節點,先將第一個節點 `h->first` 的 `pprev` 指向 `&n->next`,再來將 `n->next` 指向第一個節點 `h->first` #### `find` ```c 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, &heads[hash]) { //BBBB struct order_node *on = list_entry(p, struct order_node, node); //CCCC if (num == on->val) return on->idx; } return -1; } ``` 再算出雜湊值後,找出對應的雜湊表開頭,走訪並找到符合條件的節點,回傳數值在 `inorder` 中的索引值 #### `dfs` ```c 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); ``` 最後以dfs重建整棵樹時從 `preorder[0]` 開始,前序排列中第一個點為 root ,找出他在 `inorder` 中的索引,計算出 `preorder` 與 `inorder` 中左右子樹的索引範圍,透過遞迴重建整棵樹 #### 左子樹 ```graphviz digraph a { node [shape= "none"] pre[shape = record, label = " <l0> 3 | <l1> 9 | <l2> 20 | <l3> 15 | <l4> 7 ",height=0.2, width=2]; p_left_h [label="pre_low + 1", fontcolor="red" ] p_left_t [label="pre_low + (idx - in_low)", fontcolor="red" ] p_left_h->pre:l1 p_left_t->pre:l1 preorder->pre:l0 // in[shape = record, label = " <l0> 9 | <l1> 3 | <l2> 15 | <l3> 20 | <l4> 7 ",height=0.2, width=2]; i_left_h [label="in_low", fontcolor="red" ] i_left_t [label="idx - 1", fontcolor="red" ] in:l0 -> i_left_h [dir=back] in:l0 -> i_left_t [dir=back] inorder->in:l0 } ``` #### 右子樹 ```graphviz digraph a { node [shape= "none"] pre[shape = record, label = " <l0> 3 | <l1> 9 | <l2> 20 | <l3> 15 | <l4> 7 ",height=0.2, width=2]; p_right_h [label="pre_high-(in_high-idx-1)", fontcolor="blue"] p_right_t [label="pre_high", fontcolor="blue"] p_right_h -> pre:l2 p_right_t -> pre:l4 preorder->pre:l0 // inorder in[shape = record, label = " <l0> 9 | <l1> 3 | <l2> 15 | <l3> 20 | <l4> 7 ",height=0.2, width=2]; i_right_h [label="idx + 1", fontcolor="blue"] i_right_t [label="in_high", fontcolor="blue"] i_right_h -> in:l2 i_right_t -> in:l4 inorder->in:l0 } ``` #### 延伸問題 ### 測驗二 #### 程式碼解說 在 [Leetcode 146. LRU Cache](https://leetcode.com/problems/lru-cache/) 中我們得知三個函式的功能: - `lRUCacheCreate` : 創建大小為 `size` 的快取 - `lRUCacheGet` : 如果 `key` 存在快取回傳 `value`,否則回傳 -1 - `lRUCachePut` : 如果 `key` 存在,更新快取中的 `key,value` ,否則新增 `key,value` 至快取中,如果超過快取大小,移除最久沒使用的 `key` 為了完成程式碼,需了解以下結構: ```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; ``` `LRUCache` 中的 `dhead` 使用雙向環狀串列來紀錄快取的使用情形,近期有使用過的節點會越靠近 `dhead`,`hhead[]` 則是像測驗一中一樣,用雜湊表來紀錄節點,提高存取節點的速度 `LRUNode` 中的 `node` 會根據雜湊值被紀錄在對應的 `hhead[]` 裡,`link` 則會加入 `dhead` 的串列中,根據使用情形改變在串列的位置 #### lRUCacheGet ```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, node); //HHHH if (cache->key == key) { list_move(&cache->link, &obj->dhead); //IIII return cache->value; } } return -1; } ``` 先以 `value` 計算出雜湊值,再至對應的串列逐一走訪,找出 `key` 值相符的 `cache` ,如果資料存在,將找到的節點更新至 `dhead` 的開頭 #### lRUCachePut ```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, node); //JJJJ if (c->key == key) { list_move(&cache->link, &obj->dhead); //KKKK 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; } ``` 主要在做三件事, 1. 第一段與 `lRUCacheGet` 相似,目的是查看欲加入的節點是否存在快取中,如果存在更新他在 `dhead` 中的位置 2. 再來是如果想加入節點但空間不足的情況,會將最久沒使用的節點刪除,並加入新節點在 `dhead` 開頭 3. 最後可以加入節點,配置節點需要的空間,插入新節點到雜湊表,也加入到 `dhead` 開頭 ### 測驗三