---
tags: Linux Kernel
---
# 2022q1 Homework1 (quiz1)
contributed by < `kevinshieh0225` >
> [作業需求](https://hackmd.io/@sysprog/B166rc3Jq)
> [測驗題目](https://hackmd.io/@sysprog/linux2022-quiz1)
部分參考 `freshLiver` 的[筆記](https://hackmd.io/DkeAf1ziR4Ggm4-2G5G0EA?view)作法進行改寫。
## 測驗 1 Hash Map Implementation
Two Sum 是在 leetcode 上的經典首選,讓大家見識到 hash table 演算法的妙用。本測驗希望我們進入到 hash table 內部實作中。
### hash table 實作結構
hash table (HT)的性質如下:輸入特定鍵(key),HT 將回傳其對應的值(value)。基於一對一關係,理想上可使索取的時間複雜度為 O(1)。
在 HT 的實作如下:由於建立一個能含括所有 key 的 HT 是不實際的,故我們先為 key 做編碼,對應到我們建立的有限目錄中,隨後以 linked list (ll)的結構將鍵值串接起來(如同我們先將資料分門別類,隨後從分類中尋找我們要的是否存在)。
![](https://i.imgur.com/5FQZ6Lo.png)
(取自 [jserv](https://hackmd.io/@sysprog/linux2022-quiz1))
回到程式碼:map_t 建立 map ,其下會有 an array of hlist_head 。我們前面提到 key 會先編碼後置入目錄對應的 LL ,這裡即可看到我們會建立 $2^{bits}$ 個目錄與 LL 。
hash_key 與 hlist_node 即為 key/value 節點。在透過編碼後 hlist_node 被安插在對應目錄中,在尋鍵時透過尋訪 hlist_node 並以 container_of 取 hash_key 的值。
```c
struct hlist_head { struct hlist_node *first; };
typedef struct { int bits; struct hlist_head *ht; } map_t;
struct hlist_node { struct hlist_node *next, **pprev; };
struct hash_key {
int key;
void *data;
struct hlist_node node;
};
#define container_of(ptr, type, member) \
({ \
void *__mptr = (void *) (ptr); \
((type *) (__mptr - offsetof(type, member))); \
})
```
### hash table init
我們可以看到 map_t 初始化的過程,可以看到我們同時建立了一個 map_t 與(1 << bits == $2^{bits}$)個 hlist_head,若建立中有失敗則清除內容並返回 NULL。
```c
#define MAP_HASH_SIZE(bits) (1 << bits)
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;
}
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);
}
```
### hash encode
執行 hash 的演算法。最後會生成並回傳一個 $[0, 2^{bits}-1]$ 範圍內的無號整數。
```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);
}
```
### hash find, get
**find_key function**:在 HT 中尋找是否有對應 key 值,若有則回傳對應的 hasy_key * ,若無則回傳 NULL。
這裡首先我們先確認 key 被放在哪一個編碼的 bucket 中,透過 **hash(key, map->bits)** 這個函式取得編碼,並在 array 中取得對應的 hlist_head。接著就是逐一尋訪該 LL 去確認是否有 key 於當中。
```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;
}
```
**map_get function**:從 HT 中確認是否有對應鍵值,若有則回傳鍵值對應的 value,若無則返還 NULL。
這裡特別的是此 function 的回傳值是 void pointer。這是一種很有彈性的作法,我們可以用 void pointer 去指不同的變數,再把它透過轉型(casting)還原回來。由於我們在這個實作中不確定是否能有對應的回傳鍵值,所以使用這個的方式,若回傳 NULL 即可讓 if-else 更直覺的判斷與執行對應操作。
```c
void *map_get(map_t *map, int key)
{
struct hash_key *kn = find_key(map, key);
return kn ? kn->data : NULL;
}
```
### map_add 與考試答案
**map_add function**:確認是否有在 HT 中有對應鍵值,若有則不執行,無則新增鍵值在 HT 中。
```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 到 BBB 的 程式碼意思是要將新的 hlist_node *n 插入在 hlisthead *h 的 first 位置,其中如果 h->first 早就已經有 hlist_node 時,我就插入兩者之間。
:::info
AAA 應為 (c\) n->next = first
BBB 應為 (a) n->pprev = &h->first
:::
## 測驗 2 : [82. Remove Duplicates from Sorted List II](https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/)
本題透過遞迴的方式實現刪除重複節點的函式。其中:
:::info
COND1 = head.next != null && head.val == head.next.val
COND2 = head.next != null && head.val == head.next.val
:::
為何需要用 if 來包裝和裡面 while 相同的條件式呢?看似重複操作,但其實是因為條件內外執行操作是不同的行為,而必須在新增一個 if(COND1) 來隔離條件。
在 if(COND1) 內的操作是我們已經確定要移除當下重複的節點了,我們用 while 不斷將 head 重新指向其後,並在最後 return 遞迴中 head->next 的 ListNode 在這一番的操作下便可以把所有重複節點在這操作下完全移除。
相比於此,我們看 if(COND1) 外的操作:我們先對 head->next 後的節點進行 deleteDuplicates 的遞迴整理,最後回傳的還是 head node ,光是操作和回傳的形式都不同,而使他必須用 if(COND1) 來做內外切分。
### iterate version
```c
#include <stddef.h>
struct ListNode {
int val;
struct ListNode *next;
};
struct ListNode *deleteDuplicates(struct ListNode *head)
{
if (!head)
return NULL;
struct ListNode **ckp = &head;
struct ListNode *iter = (*ckp)->next;
while (iter) {
if ((*ckp)->val == iter->val) {
do {
iter = iter->next;
} while (iter && (*ckp)->val == iter->val);
*ckp = iter;
if (!iter)
break;
} else {
ckp = &((*ckp)->next);
}
iter = iter->next;
}
return head;
}
```
## 測驗 3 : [146. LRU Cache](https://leetcode.com/problems/lru-cache/)
在 Cache replacement policies 中,Least Recently Used 是一個策略之一。當 Cache 要更新時,將裡頭最後被使用的資料去除並更新。在 LRU 演算法我們面臨的問題是:如何在常數時間內達到兩件目的:如何確認 Cache hit、如何更新使用時間的紀錄。
### LRU Cache 實作原理:Hashmap + DoubleLinkedList
![](https://leetcode.com/problems/lru-cache/Figures/146/structure.png)
透過 Hashmap 可在常數時間裡「確認是否 Cache hit」;透過 DoubleLinkedList 讓我們在常數時間裡「更新 Cache 紀錄」。
這時可能會有一個問題是:如果要做 DoubleLinkedList 的尋訪,那應該也需要線性時間吧?然而我們如果搭配著 list.h 與 container_of 的技巧:在 Hashmap 與 DoubleLinkedList 上使用**共用**的 list_head 節點,那麼當我要維護 DoubleLinkedList 時,只要從 Hashmap 取得該節點更新;或是當我要刪除最近未使用的節點時,從 DoubleLinkedList 找到最末的節點。
LRUNode 是紀錄在 LRUCache 中的 DoubleLinkedList(DLL) 節點。
我們觀察 LRUCache,dhead 與 hheads[] 分別就是 DLL 的首節點,與第一題類似形式的 Hashmap 實作。
capacity 代表 Cache 的大小, count 代表目前總共有多少筆資料存放其中。
```c
typedef struct {
int capacity, count;
struct list_head dhead, hheads[];
} LRUCache;
typedef struct {
int key, value;
struct list_head hlink, dlink;
} LRUNode;
```
### LRU Cache create/free
在 lRUCacheCreate 中我們初始化紀錄使用時間 DLL 的 dhead,和紀錄是否存在其中的 Hashmap hhead[]。
在 lRUCacheFree 中我們也針對這些建立的動態記憶體位置依序釋放。
:::info
MM1 應填入 list_for_each_entry_safe ,在 DLL 中依序釋放節點,並不斷確認下一個節點是否為安全節點。
:::
```c
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);
}
```
### LRU Cache Get
輸入整數 key,如果其值有在 hashmap 中找到,更新 &obj->dhead 的 DLL結構,把該節點更新到開頭,代表最近使用,並回傳該 lru->value;若未找到則回傳 -1。
這裡可以看到這裡的編碼方式是 key 值除 capacity 得到 $[0, capacity-1]$ 的 hash ,相比前面的編碼來看是比較簡易的實作。
:::info
MMM2 應該是 list_for_each_entry。用該函數去尋訪 &obj->hheads[hash] 中的每個節點來確認是否該資料已存在於 hashmap 中。
:::
```c
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;
}
```
### LRU Cache Put
```c=
/**
* @brief 插入新的鍵值並更新 Cache
*
* @param *obj LRUCache
* @param key 鍵
* @param value 值
*/
void lRUCachePut(LRUCache *obj, int key, int value)
{
/**
* @ brief 執行 lRUCacheGet 行動:
*
* 確認該 key 是否已存在於 Cache 中
* 尋訪 LRUCache 的 HashTable
* 若存在則更新 DLL 與該節點對應的 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;
}
}
/**
* 確認此時的 Cache 存放容量(count)是否已經達到上限
* if 到達上限,準備更新最近未使用的節點資料
* else 未達到上限,建立新節點並更新 count 的大小
*/
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 節點進行更新
*/
lru->key = key;
list_add(&lru->dlink, &obj->dhead);
list_add(&lru->hlink, &obj->hheads[hash]);
lru->value = value;
}
```
:::info
MMM3 應填入 list_for_each_entry。
:::
:::danger
MMM4 應填寫 list_last_entry 取得 DLL 中最後一個節點,意即是最近未使用的節點。
答案卻是 list_for_each_entry?有點怪。
:::
## 測驗 4 : [128. Longest Consecutive Sequence](https://leetcode.com/problems/longest-consecutive-sequence/description/)
### 程式碼解析
```c
/**
* @brief LL 節點結構
*/
struct seq_node {
int num;
struct list_head link;
};
/**
* @brief 尋訪 HashTable 中是否存在該資料
*
* 對 @num 取絕對值並編碼出所屬欄位
* 尋訪該欄位確認是否存在該筆資料
*
* @param num 目標數值
* @param size HT 大小(欄位大小)
* @param *heads HT 的首節點
* @return if true struct seq_node*; else NULL;
*/
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
/**
* @brief 從一組整數陣列中,回傳最長的連續數組
*
* 1. 建立欄位數 = n_size 大小的 HashTable
* 2. 將整數陣列的數值插入進 HashTable 中
* 3. 尋訪最長連續數組
*
* @param *nums 整數陣列
* @param n_size 陣列之大小
*/
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]);
}
}
/**
* @brief 尋訪最長連續數組
*
* length : 搜尋截至目前最長的連續數組
* len : 本次搜索中的連續數組長度
*
* for 迭代過所有的陣列數值
*
* len 重置為 0
* 如果 node 存在於 HT 中,則開始尋找與其相連的連續數組
*
* while 向左 LLL = --left 搜尋連續數組
* 若有則增加 len 長度,並刪除 HT 上該節點
* while 向右 RRR = ++right 搜尋連續數組
* 若有則增加 len 長度,並刪除 HT 上該節點
*
* 比較 length 與 len 的長度進行更新
*
* 最後回傳 length
*/
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;
}
```
:::info
LLL = \-\-left
RRR = \+\+right
:::