--- tags: linux2022 --- # 2022q1 Homework (quiz1) contributed by < `hankluo6` > > [第 1 週測驗題](https://hackmd.io/@sysprog/linux2022-quiz1) ## 測驗 `1` ### 運作原理 ```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; } ``` #### `AAA` - `n->next = first` #### `BBB` - `n->pprev = &h->first` `map_add` 將新的節點加入到 hash 中,`find_key` 先從 hash 檢查該 key 是否已經存在,如果已經存在則直接回傳,尚未存在則分配新的記憶體放置 `data`。故可知 `AAA` 及 `BBB` 為將新的節點連接到 hash 上的 linked list,且為插入到 linked list 的前端。 ```graphviz digraph linked_list{ rankdir = LR; node [shape = record]; node0 [label = "<f0> map.ht[0] | <f1> map.ht[1] |<f2> map.ht[2]",height=1.2]; old_data [height=0.4]; "map.ht"[height=0.4,color=white]; node0:f0->old_data [label=first]; old_data->node0:f0 [label=prev]; { rank="same"; "map.ht"; } } ``` * add new item ```graphviz digraph linked_list{ rankdir = LR; node [shape = record]; node0 [label = "<f0> map.ht[0] | <f1> map.ht[1] |<f2> map.ht[2]",height=1.2]; old_data [height=0.4]; new_data [height=0.4]; "map.ht"[height=0.4,color=white]; node0:f0->new_data [label=<<font color="red">first</font>>, color=red]; new_data->node0:f0 [label=<<font color="red">prev</font>>, color=red]; new_data->old_data [label=next]; old_data->new_data [label=prev]; { rank="same"; "map.ht"; } } ``` ```c void map_deinit(map_t *map) { if (!map) return; for (int i = 0; i < MAP_HASH_SIZE(map->bits); i++) { struct hlist_head *head = &map->ht[i]; for (struct hlist_node *p = head->first; p;) { struct hash_key *kn = container_of(p, struct hash_key, node); struct hlist_node *n = p; p = p->next; if (!n->pprev) /* unhashed */ goto bail; struct hlist_node *next = n->next, **pprev = n->pprev; *pprev = next; if (next) next->pprev = pprev; n->next = NULL, n->pprev = NULL; bail: free(kn->data); free(kn); } } free(map); } ``` `map_deinit` 釋放所有 hash 的記憶體,`for` 迴圈遍歷所有 hash entry,內部在一個一個把 linked list 上的節點及其 data 釋放。在遍歷每個節點時,同時透過指標的指標更改 `head->firt` 及 `next->prev`。 ```graphviz digraph linked_list{ rankdir = LR; node [shape = record]; node0 [label = "<f0>map.ht[0]|{<f1>first.addr|<f2>first}",height=0.4]; data1 [label = "<f0>data1|{<f1>pprev|<f2>next}|<f3>node.addr", height=0.4]; data2 [label = "<f0>data2|{<f1>pprev|<f2>next}|<f3>node.addr", height=0.4]; node0:f2->data1:f0; data1:f1->node0:f1; data1:f2->data2:f0; data2:f1->data1:f3; "map.ht"[height=0.4,color=white]; } ``` * 刪除的節點 `n` 為 `data1`。 ```graphviz digraph linked_list{ rankdir = LR; node [shape = record]; node0 [label = "<f0>map.ht[0]|{<f1>first.addr|<f2>first}",height=0.4]; data1 [label = "<f0>data1|{<f1>pprev|<f2>next}|<f3>node.addr", height=0.4]; data2 [label = "<f0>data2|{<f1>pprev|<f2>next}|<f3>node.addr", height=0.4]; node0:f2->data1:f0; data1:f1->node0:f1 [label=pprev]; data1:f2->data2:f0 [label=next]; data2:f1->data1:f3; "map.ht"[height=0.4,color=white]; "n"[height=0.4,color=white]; {rank=same;"n";data1}; } ``` * 將 `pprev` 的**指標內指向的值**(`map.ht[0].first`)設為 `next`;`next` 的 `prev` 設為 `pprev` 指標的指標,並把 `n` 的 `prev` 及 `next` 設為 `NULL`,最後釋放。 ```graphviz digraph linked_list{ rankdir = LR; node [shape = record]; node0 [label = "<f0>map.ht[0]|{<f1>first.addr|<f2>first}",height=0.4]; data1 [label = "<f0>data1|{<f1>pprev|<f2>next}|<f3>node.addr", height=0.4]; data2 [label = "<f0>data2|{<f1>pprev|<f2>next}|<f3>node.addr", height=0.4]; node0:f2->data2:f0; data2:f1->node0:f1; "map.ht"[height=0.4,color=white]; "n"[height=0.4,color=white]; "n"->"map.ht"[color=white]; {rank=same;"n";data1}; {rank=same;node0;"map.ht"}; } ``` ### Linux `hashtable` #### `DEFINE_HASHTABLE` & `DECLARE_HASHTABLE` ```c #define DEFINE_HASHTABLE(name, bits) \ struct hlist_head name[1 << (bits)] = \ { [0 ... ((1 << (bits)) - 1)] = HLIST_HEAD_INIT } #define DECLARE_HASHTABLE(name, bits) \ struct hlist_head name[1 << (bits)] ``` 以 `hlist_head` 當作每個 hash table 上每個 bucket 的 list,其中不用 `list_node` 的原因為不需要用到 circular linked list。`bits` 決定需要 hash table 的大小。 特別要注意的為初始化 `name` 時使用的 `...` 為 GCC extension 的 [Designated Initializers](https://gcc.gnu.org/onlinedocs/gcc/Designated-Inits.html),可以一次初始化所有 index 為 `HLIST_HEAD_INIT`。 `HLIST_HEAD_INIT` 定義在 `list.h` 內,及初始化 `first` 為 `NULL`。 #### `hash_init` ```c static inline void __hash_init(struct hlist_head *ht, unsigned int sz) { unsigned int i; for (i = 0; i < sz; i++) INIT_HLIST_HEAD(&ht[i]); } #define hash_init(hashtable) __hash_init(hashtable, HASH_SIZE(hashtable)) ``` 專門將 `DECLARE_HASTABLE` 宣告的 hash table 初始化,作用與 `DEFINE_HASTABLE` 相同。 #### `hash_add` & `hash_del` ```c #define hash_add(hashtable, node, key) \ hlist_add_head(node, &hashtable[hash_min(key, HASH_BITS(hashtable))]) static inline void hash_del(struct hlist_node *node) { hlist_del_init(node); } ``` `add` 與 `del` 都為呼叫 `hlist` API,其實作皆與 `list_head` 相似。 #### `hash_for_each` ```c #define hash_for_each(name, bkt, obj, member) \ for ((bkt) = 0, obj = NULL; obj == NULL && (bkt) < HASH_SIZE(name);\ (bkt)++)\ hlist_for_each_entry(obj, &name[bkt], member) ``` 遍歷 hash table **所有**的 entry,`bkt` 表示 bucket,每一次迴圈透過 `hlist_for_each_entry` 遍歷這個 bucket 內的 linked list。 ### GOLDEN_RATIO ```c #define GOLDEN_RATIO_32 0x61C88647 #define GOLDEN_RATIO_64 0x61C8864680B583EBull ``` :::info 註解中的 `phi = (sqrt(5)-1)/2` 指的是 $\phi^{-1}$,為 golden ratio 的倒數。 ::: * $\text{0x61C88647}=\text{1640531527}=2^{32}\times\phi^{-1}=2^{32}\times\frac{\sqrt{5}-1}{2}$ * $\text{0x61C8864680B583EB}=\text{7046029254386353842}\approx2^{64}\times\phi^{-2}=2^{64}\times\frac{3-\sqrt{5}}{2}$ 任意數乘上這兩個數再取其高位元可以呈現好的數值分布。在連續的數值中,利用這種 Fibonacci Hashing 方法可以將 hash 後對應的值分散到其他 hash value 的值之間 (將最大區間以 golden ratio 分割的點)。 ![](https://i.imgur.com/4GOD8gB.png) 而現實生活上的 keys 值分布常常會有某種數值分布,類似 $\{K, K+d, K+2d, ..., K+td\}$,例如字串集合 $\{"PART1", "PART2", "PART3"\}$,這種情況利用此種 hash function 的行為就如同計算 $h(0), h(1), h(2)$,便能將 key 值映射到沒有使用過的 value,減少 collision 的機率。 假設 $\theta$ 為一有理數,要將 $\{\theta, 2\theta, 3\theta, ..., n\theta\}$ 的小數部分放到 $[0, 1]$ 之間,則會發現 $(n+1)\theta$ 放置的位置會在由前面的點分割成的線段中最長的線段,而當 $\theta$ 過大或過小時 (接近 0 或 1),產生的分布區間便會由小而大,並不是均勻分布。 可證明能產生較好的分布的數值為 $\phi^{-1}$ 及 $\phi^{-2}$ (Knuth vol 3, section 6.4, exercise 9.),故選擇這兩個數計算 hash。 * Reference: [The Art of Computer Programming, section 6.4](https://doc.lagout.org/science/0_Computer%20Science/2_Algorithms/The%20Art%20of%20Computer%20Programming%20%28vol.%203_%20Sorting%20and%20Searching%29%20%282nd%20ed.%29%20%5BKnuth%201998-05-04%5D.pdf) ## 測驗 `2` ### 運作原理 #### `COND1` - `head->next && head->val == head->next->val` #### `COND2` - `head->next && head->val == head->next->val` 根據註解可知道第二個 `if` 用來刪除重複的節點,故 `COND1` 應為判斷是否有重複的值。在 `if` block 裡面最後回傳 `head->next`,可知道中間 `while` 迴圈需跳過相同值的節點,最後停在所有有著相同值的節點中的最後一個節點,得出 `COND2`。 ### 改寫 ```c struct ListNode *deleteDuplicates(struct ListNode *head) { if (!head) return NULL; struct ListNode *newhead = malloc(sizeof(struct ListNode)); newhead->next = head; struct ListNode *prev = newhead; while (head) { if (head->next && head->val == head->next->val) { while (head->next && head->val == head->next->val) { head = head->next; } prev->next = head->next; } else { prev = prev->next; } head = head->next; } return newhead->next; } ``` ## 測驗 `3` ### 運作原理 #### `MM1` - `list_for_each_entry_safe` #### `MM2` - `list_for_each_entry` #### `MM3` - `list_for_each_entry` #### `MM4` - `list_last_entry` LRU 由兩個部分組成 * `dhead`:為 doubly-linked list,用來取得 **Least Recently Used** 的 node,靠近頭端部分表示存取時間與當前時間越近,靠近尾端則越遠。 * `hheads`:為 hash map,用來儲存 key 及 value 的資訊,以 `key % capacity` 當作 hash function。 `GET` 操作時,從對應的 hash 值的 list 中尋找,如果有找到,透過 `list_move` 將該 node 移到 `dhead` 的頭端,表示最近使用。故 `MM2` 為 `list_for_each_entry` 遍歷對應 hash 上的 list。 `PUT` 操作時,先檢查該 key 是否有存在 hash map 當中,故 `MM3` 與 `MM2` 相同,如果不存在,則要新建一個新的 node,此時若 LRU 已滿,必須剔除掉最久沒使用的節點,該節點及為 `dhead` 的 tail,故 `MM4` 為 `list_last_entry`。建立完新的節點後,再放到 `dhead` 及對應的 `hhead` 頭端。 `CREATE` 操作時,必須將所有 `list_head` 透過 `INIT_LIST_HEAD` 初始化,把相對應的 `next` 與 `prev` 欄位設定,list 相關巨集才能正確使用。 `FREE` 操作時,因為每個存在於 hash map 的 node 都與 `dhead` 鏈結,所以只要遍歷 `dhead` 釋放所有節點即可。 * 初始化,capacity 為 3 ```graphviz digraph linked_list{ rankdir = LR; node [shape = record]; node0 [label = "<f0> hheads[0]<style></style> | <f1> hheads[1] |<f2> hheads[2]",height=1.2]; "dhead"[height=0.4]; { rank="same"; node0; "dhead" } } ``` * push item1 (hash value: 0) ```graphviz digraph linked_list{ rankdir = LR; node [shape = record]; node0 [label = "<f0> hheads[0] | <f1> hheads[1] |<f2> hheads[2]",height=1.2]; "dhead"[height=0.4]; "item1:hlink"[height=0.4]; "item1:dlink"[height=0.4]; node0:f0->"item1:hlink"; "dhead"->"item1:dlink"; { rank="same"; node0; "dhead" } } ``` * push item2 (hash value: 1),新建立的 node 放到 list 的頭端 ```graphviz digraph linked_list{ rankdir = LR; node [shape = record]; node0 [label = "<f0> hheads[0] | <f1> hheads[1] |<f2> hheads[2]",height=1.2]; "dhead"[height=0.4]; "item1:dlink"[height=0.4]; "item2:dlink"[height=0.4]; node1 [label = "<f0> item1:hlink | <f1> item2:hlink",height=0.8]; node0:f0->node1:f0; node0:f1->node1:f1; "dhead"->"item2:dlink"->"item1:dlink"; { rank="same"; node0; "dhead" } } ``` * push item3 (hash value: 0) ```graphviz digraph linked_list{ rankdir = LR; node [shape = record]; node0 [label = "<f0> hheads[0] | <f1> hheads[1] |<f2> hheads[2]",height=1.2]; "dhead"[height=0.4]; "item1:hlink"[height=0.4]; "item1:dlink"[height=0.4]; "item2:dlink"[height=0.4]; "item3:dlink"[height=0.4]; node1 [label = "<f0> item3:hlink | <f1> item2:hlink",height=0.8]; node0:f0->node1:f0->"item1:hlink"; node0:f1->node1:f1; "dhead"->"item3:dlink"->"item2:dlink"->"item1:dlink"; { rank="same"; node0; "dhead" } } ``` * push item4 (hash value: 2),因為 LRU 空間已滿,將最久未使用的節點 (item1) 移除 ```graphviz digraph linked_list{ rankdir = LR; node [shape = record]; node0 [label = "<f0> hheads[0] | <f1> hheads[1] |<f2> hheads[2]",height=1.2]; "dhead"[height=0.4]; "item3:dlink"[height=0.4]; "item2:dlink"[height=0.4]; "item4:dlink"[height=0.4]; node1 [label = "<f0> item3:hlink | <f1> item2:hlink | <f2> item4:hlink",height=1.2]; node0:f0->node1:f0; node0:f1->node1:f1; node0:f2->node1:f2; "dhead"->"item4:dlink"->"item3:dlink"->"item2:dlink"; { rank="same"; node0; "dhead" } } ``` :::info `item:dlink` 與 `item:hlink` 為同一個物件中的不同成員 ::: --- ## 測驗 `4` ### 運作原理 #### `LLL` - `--left` #### `RRR` - `++right` `longestConsecutive` 內的第二個 `for` 迴圈將 `nums` 陣列中出現的所有數字以 hash map 存起。第三個 `for` 迴圈遍歷整個陣列,每次迴圈有個 current value 當作基準點,`left` 表示往小於 current value 的方向從 hash map 中尋找;`right` 表示往大於 current value 的方向尋找。因為要尋找**連續**的序列,故每次都以加一或減一的值尋找,得出 `--left` 及 `++right`。每找到一個數字與 current value 為連續序列,則將其從 hash map 中移除,下次迴圈選到相同序列內的數就不需要重新計算。 ### 改寫 ```c struct seq_node { int num; struct hlist_head link; }; static struct seq_node *find(int num, int size, struct hlist_head *map) { struct seq_node *node; int hash = num < 0 ? -num % size : num % size; hash_for_each_possible (node, map, link) { if (node->num == num) return node; } return NULL; } int longestConsecutive(int *nums, int n_size) { DEFINE_HASHTABLE(map, n_size); 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]; hash_add(map, node, hash); } } for (int i = 0; i < n_size; i++) { int len = 0; int num; node = find(nums[i], n_size, heads); while (node) { len++; num = node->num; hash_del(&node->link); int left = num, right = num; while ((node = find(LLL, n_size, heads))) { len++; hash_del(&node->link); } while ((node = find(RRR, n_size, heads))) { len++; hash_del(&node->link); } length = len > length ? len : length; } } return length; } ```