# 2022q1 Homework1 (quiz1) contributed by <`arthurchang09`> ## Q1 ### 解題 ```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; } ``` 1. AAA 是 `n->next = first` 。 由下方的 `first->pprev = &n->next` 以及 `h->first = n` 可判斷是要將新的節點插入開頭,因此 AAA 的 `next` 指向原本第一個節點。 2. BBB 是 n->pprev = &h->first 。由 1. 可知次將新節點插入開頭,由於 `hlist` 是雙向的,要將節點之 `pprev` 指回前一個節點,在此便是指回開頭。又 `pprev` 是指標的指標,因此是指向 `h->first` 之位址, ### 解釋上述程式碼運作原理 * map_init() 初始化整個 hash table之 `map_t` 及各個 `hlist_head` ,並將每個 `hlist_head` 初始化為 `null` 。 ```graphviz digraph G{ rankdir=LR; node[shape=record]; m [label = "map|<h0>ht[0].first|<h1>ht[1].first|<h2>ht[2].first|<other>..."]; null; m:h0:e->null m:h1:e->null m:other:e->null } ``` * find_key() 查詢某個數值是否在 hash table 中並回傳位址。透過指定的 hash function 找到 `map` 中對應的 `hlist_head` ,接著走訪整個 linked list ,使用 `container_of` 巨集,找到整個 `hash_key` 的位址並取出值比較,若相符則回傳此位址。若無則回傳 `null` 。 * map_get() 呼叫 `find_key` 的 function 。透過 `find_key` 找到欲找的結點並回傳其值之指標,或者回傳 `null` 。 * map_add() 新增 `hash_key` 節點。先呼叫 `find_key()` 查詢節點是否存在。若否,則在其對應的 `hlist_head` 的開頭新增節點。 * map_deinit() 透過逐一走訪 `map` 中所有的 `hlist_head` 釋放整個 `hash table`,使用 `container_of` 找到對應 `hash_key` 的位址後釋放記憶體空間。最後釋放 `map` 的空間。 ## Q2 針對 [LeetCode 82. Remove Duplicates from Sorted List II](https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/),以下是可能的合法 C 程式實作: ```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; } ``` 此題之目標是要刪除值重複的節點, COND1 答案必然包含檢查當前節點的值是否和下一個節點的值相等,即 `head->value == head->next->value` 。然而,下一個節點可能為 `NULL` ,代表 linked list 已經走到結尾,直接 `head->next->value` 可能導致錯誤,因此要先判斷 `head->next` 是否為 `NULL` 。因此 COND1 為 `head->next && head->value == head->next-value` 。 while 迴圈的目的是要跳過所有值重複的節點,因此其條件和 COND1 相同,為 `head->next && head->value == head->next-value` 。 ### 嘗試避免遞迴,寫出同樣作用的程式碼 ```c struct ListNode { int val; struct ListNode *next; }; struct ListNode *deleteDuplicates(struct ListNode *head) { if (!head) return NULL; struct ListNode *node = NULL, *prev = NULL; int same_val = 0; for (node = head; node;) { if (node->next && node->val == node->next->val) { if (!prev) { head = node->next; same_val = 1; prev = NULL; } else { prev->next = node->next; same_val = 1; prev = node; } } else if (same_val) { same_val = 0; if (!prev) { head = node->next; prev = NULL; } else { prev->next = node->next; prev = node; } } else { prev = node; } node = node->next; } return head; } ``` ## Q3 針對 LeetCode 146. LRU Cache,以下是 [Least Recently Used (LRU)](https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU)) 可能的合法 C 程式實作: ```c #include <stdio.h> #include <stdlib.h> #include "list.h" typedef struct { int capacity, count; struct list_head dhead, hheads[]; } LRUCache; typedef struct { int key, value; struct list_head hlink, dlink; } LRUNode; LRUCache *lRUCacheCreate(int capacity) { LRUCache *obj = malloc(sizeof(*obj) + capacity * sizeof(struct list_head)); obj->count = 0; obj->capacity = capacity; INIT_LIST_HEAD(&obj->dhead); for (int i = 0; i < capacity; i++) INIT_LIST_HEAD(&obj->hheads[i]); return obj; } void lRUCacheFree(LRUCache *obj) { LRUNode *lru, *n; MMM1 (lru, n, &obj->dhead, dlink) { list_del(&lru->dlink); free(lru); } free(obj); } int lRUCacheGet(LRUCache *obj, int key) { LRUNode *lru; int hash = key % obj->capacity; MMM2 (lru, &obj->hheads[hash], hlink) { if (lru->key == key) { list_move(&lru->dlink, &obj->dhead); return lru->value; } } return -1; } void lRUCachePut(LRUCache *obj, int key, int value) { LRUNode *lru; int hash = key % obj->capacity; MMM3 (lru, &obj->hheads[hash], hlink) { if (lru->key == key) { list_move(&lru->dlink, &obj->dhead); lru->value = value; return; } } if (obj->count == obj->capacity) { lru = MMM4(&obj->dhead, LRUNode, dlink); list_del(&lru->dlink); list_del(&lru->hlink); } else { lru = malloc(sizeof(LRUNode)); obj->count++; } lru->key = key; list_add(&lru->dlink, &obj->dhead); list_add(&lru->hlink, &obj->hheads[hash]); lru->value = value; } ``` MMM1 可看出來是包含四個參數,其目的是走訪整個 linked list ,並釋放每個 node 的記憶體空間,因此需要 list_entry 這個巨集。又因為要醜訪並刪除、釋放記憶體空間,需要額外之指標,因此 MMM1 為 `list_for_each_entry_safe` 。 MMM2 和 MMM3 皆是走放整個 linked list 且無刪除動作,另外要透過 `hlink` 找到所在節點的 `LRUNode` 位址,需要使用 `list_entry` ,因此 MMM2 、 MMM3 皆為 `list_for_each_entry` 。 ## Q4 針對 LeetCode 128. Longest Consecutive Sequence,以下是可能的合法 C 程式實作: ```C= #include <stdio.h> #include <stdlib.h> #include "list.h" struct seq_node { int num; struct list_head link; }; static struct seq_node *find(int num, int size, struct list_head *heads) { struct seq_node *node; int hash = num < 0 ? -num % size : num % size; list_for_each_entry (node, &heads[hash], link) { if (node->num == num) return node; } return NULL; } int longestConsecutive(int *nums, int n_size) { int hash, length = 0; struct seq_node *node; struct list_head *heads = malloc(n_size * sizeof(*heads)); for (int i = 0; i < n_size; i++) INIT_LIST_HEAD(&heads[i]); 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]); } } 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; 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; } } return length; } ``` 在第三個 `for` 迴圈中, left 和 right 分別以 num 為中心,向左或向右搜尋 hash table 中搜尋連續的數字並移除掉該節點,並更新最長長度,直到找不到連續的數值之節點,再移到下一個陣列中元素進行搜尋。注意到在 46 行中已經先刪除了一個節點 `num` ,且接下來兩個 `while` 迴圈會逐步刪除數字是連續之節點,即 `num - 1` 、 `num - 2` ... 和 `num + 1` 、 `num + 2` ... ,因此 `left` 和 `right` 要先進行加或減。 LLL 為 `--left` , RRR 為 `++right` 。