# 2022q1 Homework1 (quiz1) contributed by < `freshLiver` > ###### tags: `linux2022` ## 第一題 - [LeetCode 1. Two Sum](https://leetcode.com/problems/two-sum/) ### Hash Table #### 建立雜湊表 ```c /** * @brief 建立並初始化一個有 2^bits 個欄位的雜湊表 * * @param bits * @return map_t* */ 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; } ``` #### 節點搜尋 ```c /** * @brief 從雜湊表中尋找並回傳一節點 kn,且 kn.key == @key * * 將 @key 經過雜湊函數計算後得到所屬欄位, * 接著依序走訪該欄位並檢查目標資料 @key 是否存在於該欄位中, * 若有找到則回傳包含該資料的節點,若不存在則回傳 NULL。 * * @param map 目標雜湊表 * @param key 目標節點的 key * @return struct hash_key* */ 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; } ``` #### 資料獲取 ```c /** * @brief 從雜湊表中提取 key 與 @key 相符之節點 kn 的資料 data * * @param map 目標雜湊表 * @param key 數列 nums 中的某數 k * @return void* */ void *map_get(map_t *map, int key) { struct hash_key *kn = find_key(map, key); return kn ? kn->data : NULL; } ``` #### 資料儲存 ```c /** * @brief 建立一節點 kn 來保存 @key 以及 @data * * @param map 目標雜湊表 * @param key 數列 nums 中的某數 k * @param data k 在數列 nums 中的 index */ void map_add(map_t *map, int key, void *data) { struct hash_key *kn = find_key(map, key); if (kn) return; // 所有節點的 key 不應重複 // 分配空間以及將資料複製到該節點 kn = malloc(sizeof(struct hash_key)); kn->key = key, kn->data = data; /** * @brief 將新增的節點插入到對應欄位的首位 * * 首先透過雜湊函式取得目標欄位的地址 h * 以及該欄位的首位節點 first * 最後再為 h->next, n, first 重新建立連結 * * - 新的節點 n 需要設定 pprev 以及 next * - n->next 指向原本的首位節點 first * - n->pprev 則指向欄位 h 指向首位節點的結構體地址 &h->first * * - 欄位 h 應指向新的首位節點 n * - 原首位節點 first 的 pprev 改指向新節點 n 的 next 的地址 &n->next * */ struct hlist_head *h = &map->ht[hash(key, map->bits)]; struct hlist_node *n = &kn->node, *first = h->first; n->next = first; if (first) first->pprev = &n->next; h->first = n; n->pprev = &h->first; } ``` :::info > 我自己的發現是 其實你仔細看 n->next = first > first->pprev = &n->next 等同於 first -> pprev = & first > n->pprev = &n > pprev 在這邊的物理意義等於指向自己位址的指標 > 所以下面的 if (!n->pprev) goto bail; > 意思是 如果指向自己的指標為 null (即自己為空) > 那麼就跳至最後面處理 > > 上述可以在 leetcode代換程式碼發現結果不變 , 但我也不能百分百肯定是不是這樣,因為覺得這樣 pprev 變得很奇怪? > > by修課同學 Haoyu 如果我的認知沒錯的話,indirect pointer 應該是用來儲存某個「指標的地址」,而這邊的 `**prev` 就應該是用來儲存「前一個節點的 `next` **指標的地址**」。 但這邊的 `first` 是一個指向下個節點的指標,即使在 `n->next = first` 後,也只是這兩個指標指向相同的地址(儲存的值都是下個節點的地址),若是對 `first` 取址的話應該會得到 `first` 這個區域變數的地址,所以 `&first == &n->next` 應該不會成立。 若在這個認知上去思考 `if (!n->pprev)` 成立的情境的話,應該是代表前一個節點的 `next` 的地址為 `NULL`,或是 `pprev` 被手動指向 `NULL`,但我想不通正常操作下什麼時候會發生這個情況。 而我在 LeetCode 測試時把那行 `if (...) goto ...` 移除後結果也是 Accepted。 > 我的理解是 `**pprev` 變數名稱看起來的確是應該用來儲存「前一個節點的 `next` 指標的地址」 > `first` 指向的是串列的首個節點 `*first = h->first` > `n->next = first` 就是把 原本串列的首個節點接在 n 這個節點的後面 > `if (first)` 表示串列的首個節點是否存在 , 存在的話 > 將 `first->pprev` 指向 (指向 `n->next` 的位址) > 而上面才剛修改過 `n-> next` 為 `first` > 所以 `first -> pprev` 會指向 指向 `first` 的位址 以上為根據程式碼判斷的結論 看起來是我們對於 `first` 的想法不太一樣? 如果我對程式理解有誤 麻煩在幫我指出哪裡有錯誤 首先,我看不太懂你的敘述內容且 `**` 造成 Markdown 格式問題,所以我對你的內文做了些調整。 再來是我實際寫了程式測試了在進行 `new->next = first` 後 `&first == &n->next` 是否會成立: ```c= #include <stdint.h> #include <stdio.h> #include <stdlib.h> #define print_ptr(p) \ ({ \ printf("%d:Pointer %s\n", __LINE__, #p); \ printf("\tAddress = %p\n", &p); \ printf("\t Value = %p\n\n", p); \ }) #define print_ind_ptr(p) \ ({ \ printf("%d:Indirect Pointer %s\n", __LINE__, #p); \ printf("\tAddress = %p\n", &p); \ printf("\t Value = %p\n", p); \ printf("\t Target = %p\n\n", *p); \ }) typedef struct NODE { struct NODE **pprev, *next; } hlist_node; typedef struct HEAD { hlist_node *first; } hlist_head; int main(int argc, char const *argv[]) { hlist_head *head = &(hlist_head){ .first = &(hlist_node){ .pprev = &head->first, .next = NULL, }, }; hlist_node *first = head->first; hlist_node *new = &(hlist_node){.pprev = NULL, .next = NULL}; print_ptr(head); print_ptr(head->first); print_ptr(first); print_ptr(new); new->next = first; print_ptr(first); print_ptr(new->next); if (first) { first->pprev = &new->next; print_ind_ptr(first->pprev); } head->first = new; new->pprev = &head->first; // end return 0; } ``` 測試結果: ```shell $ gcc indirect.c && ./a.out 41:Pointer head Address = 0x7ffd4e7ac880 Value = 0x7ffd4e7ac888 42:Pointer head->first Address = 0x7ffd4e7ac888 Value = 0x7ffd4e7ac8a0 43:Pointer first Address = 0x7ffd4e7ac890 Value = 0x7ffd4e7ac8a0 44:Pointer new Address = 0x7ffd4e7ac898 Value = 0x7ffd4e7ac8b0 47:Pointer first Address = 0x7ffd4e7ac890 Value = 0x7ffd4e7ac8a0 48:Pointer new->next Address = 0x7ffd4e7ac8b8 Value = 0x7ffd4e7ac8a0 51:Indirect Pointer first->pprev Address = 0x7ffd4e7ac8a0 Value = 0x7ffd4e7ac8b8 Target = 0x7ffd4e7ac8a0 ``` 可以看到在 46 行結束後,指標 `first` 指向的地址(變數 `first` 儲存的值)與 `new->next` 指向的地址(變數 `new->next` 的值)相同,但 `first` 的地址(`&first`)與 `new->next` 的地址(`&new->next`)明顯不同。 ::: #### 清除雜湊表 ```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); // n 保存目前節點的地址,p 則移至下一個節點 struct hlist_node *n = p; p = p->next; if (!n->pprev) /* unhashed */ goto bail; // 將當前節點 n 移出當前欄位 struct hlist_node *next = n->next, **pprev = n->pprev; *pprev = next; if (next) next->pprev = pprev; n->next = NULL, n->pprev = NULL; // 釋放節點 n 的資料以及節點本身佔用的資源 bail: free(kn->data); free(kn); } } free(map); } ``` :::warning TODO : `if (!n->pprev) goto bail;` 用途 ::: ### 使用雜湊表完成 Two Sum ```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; // 無法分配空間給答案 /** * @brief 利用雜湊表紀錄已看過得數值以及它們的 index * * 由於目標是要找到 num 與 k 使得 num + k == target * 因此檢查到數列 nums 中的數值 num 時 * 就檢查雜湊表中是否有 key 為 k 的數值存在 * * Case 1. k 不存在 * * 將 num 作為 key、index 作為 data 新增至雜湊表 * 若之後遇到另一數 k 時就會因 num 存在而找到解 * * Case 2. k 存在 * * 代表 k 有在數列 nums 中 * 因此 k 的節點的資料 *p(代表 k 所在的 index)取出 * 接著再將 *p 以及當前 index i 作為答案回傳 * */ for (int i = 0; i < numsSize; i++) { // 若雜湊表中有 key 為 k 的節點存在,則 p 不為 NULL int *p = map_get(map, target - nums[i]); // k 在雜湊表中 if (p) { ret[0] = i, ret[1] = *p; *returnSize = 2; break; } // k 不在雜湊表中,將 num (nums[i]) 以及其 index i 加入雜湊表 p = malloc(sizeof(int)); *p = i; map_add(map, nums[i], p); } bail: map_deinit(map); return ret; } ``` ### Linux 核心 Hash Table 的設計和實作手法 #### `hlist_head` 以及 `hlist_node` 在 [include/linux/types.h](https://github.com/torvalds/linux/blob/master/include/linux/types.h) 中有定義 hlist_head 以及 hlist_node 兩個結構體: ```c struct hlist_head { struct hlist_node *first; }; struct hlist_node { struct hlist_node *next, **pprev; }; ``` 與 Linux Kernel Linked List API 相比: - 相似處 - 結構體內都只有定義指向串列節點的指標,應該與 `list_head` 的設計相似,是為了能夠方便擴充。 - 都是用雙向鏈結串列在節點間移動。 - 不同處 - 雜湊表多了 `hlist_head` 作為欄位,而每個欄位都透過 `first` 指向各自的鏈結串列,是透過 Chaining 的方式來處理 Collision。 - 前述的 Chaining List 是**非環狀**的雙向鏈結串列,且 `pprev` 並不是單純指向前一個節點。(使用 indirect pointer 的設計可以參考 [第二週的討論](https://hackmd.io/@sysprog/B1RLIlY19)) - 雜湊表的欄位數 (`hlist_head` 數量) 在宣告時就決定了(宣告成陣列的形式)。 #### Hash Table 相關實作 `hlist_head` 以及 `hlist_node` 相關基本操作的函式以及巨集則是定義在 [include/linux/hashtable.h](https://github.com/torvalds/linux/blob/master/include/linux/hashtable.h) 以及 [include/linux/list.h](https://github.com/torvalds/linux/blob/master/include/linux/list.h) 中。 - `DECLARE_HASHTABLE(name, bits)` - 宣告一個有 $2^{bits}$ 個欄位的雜湊表 - `hash_init(table)` - 透過 [Designated Initializers](https://gcc.gnu.org/onlinedocs/gcc-4.1.2/gcc/Designated-Inits.html) 對所有欄位進行初始化。 - `hash_add_head(head, node, key)` - 新增資料到對應的欄位中 - 也有 `add_before`, `add_behind` 等變化。 - 比較特別的是 `hlist_add_fake` 這個函式,不會新增 `node` 到雜湊表中,只會將 `node->pprev` 指向 `&node->next`。整個 linux 專案[包含定義只出現四次](https://github.com/torvalds/linux/search?q=hlist_add_fake),不確定實際用途但似乎[與檔案系統有關](https://github.com/torvalds/linux/blob/9f7fb8de5d9bac17b6392a14af40baf555d9129b/include/linux/fs.h#L744)。 - `hash_del(node)` - 將指定節點從它所在的 Chaining List 中移除。 - 若要 unhash(將 `pprev` 以及 `next` 指向 `NULL`)這個 `node`,則需要使用 `hlist_del_init`。 - `hlist_unhashed(node)` - 檢查 `node` 是否已被 unhashed。 - `hlist_move_list(new_head, old_head)` - 用 `old_head` 的 Chaining List 取代 `new_head` 的 Chaining List,並移除 `old_head` 的 `first`。 - `hlist_for_each` 系列巨集 - 與 `list_for_each` 系列相似,用來走訪 Chaining List,也有 entry 以及 safe 版。 - `hlist_for_each_entry_from` - 從 Chaining List 的指定節點開始走訪剩下的節點,而不是從 `head->first` 開始。 - `hlist_for_each_entry_continue` - 與 `hlist_for_each_entry_from` 相似,但是從指定節點的**下一個節點**開始繼續走訪。 :::warning TODO 1. [RCU 是什麼](https://lwn.net/Articles/262464/) 2. `GOLDEN_RATIO_PRIME` ::: --- ## 第二題 - [LeetCode 82. Remove Duplicates from Sorted List II](https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/) 這題的目標是將一個已排序過的 Singly-linked List 中重複的節點移除,需要注意的部份是: - 串列可能為空或 `NULL` - 所有重複出現的節點皆要移除,而不是只留下一個使其不重複出現 ### 遞迴版 ```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; } ``` 在這個程式碼中,可以發現被 `if` 分成了三個部份,且這三個部份的的最後皆以 `return` 結束,分別代表了以下三種情況: 1. 輸入的 `head` 為 `NULL` 時 2. 當 1 不成立且 `COND1` 成立時 3. 非上述兩種情況時 第一個部份是為了處理原串列為 `NULL` 以及遞迴呼叫到串列尾端時的遞迴終止條件。 第二個部份則是為了處理 `head` 與 `head->next` 兩節點資料相同的情況,所以 `COND1` 應該是 `head->val == head->next->val`,但這邊有個陷阱是 `head->next->val` 若是在 `head->next == NULL` 時會出問題,所以必須先檢查 `head->next != NULL`,因此正確的 `COND1` 應為: ```c head->next && head->val == head->next->val ``` 而進入第二部份的 `if` 之後則是透過 `while` 迴圈跳過所有**連續**重複資料的節點,所以當目前指向的節點 `head` 的資料與其下一個節點 `head->next` 的資料相同時就要跳過目前節點,因此 `COND2` 應該是 `head->val == head->next->val`。同樣的,這邊也有個陷阱是當串列走到尾端時, `head->next` 會是 `NULL`,因此這邊也必須先檢查 `head->next != NULL`,所以正確的 `COND2` 應和 `COND1` 相同,皆為: ```c head->next && head->val == head->next->val ``` 而在 `while` 迴圈結束後(即 `head->next == NULL || head->val != head->next->val` ),就需要再次透過遞迴呼叫 `deleteDuplicates` 來找到剩餘串列中部重複的節點,但由於 `head` 也是重複的節點,所以傳入 `deleteDuplicates` 的參數應為 `head->next` 才對。 而最後的第三部份則因為 `COND1` 確保了 `head` 的資料不與其他節點重複,所以只需要透過 `deleteDuplicates` 找出 `head->next` 要接到哪個節點,最後再回傳 `head` 即可。 ### 迭代版 ```c= struct ListNode* deleteDuplicates(struct ListNode* head){ if (!head) return NULL; bool found = false; for (struct ListNode **ptr = &head; *ptr;) { // same val if ((*ptr)->next && (*ptr)->val == (*ptr)->next->val) { *ptr = (*ptr)->next; found = true; } else { // don't forget to rm last repeated node if (found) *ptr = (*ptr)->next; else ptr = &(*ptr)->next; found = false; } } return head; } ``` 這個迭代的實作參考了[你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list)中提到的**有品味的**移除節點的操作,使用了 `ListNode` 的指標的指標來避免額外判斷當前節點是否為 `head`。 #### 增加一個 `bool` 變數以簡化程式碼 ```c struct ListNode* deleteDuplicates(struct ListNode* head){ if (!head) return NULL; bool prev = false, found = false; for (struct ListNode **ptr = &head; *ptr; prev = found) { found = (*ptr)->next && (*ptr)->val == (*ptr)->next->val; if (found || prev) *ptr = (*ptr)->next; else ptr = &(*ptr)->next; } return head; } ``` 透過分別使用 `prev` 以及 `found` 來紀錄上一個節點以及目前節點的比較結果,這能讓原本程式碼中第 16 行的 `if` 與第 10 行的 `if` 合併。 ### Circular Doubly-linked List 版 與 lab0-c 中要實作的 function `q_delete_dup` 相似,但可以不用刪除節點資料,且儲存的資料型態為 `int` 而不是 `char *`。 #### 迭代版 ##### 主要函式 ```c bool q_delete_dup(struct list_head *head) { if (!head) return false; LIST_HEAD(duplist); bool prev = false; element_t *ptr = list_entry(head->next, element_t, list), *next = ptr; for (bool same; next->list.next != head; ptr = next) { next = list_entry(ptr->list.next, element_t, list); same = ptr->value == next->value; if (same || prev) list_move(&ptr->list, &duplist); prev = same; } // don't forget last node if (prev) list_move(&ptr->list, &duplist); return true; } ``` ##### 轉成雙向鏈結 ```c= typedef struct ListNode Node; struct ListNode *deleteDuplicates(struct ListNode *head) { if (!head) return NULL; // build doubly-linked list struct list_head doubly; INIT_LIST_HEAD(&doubly); for (Node *ptr = head; ptr; ptr = ptr->next) { element_t *e = malloc(sizeof(element_t)); e->value = ptr->val; e->list.next = NULL; list_add_tail(&e->list, &doubly); } q_delete_dup(&doubly); // back to singly Node *new = NULL, **pnew = &new; element_t *ptr; list_for_each_entry (ptr, &doubly, list) { Node *n = malloc(sizeof(Node)); n->val = ptr->value; n->next = NULL; *pnew = n; pnew = &(*pnew)->next; } return new; } ``` :::danger 如果第 23 行的 `*new` 不初始化成 `NULL` 的話,可能在 `doubly` 的元素皆被移除時(例如輸入串列為 `[1,1]` 時)跳出以下錯誤: > Line 70: Char 15: runtime error: member access within misaligned address 0x000041b58ab3 for type 'struct ListNode', which requires 8 byte alignment [ListNode.c] 0x000041b58ab3: note: pointer points here \<memory cannot be printed\> ::: #### 遞迴版 ##### 主要函式 ```c typedef struct ListNode Node; #define assign_and_cmp(ptr, next, p_ptr_entry, p_next_entry) \ ({ \ p_ptr_entry = list_entry(ptr, element_t, list); \ p_next_entry = list_entry(next, element_t, list); \ p_ptr_entry->value == p_next_entry->value; \ }) struct list_head *q_delete_dup(struct list_head *head) { if (!head) return NULL; element_t *ptr, *next; if (head->next && assign_and_cmp(head, head->next, ptr, next)) { while (head->next && assign_and_cmp(head, head->next, ptr, next)) head = head->next; return q_delete_dup(head->next); } head->next = q_delete_dup(head->next); if (head->next) head->next->prev = head; return head; } ``` 把 `list_head` 當作 Singly-linked List 操作,並透過定義巨集讓函式能直接沿用 Singly-linked List 的遞迴函式的結構,只需要額外宣告變數來存取 `element_t` 的資訊,以及要額外處理 `prev` 連結。 ##### 轉成雙向鏈結 ```c struct ListNode *deleteDuplicates(struct ListNode *head) { if (!head) return NULL; // build doubly-linked list struct list_head doubly; INIT_LIST_HEAD(&doubly); for (Node *ptr = head; ptr; ptr = ptr->next) { element_t *e = malloc(sizeof(element_t)); e->value = ptr->val; e->list.next = NULL; list_add_tail(&e->list, &doubly); } // break circular doubly.prev->next = NULL; // head do not contain data, start from first node doubly.next = q_delete_dup(doubly.next); if (doubly.next) doubly.next->prev = &doubly; // make circular struct list_head *ptr; for (ptr = &doubly; ptr->next; ptr = ptr->next) ; ptr->next = &doubly; doubly.prev = ptr; // back to singly Node *new = NULL, **pnew = &new; list_for_each (ptr, &doubly) { element_t *entry = list_entry(ptr, element_t, list); Node *n = malloc(sizeof(Node)); n->val = entry->value; n->next = NULL; *pnew = n; pnew = &(*pnew)->next; } return new; } ``` 大致上與迭代版本的相同,但差在由於這邊的遞迴版是沿用單向的遞迴的函式,因此在檢查條件時也是沿用 `!head` 以及 `head->next` ,所以這邊要先把最後一個節點的 `next` 指向 `NULL`,讓串列變成**非環狀**串列,並以首位節點作為開頭進行 `q_delete_dup`,然後在結束時還需要讓串列變回環狀。 #### 測試函式 由於 `deleteDuplicates` 是有被規定的,因此不論是遞迴還是迭代版都可以用相同函式測試 `deleteDuplicates`。 ```c int main(int argc, char const *argv[]) { Node *head, **phead = &head; int vals[] = {1, 1}; for (int i = 0; i < (sizeof(vals) / sizeof(vals[0])); ++i) { Node *n = malloc(sizeof(Node)); n->val = vals[i]; n->next = NULL; *phead = n; phead = &(*phead)->next; } deleteDuplicates(head); return 0; } ``` --- ## 第三題 - [LeetCode 146. LRU Cache](https://leetcode.com/problems/lru-cache/) ### 說明 #### 建立 LRU 快取 ```c LRUCache *lRUCacheCreate(int capacity) { LRUCache *cache; // 利用 GNU C Extension 建立可變大小的 LRUCache 結構體物件 // REF : https://gcc.gnu.org/onlinedocs/gcc/Zero-Length.html cache = malloc(sizeof(LRUCache) + capacity * sizeof(struct list_head)); cache->capacity = capacity; cache->count = 0; // 初始化 dhead 以及雜湊表 INIT_LIST_HEAD(&cache->dhead); for (int i = 0; i < capacity; ++i) INIT_LIST_HEAD(&cache->hheads[i]); return cache; } ``` #### 新增資料 (==MMM3==, ==MMM4==) ```c /** * @brief 新增一資料 (key,value) 到快取中 * * TRICK : * 新增的資料並非放到原 LRU 節點所在的欄位 * 實際上與雜湊表概念相同 * 是將資料放在第 HASH(key) 個欄位 * 而實際的 Cache 存取紀錄則是由 LRUCache 的 dhead 管理 * 透過使用雜湊表 hhead 搭配「額外的」環狀雙向鏈結串列 dhead * * - 新增資料: * - 加入(資料不存在,Cache 未滿): * 1. 計算 HASH(key) 並將資料加入雜湊表 * 2. 將該節點加入到 dhead 串列 * * - 取代(資料不存在,Cache 已滿): * 1. 將 LRU 從 hhead 中移除 * 2. 將 LRU 從 dhead 中移除 * 3. 計算 HASH(key) 並將新資料加到雜湊表 * 4. 將新資料的節點加到 dhead 串列 * * - 更新(資料存在): * 1. 從雜湊表中尋找該資料的節點 * 2. 將該節點提昇至 dhead 首位(移除再加入) * * - 取得資料: * 1. 從雜湊表中尋找該資料的節點 * 2. 若有找到該資料則將該節點提昇至 dhead 首位 * 3. 回傳節點 * * @param cache LRUCache 物件的地址 * @param key 欲新增資料的 key * @param value 欲新增資料的 value */ void lRUCachePut(LRUCache *cache, int key, int value) { int hash = HASH(key, cache); LRUNode *node, *next; // MMM3, 只是 move 不需要使用到 safe list_for_each_entry (node, &cache->hheads[hash], hlink) { if (node->key == key) { // 更新該 key 的 value node->value = value; // 提昇至 dhead 首位 list_move(&node->dlink, &cache->dhead); return; } } // 未滿,直接新增節點 if (cache->count < cache->capacity) { node = malloc(sizeof(LRUNode)); ++cache->count; } // 已滿,移除 dhead 中的 LRU else { // MMM4 node = list_last_entry(&cache->dhead, LRUNode, dlink); list_del(&node->dlink); list_del(&node->hlink); } // 分別將資料加入到雜湊表以及 dhead node->key = key; node->value = value; list_add(&node->dlink, &cache->dhead); list_add(&node->hlink, &cache->hheads[hash]); } ``` :::danger **MMM4** 的部份可能是答案錯了,能夠賦值的巨集應該是 `list_entry`, `list_first_entry` 或是 `list_last_entry`,而這邊因為是要獲取 LRU 所以要找的節點應該是 `dhead->prev`,所以答案應為 `list_last_entry` 才正確。 ![](https://i.imgur.com/oRbYCQz.png) ::: #### 取得資料 (==MMM2==) ```c /** * @brief 從 LRU Cache 取得資料 * * 透過 HASH(key) 計算出雜湊表的欄位索引 * 接著到該欄位尋找目標的 key * 而不是走訪 dhead 中所有節點尋找 key * * @param cache * @param key * @return int */ int lRUCacheGet(LRUCache *cache, int key) { LRUNode *node; // MMM2 list_for_each_entry (node, &cache->hheads[HASH(key, cache)], hlink) if (node->key == key) { list_move(&node->dlink, &cache->dhead); return node->value; } return -1; } ``` #### 清除快取 (==MMM1==) ```c /** * @brief 清除 LRU Cache * * 雖然快取紀錄是由 dhead 管理 * 但 LRUNode 同時包含 dlink 以及 hlink * 分別用來指向 dhead 及 hhead 的前後節點 * 因此實際上 dhead 中的節點與 hhead 中的節點相同 * 只須刪除一邊的節點即可 * * @param cache */ void lRUCacheFree(LRUCache *cache) { LRUNode *node, *next; // MMM 1 list_for_each_entry_safe (node, next, &cache->dhead, dlink) { list_del(&node->dlink); free(node); } free(cache); } ``` #### 測試程式 ```c static inline void GET_AND_SHOW(LRUCache *cache, int key) { int value = lRUCacheGet(cache, key); printf("GET (%d,%d)\n", key, value); } int main(int argc, char const *argv[]) { LRUCache *cache = lRUCacheCreate(2); lRUCachePut(cache, 1, 1); lRUCachePut(cache, 1, 1); GET_AND_SHOW(cache, 1); lRUCachePut(cache, 3, 3); GET_AND_SHOW(cache, 2); lRUCachePut(cache, 4, 4); GET_AND_SHOW(cache, 1); GET_AND_SHOW(cache, 3); GET_AND_SHOW(cache, 4); lRUCacheFree(cache); return 0; } ``` ### 可改進部份 :::warning TODO ::: ### 探討 Linux 核心中 LRU 相關程式碼 --- ## 第四題 - [LeetCode 128. Longest Consecutive Sequence](https://leetcode.com/problems/longest-consecutive-sequence/description/) ### 說明 #### 尋找雜湊表中是否包含某數 ```c /** * @brief 找出雜湊表中包含某個數值的節點 * * 會先對 @num 取絕對值並計算出該數所屬的欄位 * 接著在走訪該欄位、檢查是否包含此數 * * @param num 目標數值 * @param size 雜湊表大小(有幾個欄位) * @param heads 雜湊表 * @return struct seq_node* */ 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; } ``` #### 尋找最長連續數列長度 ```c= int longestConsecutive(int *nums, int n_size) { // 建立並初始化雜湊表 struct list_head *heads = malloc(n_size * sizeof(*heads)); for (int i = 0; i < n_size; i++) INIT_LIST_HEAD(&heads[i]); // 將每個數字放入雜湊表 struct seq_node *node; for (int i = 0, hash; i < n_size; i++) { // 跳過重複數字 if (!find(nums[i], n_size, heads)) { // 使用 modulo 作為雜湊函數,將數字取絕對值計算後放入雜湊表 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]); } } int global = 0; for (int i = 0, num; i < n_size; i++) { /** * @brief 尋找最長的連續數列 (global 代表目前找到最長的數列長度) * * Step 1. 檢查當前數字是否在雜湊表中 * * Y : 跳至 Step 2 * N : 跳至 Step 4 * * Step 2. 開始尋找包含此數(num)的連續數列 (local 代表此連續數列長度) * * 2-1 : 將此數移出雜湊表並將 local + 1 * * 2-2 : 從 num 開始向下尋找最多有幾個相鄰整數 (k>0) * * 2-2-1 : 尋找雜湊表中是否包含 num-k * Y : ++local, ++k, 將 num-k 從雜湊表中移除 * N : 結束此迴圈 * * 2-3 : 類似 2-2,但是從 num+1 往上找 * * Step 3. 如果 local 較大的話就更新 global * * Step 4. 如果尚未檢查到最後一個數字的話就回到 Step 1 * * TRICK : * 由於雜湊表不包含重複數字且在 Step 2 找連續數字時會將找到的數字移除 * 所以即使 nums 中包含連續或重複數字也只有第一次遇到時會進入第二個迴圈 * 之後遇到該群連續數列時就只需要檢查雜湊表是否包含該數而已 * 因此運作起來會像是在計算每群連續數列的長度 */ int local = 0; if (node = find(nums[i], n_size, heads)) { local++; num = node->num; list_del(&node->link); // 從 num-1 向下,因 num 已取出且每次找到應將 left 遞減,故 --left for (int left = num; (node = find(--left, n_size, heads));) { local++; list_del(&node->link); } // 從 num+1 向上,因 num 已取出且每次找到應將 right 遞增,故 ++right for (int right = num; (node = find(++right, n_size, heads));) { local++; list_del(&node->link); } global = local > global ? local : global; } } return global; } ``` #### 測試程式 :::warning TODO ::: ### 可改進部份 #### `while(node)` 可改用 `if` 第 56 列的 `if` 原本是 `while(node)`,但是在第 68 列的迴圈結束條件為 `node` 被賦予 `NULL` 時,因此實際上 `while` 只會執行一次,可用 `if` 替代。 #### `while` 迴圈可改用 `for` 迴圈使程式碼更精簡 `left` 與 `right` 只會分別在各自的 `while` 迴圈中使用到,因此可利用 `for` 迴圈進行宣告,使得程式碼更加簡短並縮短 `left` 以及 `right` 的生命週期。 #### `len` 以及 `length` 兩變數名稱有相近含意 原先的 `len` 是用來表示 `Step 2` 中找到的連續數列的長度,而 `length` 則是代表目前找到的最長連續數列的長度,兩個變數名稱相近可能會造成混淆,因此將 `length` 以及 `len` 分別以 `global` 以及 `local` 進行替換。 ### Linux 核心風格的 hash table 重新實作 :::warning TODO :::