--- tags: linux2022 --- # 2022q1 Homework1 (quiz1) contributed by < `cwl0429` > > [測驗題目](https://hackmd.io/@sysprog/linux2022-quiz1) ## 測驗 `1` - 藉由 bitwise operation,定義 hash table 大小 ```c #define MAP_HASH_SIZE(bits) (1 << bits) ``` - 配置空間給 map 及 map->ht,建立一個 map ```c map_t *map_init(int bits) { map_t *map = malloc(sizeof(map_t)); if (!map) return NULL; map->bits = bits; map->ht = malloc(sizeof(struct hlist_head) * MAP_HASH_SIZE(map->bits)); if (map->ht) { for (int i = 0; i < MAP_HASH_SIZE(map->bits); i++) (map->ht)[i].first = NULL; } else { free(map); map = NULL; } return map; } ``` 記得將 map->ht 所有的 first 指標初始化,避免有存取安全疑慮 ```c for (int i = 0; i < MAP_HASH_SIZE(map->bits); i++) (map->ht)[i].first = NULL; ``` - 用 linux 風格,將 hlist_node 嵌在 hash_key 內,方便後續操作 ```c struct hash_key { int key; void *data; struct hlist_node node; }; ``` - 藉由 type 及當前 ptr 位址,回推出此 type 結構的起始位址 ```c #define container_of(ptr, type, member) \ ({ \ void *__mptr = (void *) (ptr); \ ((type *) (__mptr - offsetof(type, member))); \ }) ``` - 根據 hash table 的 bits 數量,將 hash 結果限縮在多少 bits 內 - 譬如 bits = 4,則 hash 範圍在 0~15 (0000~1111) ```c #define GOLDEN_RATIO_32 0x61C88647 static inline unsigned int hash(unsigned int val, unsigned int bits) { /* High bits are more random, so use them. */ return (val * GOLDEN_RATIO_32) >> (32 - bits); } ``` 為何使用 GOLDEN RATIO 作為 hash function? - 根據 [Fibonacci Hashing](https://probablydance.com/2018/06/16/fibonacci-hashing-the-optimization-that-the-world-forgot-or-a-better-alternative-to-integer-modulo/) 的說明,在作者設計的實驗中,fibonacci hashing 較傳統的 integer modulo 快了兩倍左右(在資料量大於 400000 時),圖表如下: ![](https://i.imgur.com/eiKVFn4.png) 其中只有 ska::unordered_map 使用 fibonacci hashing - 先藉由 key 找到對應的 hlist_head,接著在此 head 不斷往後找,直到有 key 相等或找完所有 hlist ```c static struct hash_key *find_key(map_t *map, int key) { struct hlist_head *head = &(map->ht)[hash(key, map->bits)]; for (struct hlist_node *p = head->first; p; p = p->next) { struct hash_key *kn = container_of(p, struct hash_key, node); if (kn->key == key) return kn; } return NULL; } ``` - 取得 key 所在位址,若找到則傳該 key node ```c void *map_get(map_t *map, int key) { struct hash_key *kn = find_key(map, key); return kn ? kn->data : NULL; } ``` - 將 data 加入 map 中 ```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; n->next = first; // AAA; if (first) first->pprev = &n->next; h->first = n; n->pprev = &h->first; // BBB; ``` 原先的 code 缺少了 AAA 及 BBB 兩部分,以下是不加 AAA 及 BBB 的示意圖(圖表改自[開發紀錄(quiz1)](https://hackmd.io/@Risheng/linux2022-quiz1),假設 hash key 為 n ): ```graphviz digraph G { rankdir = LR; node[shape = "record"] map[label = "map|bits|<h>ht"] ht[label = "Hash table|<m1>first_1|<m2>first_2|first_3|...|first_n-1|<mn>first_n"] f2_hk_1[label = "hash_key | key | data | <m>hlist_node | {<p>pprev | <n>next}"] f2_hk_2[label = "hash_key | key | data | <m>hlist_node | {<p>pprev | <n>next}"] f2_hk_3[label = "hash_key | key | data | <m>hlist_node | {<p>pprev | <n>next}"] fn_hk_1[label = "hash_key | key | data | <m>hlist_node | {<p>pprev | <n>next}"] fn_hk_2[label = "hash_key(new) | key | data | <m>hlist_node | {<p>pprev | <n>next}"] NULL_1[shape = plaintext, label = "NULL"] NULL_2[shape = plaintext, label = "NULL"] map:h -> ht:m1 ht:m2 -> f2_hk_1:m f2_hk_1:p -> ht:m2 f2_hk_1:n -> f2_hk_2:m f2_hk_2:p -> f2_hk_1:n f2_hk_2:n -> f2_hk_3:m f2_hk_3:p -> f2_hk_2:n f2_hk_3:n -> NULL_1 ht:mn -> fn_hk_2:m fn_hk_1:p -> fn_hk_2:n fn_hk_1:n -> NULL_2 } ``` 可以看出,有兩條 link 沒有正確串接,分別是 n->next 和 n->pprev,於是將其補上,使其變成下面的圖表: ```graphviz digraph G { rankdir = LR; node[shape = "record"] map[label = "map|bits|<h>ht"] ht[label = "Hash table|<m1>first_1|<m2>first_2|first_3|...|first_n-1|<mn>first_n"] f2_hk_1[label = "hash_key | key | data | <m>hlist_node | {<p>pprev | <n>next}"] f2_hk_2[label = "hash_key | key | data | <m>hlist_node | {<p>pprev | <n>next}"] f2_hk_3[label = "hash_key | key | data | <m>hlist_node | {<p>pprev | <n>next}"] fn_hk_1[label = "hash_key | key | data | <m>hlist_node | {<p>pprev | <n>next}"] fn_hk_2[label = "hash_key(new) | key | data | <m>hlist_node | {<p>pprev | <n>next}"] NULL_1[shape = plaintext, label = "NULL"] NULL_2[shape = plaintext, label = "NULL"] map:h -> ht:m1 ht:m2 -> f2_hk_1:m f2_hk_1:p -> ht:m2 f2_hk_1:n -> f2_hk_2:m f2_hk_2:p -> f2_hk_1:n f2_hk_2:n -> f2_hk_3:m f2_hk_3:p -> f2_hk_2:n f2_hk_3:n -> NULL_1 ht:mn -> fn_hk_2:m fn_hk_2:p -> ht:mn fn_hk_2:n -> fn_hk_1 fn_hk_1:p -> fn_hk_2:n fn_hk_1:n -> NULL_2 } ``` - 使 map 完整釋放空間 - 從 `map->ht[0]` 開始,先將此 slot 內的 hlist_node 逐一釋放,重複此動作,直到釋放完所有的 `map->ht[MAP_HASH_SIZE]` :::info 需要注意的是,這段 code 使用到 goto 技巧,用以跳過 16~20 行,但我目前還沒想到何時會有 unhash 的狀況發生 ::: ```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); } ``` - 找到二數相加後等於 sum 想法是不斷地將 nums 的數放入 map 中,直到 map_get 找到正確的數進行回傳 - 這裡也用到了 goto,目的是在 ret 無法被配置時,jump 至 bail 將配置完成的 map 釋放 ```c int *twoSum(int *nums, int numsSize, int target, int *returnSize) { map_t *map = map_init(10); *returnSize = 0; int *ret = malloc(sizeof(int) * 2); if (!ret) goto bail; for (int i = 0; i < numsSize; i++) { int *p = map_get(map, target - nums[i]); if (p) { /* found */ ret[0] = i, ret[1] = *p; *returnSize = 2; break; } p = malloc(sizeof(int)); *p = i; map_add(map, nums[i], p); } bail: map_deinit(map); return ret; } ``` :::spoiler TODO 研讀 Linux 核心原始程式碼 include/linux/hashtable.h 及對應的文件 How does the kernel implements Hashtables?,解釋 hash table 的設計和實作手法,並留意到 tools/include/linux/hash.h 的 GOLDEN_RATIO_PRIME,探討其實作考量 ::: ## 測驗 `2` - 目標是將重複的 node 移出 sorted list 觀察 `COND1`,此時需要進入 if 內做刪除重複 node 的動作,因此推斷 `COND1` 會需要 `head->val == head->next->val` 的條件,另外又需要考慮到若是 `head->next` 為 NULL 會產生問題,所以再將 `head->next` 加入判斷,於是得到結論 `COND1:` `head->next && head->val == head->next->val` 再觀察 `COND2` 會發現需要的條件和 `COND2` 相同,所以 `COND2:` `head->next && head->val == head->next->val` :::spoiler 完整程式碼 ```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; } ``` ::: ### 嘗試避免遞迴的版本 利用指標的指標 `**tmp` 移除重複的 node,方便移除 node ```c= struct ListNode* deleteDuplicates(struct ListNode* head) { if(!head){ return NULL; } int curr = -111; for(struct ListNode **tmp = &head; *tmp ;) { if((*tmp)->next && (*tmp)->val == (*tmp)->next->val) { *tmp = (*tmp)->next; curr = (*tmp)->val; } if(curr == (*tmp)->val) *tmp = (*tmp)->next; else tmp = &(*tmp)->next; } return head; } ``` ### 用 linux 核心 的 circular doubly-linked list 改寫 #### 遞迴版