---
tags: Linux, linux2022
---
2022q1 Homework1 (quiz1)
===
contributed by < `NOVBobLee` >
> [測驗題目](https://hackmd.io/@sysprog/linux2022-quiz1)
## 測驗 1 [LeetCode - Two Sum](https://leetcode.com/problems/two-sum/)
Two sum 若以暴力法,則時間複雜度為 $O(n^2)$ 。若改以使用湊雜表,可降低時間複雜度到 $O(n)$ 以下。以下為使用 Linux 湊雜表結構的湊雜表實作,拆開解釋,最後利用他來完成 Two sum 。
### 湊雜表結構宣告
Linux 的湊雜表使用兩種結構,一為 `hlist_head` ,為湊雜表 bucket 的開頭,是單向鍊結結構,二為 `hlist_node` ,為 bucket 裡的元素連結,是雙向鍊結結構。為何不使用通用的 `list_head` ,其原因在湊雜表為減少碰撞發生,會把 bucket 的數量增大,且不少 bucket 是空的,若使用雙向鍊結結構,一個開頭就需要兩個指標的空間。第二是如果碰撞變少,那麼 bucket 裡的元素數量也不多,不需要像環狀雙向鍊結結構可以從尾部或從頭部兩種方向走訪。
為使 `hlist_node` 的插入刪除方便,其使用雙向鍊結結構。但是 bucket 的開頭與元素連結卻是使用不同的結構,為使雙練鍊結結構與單向鍊結結構結合,且插入刪除不會因為前一個是開頭而需要新的分支, `hlist_node` 改使用的 `pprev` ,指標的指標,非常漂亮。
```graphviz
digraph a {
rankdir=LR;
node[fontname="Helvetica";shape=record;];
edge[style=dashed];
map [
label="bucket 0|bucket 1|bucket 2|<buc>bucket 3|...|bucket n";
xlabel="map_t";
];
hn1 [
label="<n>next|<pp>pprev";
xlabel="hlist_node";
];
hn2 [
label="<n>next|<pp>pprev";
xlabel="hlist_node";
];
end [
label="NULL";
shape=plaintext;
]
map:buc -> hn1;
hn1:n -> hn2;
hn1:pp -> map:buc;
hn2:pp -> hn1:n;
hn2:n -> end;
}
```
而湊雜表本體為 `map_t` ,其包含多個 buckets ( `ht` ),而其數量會使用巨集 `MAP_HASH_SIZE` 來決定,固定為 $2$ 的冪。為何存 `bits` 而非實際數量?是因為湊砸函數 `hash` 頻繁使用到,而實際數量只有在建立和釋放湊雜表的時候用到( 2 次)而已。
```c
struct hlist_node { struct hlist_node *next, **pprev; };
struct hlist_head { struct hlist_node *first; };
typedef struct { int bits; struct hlist_head *ht; } map_t;
#define MAP_HASH_SIZE(bits) (1 << bits)
```
### 建立空湊雜表
輸入為 `bits` ,代入剛提到的巨集 `MAP_HASH_SIZE` 會得到湊雜表 bucket 要配置的數量。
```c
map_t *map_init(int bits) {
/* 湊雜表空間配置 */
map_t *map = malloc(sizeof(map_t));
if (!map)
return NULL;
map->bits = bits;
/* buckets 開頭空間配置 */
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++)
/* bucket 開頭初始化 */
(map->ht)[i].first = NULL;
} else {
free(map);
map = NULL;
}
return map;
}
```
### 湊雜表元素與相關巨集
湊雜表元素 `hash_key` ,包含一個 `key` ,拿來定位 bucket 的位置,一個 `data` ,存放資料的地址,和 `node` ,與湊雜表連結用。
```c
struct hash_key {
int key;
void *data;
struct hlist_node node;
};
```
如果我們從 bucket 的開頭出發,得到的會是 `hlist_node` ,這樣要怎麼找到包裝他的容器 `hash_key` ?其方法是使用下面的巨集 `container_of` ,在得知 `hlist_node` 在 `hash_key` 裡的位移量後,可以反推得到容器的開頭位置。
```c
#define container_of(ptr, type, member) \
({ \
void *__mptr = (void *) (ptr); \
((type *) (__mptr - offsetof(type, member))); \
})
```
湊雜函數則是使用以下的函式 `hash` ,用來取得 `key_hash` 的 `key` 值,其使用的是 Fibonacci hashing 。可以看到我們 buckets 的數量用 $2$ 的冪,取 modulo 可以直接使用 bit-shift ,比起使用 modulo 任意數更快。
```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_key`
輸入 `key` 找尋相對應的 `hash_key` 。
```c
static struct hash_key *find_key(map_t *map, int key) {
/* bucket 開頭位置 */
struct hlist_head *head = &(map->ht)[hash(key, map->bits)];
/* 走訪該 bucket 的元素 */
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` 對應的資料
用 `find_key` 搜尋 `hash_key` 湊砸表元素,找到則返回裡面存的資料 `data` ,否則返回 `NULL` 。
```c
void *map_get(map_t *map, int key)
{
struct hash_key *kn = find_key(map, key);
return kn ? kn->data : NULL;
}
```
### 加入湊砸表元素
此函式為插入新的湊雜表元素用,若 `key` 值已經被使用,則失敗,必須使用沒使用過的 `key` 值。
```c=
void map_add(map_t *map, int key, void *data)
{
/* 檢查是否 key 值重複 */
struct hash_key *kn = find_key(map, key);
if (kn)
return;
/* 建立新的湊雜表元素 */
kn = malloc(sizeof(struct hash_key));
kn->key = key, kn->data = data;
/* 插入到湊雜表對應的 bucket 裡 */
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 填空,在建立新的湊雜表元素後,我們要將其加入到湊雜表中,可以看到 `h` 是湊雜表的 bucket , `n` 是剛建立的湊雜表元素的連結 `node` ,也就是說要把 `n` 加入 `h` 中。
要幫 `n` 作連結卻沒看到 `n->next` 與 `n->pprev` 的賦值,所以這兩個填空應該與剛提的兩個賦值相關。
然後 `first` 是 bucket 的第一個元素,加上第 18 行 `h->first = n` ,所以 `n` 是要插在 bucket 第一個的位置。又在第 17 行 `first->pprev = &n->next` 會使用到 `n->next` 的值,所以 AAA 應該是 `n->next = first`。
那麼剩下的 BBB 就是要完整雙向鍊結結構,將 `n->pprev` 指到 `h->first` 的位置上,即 `n->pprev = &h->first` 。
### 刪除湊雜表
遍歷湊雜表的 buckets 和 bucket 裡的元素,一一釋放空間。
```c
void map_deinit(map_t *map)
{
if (!map)
return;
/* 走訪湊雜表的 buckets */
for (int i = 0; i < MAP_HASH_SIZE(map->bits); i++) {
struct hlist_head *head = &map->ht[i];
/* 走訪 bucket 裡的元素 */
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);
}
```
::: warning
看了一下插入刪除元素的操作,沒看到會有 `if (!n->pprev)` 為真的情況。待查。
:::
### Two sum
給定一數 `target` ,在給定數列 `nums` 中找尋兩個數相加的和為 `target` 的組合。
```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;
}
```
:::info
延伸問題:
1. 解釋程式碼運作原理
2. 研讀 Linux 核心原始程式碼 [include/linux/hashtable.h](https://github.com/torvalds/linux/blob/master/include/linux/hashtable.h) 及對應的文件 [How does the kernel implements Hashtables?](https://kernelnewbies.org/FAQ/Hashtables),解釋 hash table 的設計和實作手法,並留意到 [tools/include/linux/hash.h](https://github.com/torvalds/linux/blob/master/tools/include/linux/hash.h) 的 GOLDEN_RATIO_PRIME,探討其實作考量
:::
## 測驗 2 [LeetCode - Remove Duplicates from Sorted List II](https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/)
給定一個已排序過的單向鍊結結構,移除重複值的元素,如 1-1-2-3 變成 2-3 。
```c=
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;
}
```
程式碼可以看出是使用遞迴,然後遞迴的結束是回傳 `NULL` 或者返回 `head` 。再來看第二個 if ,若 COND1 為真,則會進入 `remove all duplicate numbers` 的區域,所以可以推測出 COND1 應該跟檢查重複值有關,而且檢查重複值元素的第一個,所以 COND1 應該是 `head->next && head->val == head->next->val` 。
然後繼續推測他是如何移除重複值元素,試想一個例子, 1-2-2-3 會變成 1-3 ,其中 1, 3 要作連結。在將這例子套進程式碼, `1->next` 會進入 `2` 的遞迴,等待回傳 `3` 的指標。而 `2` 的遞迴會進入 COND1 的區塊,這時要把 `head` 要移到哪裡,代入 `head->next` 才可以返回 `3` 的指標?因為返回只有兩種, `NULL` 和 `head` ,所以剛才要帶入的 `head->next` 應該就是 `3` 的指標。也就是說 `head` 應該要移到 `3` 的前一個元素, `2` ,那麼 COND2 區塊就是要把 `head` 移到重複元素的最後一個,那麼應該填 `head->next && head->val == head->next->val` 。
遞迴方式:走訪每一個元素,若有重複值的元素,則尋找下一個不重複值的元素,傳回其指標,跟第一個重複值元素的前一個元素相接。若沒遇到重複值元素,則照原連結方式連接。
改寫成迭代方式,原理跟上面方法一樣,只是我們不能用遞迴的 stack 紀錄前一個不重複值元素的指標,所以加入間接指標 `dup_pprev` 代替。
```c
struct ListNode *deleteDuplicates(struct ListNode *head)
{
struct ListNode *orig_head, **dup_pprev;
if (!head || !head->next)
return head;
orig_head = head;
/* 初始化 */
dup_pprev = &head;
do {
/* 發現有重複值元素 */
if (head->next && head->val == head->next->val) {
/* 直到找到不重複值元素 */
while (head->next && head->val == head->next->val)
head = head->next;
/* 前一個不重複值元素跨過重複值元素,跟找到的不重複值元素連接 */
*dup_pprev = head->next;
} else {
dup_pprev = &(*dup_pprev)->next;
}
head = head->next;
} while (head); /* 走訪所有元素 */
return orig_head;
}
```
:::info
延伸問題:
1. 嘗試避免遞迴,寫出同樣作用的程式碼
2. 以類似 Linux 核心的 circular doubly-linked list 改寫,撰寫遞迴和迭代 (iterative) 的程式碼
- [x] 迭代
- [ ] 遞迴
:::
使用類似 Linux `list_head` 雙向循環鍊結結構的版本(假設 `head` 沒有 `val` 值,只作雙向鍊結結構的開頭):
```c
struct ListNode {
int val;
struct ListNode *next, *prev;
};
struct ListNode *deleteDuplicates(struct ListNode *head)
{
struct ListNode *orig_head, *next;
int found_dup = 0;
/* 若 head 為 NULL, 空的或只有 1 個元素 */
if (!head || head->next == head || head->next = head->prev)
return head;
orig_head = head;
/* 走訪每一個元素,用 next 暫存下一個元素 */
for (head = head->next, next = head->next;
head != orig_head;
head = next, next = next->next) {
int cmp_result =
(head->next != orig_head) && (head->val != next->value);
/* 若與下一個元素重複值,或前一個元素為重複值元素 */
if (cmp_result || found_dup) {
/* 移除 head */
head->prev->next = next;
next->prev = head->prev;
/* 紀錄是否為重複值元素 */
found_dup = cmp_result;
}
}
return orig_head;
}
```
## 測驗 3 [LeetCode - LRU Cache](https://leetcode.com/problems/lru-cache/)
在 cache 上的管理,因為空間是有限的,所以我們會希望有方法可以符合需求和效率,知道哪些 cache 該留,哪些該捨棄。而 [Least Recently Used, LRU](https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU)) 為其中一種,若某一個 cache 很久沒有使用了,那可能之後也不會再使用了。
### 結構宣告
```graphviz
digraph {
rankdir=LR;
node[shape=record;fontname="Helvetica";]
hd [
label="bucket 0|<buc>bucket 1|...|bucket n";
xlabel="hheads";
];
hl1 [
label="key|value|{<p>prev|<n>next}|dlink";
xlabel="LRUNode";
];
hl2 [
label="key|value|{<p>prev|<n>next}|dlink";
xlabel="LRUNode";
];
hd:buc -> hl1:p[dir=both];
hl1:n -> hl2:p[dir=both];
hl2:n -> hd:buc[dir=both];
dd [
label="{<p>prev|<n>next}";
xlabel="dhead";
];
dl1 [
label="key|value|hlink|{<p>prev|<n>next}";
xlabel="LRUNode";
];
dl2 [
label="key|value|hlink|{<p>prev|<n>next}";
xlabel="LRUNode";
];
dd:n -> dl1:p[dir=both];
dl1:n -> dl2:p[dir=both];
dl2:n -> dd:p[dir=both];
}
```
從結構上看來, `LRUCache` 有一個湊雜表 `hheads` 和一個雙向陣列結構的開頭 `dhead` ,有 cache 的上限數量 `capacity` 和已使用的數量 `count` 。而 `LRUNode` 則是 cache 的單元,包括拿來湊雜搜尋用的 `key` 和 cache 儲存的內容 `value` ,還有分別對應 `hheads` 和 `dhead` 用的元素 `hlink`, `dlink` ,其連結方式如上圖。
```c
typedef struct {
int capacity, count;
struct list_head dhead, hheads[];
} LRUCache;
typedef struct {
int key, value;
struct list_head hlink, dlink;
} LRUNode;
```
### 建立與移除 cache
給定 cache 得容量 `capacity` ,然後依照其值配置對應的記憶體。最初還沒有使用 cache ,所以 `count` 會是零, `dhead`, `hheads` 分別指向自己,代表還是空的。
```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);
}
```
從結構上來看,要移除 cache `LRUCache` 需要先移除其元素 `LRUNode` ,其連接方式有兩種,若要找到所有元素最快的方法是從 `dhead` 遍歷。而 `dhead` 與 `dlink` 相連,利用 `list_entry` 可以找到對應的 `LRUNode` ,且我們會移除 `dlink` ,所以 MMM1 要使用 `list_for_each_entry_safe` 。
### 讀取 cache
當要讀取某特定的 cache 的時候,需要給該 cache 的 `key` 值,使用湊雜表快速搜尋該 cache 的位置,找到則回傳 cache 儲存的內容 `value` ,否則回傳 $-1$ 。過程中因為要使用 LRU 方法,所以會需要排列雙向鍊結結構的順序,
```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;
}
```
找到 bucket 位置,然後遍歷該 bucket ,依照 LRU 的規則,遍歷方向是 next 的方向。遍歷的變數是 `lru` ,型態為 `LRUNode` ,加上只要查看,沒有要移除節點,所以 MMM2 應該為 `list_for_each_entry` 。
### 存入 cache
輸入資料 `value` 和對應的 `key` 值,將這些內容存入或更新對應 `key` 值的 cache 的元素 `LRUNode` 。但是就如剛才所提的,這些元素是有限的,所以當我們是存入(沒有對應 `key` 值的 cache 元素),遇到已使用的元素數量 `count` 到達上限 `capacity` 時,我們需要利用 LRU 的規則,把最久沒有使用的 cache 元素給移除,然後再插入新的元素。而如果是更新,那只需要移動其元素在雙向鍊結結構裡的位置,以符合 LRU 要求。
```c
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;
}
```
就上述所說, MMM3 的區塊是更新元素的區塊,所以會遍歷對應 bucket 裡的元素,依照 LRU 規則,方向為 next 。而 `lru` 為 `LRUNode` 結構,移動完元素位置後就停止遍歷,所以 MMM3 為 `list_for_each_entry` 。
而下一個區塊會檢查 `count` 與 `capacity` ,可以知道是要存入元素。而如果相同,根據 LRU 的規則,那就要先移除最久沒有使用的元素,在雙向鍊結結構的末尾,所以 MMM4 應該為 `list_last_entry` 。移除後將要存入的資料更新上去,然後在加回到 `lRUCache` 中。
:::info
延伸問題:
1. 解釋上述程式碼的運作,撰寫完整的測試程式,指出其中可改進之處並實作
2. 在 Linux 核心找出 LRU 相關程式碼並探討
:::
## 測驗 4 [LeetCode - Longest Consecutive Sequence](https://leetcode.com/problems/longest-consecutive-sequence/description/)
給定一未排序陣列,找出最長連續數列的長度,比如 1-2-5-3-7-8 ,最長連續數列為 1-2-3 ,所以長度是 3 。此處使用湊雜表,在建立完湊雜表(時間複雜度 $O(n)$ )後,要搜尋數列下一個元素的時間複雜度變為 $O(1)$ ,總時間複雜度為 $O(n)$ 。
### 結構宣告
```graphviz
digraph a {
rankdir=LR;
node[fontname="Helvetica";shape=record;];
edge[style=dashed];
heads [
label="bucket 0|<buc>bucket 1|...|bucket n";
xlabel="heads";
];
hn1 [
label="num|{<pp>pprev|<n>next}";
xlabel="seq_node";
];
hn2 [
label="num|{<pp>pprev|<n>next}";
xlabel="seq_node";
];
end [
label="NULL";
shape=plaintext;
]
heads:buc -> hn1;
hn1:n -> hn2;
hn1:pp -> heads:buc;
hn2:pp -> hn1:n;
hn2:n -> end;
}
```
此為湊雜表的元素, `num` 是陣列的某一位置的數值, `link` 是與湊雜表和湊雜表元素連結的成員。
```c
struct seq_node {
int num;
struct list_head link;
};
```
### 湊雜表元素搜尋
搜尋湊雜表元素,湊砸函數使用到陣列的數值 `num` 和大小 `size` ,得到湊雜值 `hash` 後在對應的 bucket 裡( `heads[hash]` )找尋元素,找到則回傳該指標,否則回傳 `NULL` 。
```c
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;
}
```
### 主函式
首先先建立湊雜表,並將陣列數值放入湊雜表中。在來依序取陣列的數值,找尋包含該數值之最長連續數列長度 `len` ,若找到的長度比 `length` 更長,則更新最終最長長度 `length` 。
```c=
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]);
}
}
/* 依序取陣列的數值,找尋包含該數值最長連續數列長度
* 若找到長度比 length 更長,則更新 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;
}
```
兩個填空 LLL, RRR 剛好是在對稱的位置上,從第 31 行看到 `node` 被移除,也就是 `num` 已經造訪過了,之後應該要往數值的兩邊作找尋,也就是第 34, 39 行。從第 33 行得知 `left` 與 `right` 是 `num` ,所以兩填空應該要為 `--left` 與 `++right` 才會進入迴圈。
:::info
延伸問題:
1. 解釋上述程式碼的運作,撰寫完整的測試程式,指出其中可改進之處並實作
2. 嘗試用 Linux 核心風格的 hash table 重新實作上述程式碼
:::
### 使用 Linux 核心風格的 hash table 改寫
在 Linux 裡湊雜表元素是使用 `sturct hlist_node` ,這裡替換原本的 `struct list_head` 。
```c
struct seq_node {
int num;
struct hlist_node link;
};
```
在 Linux 裡的湊雜函數是使用 Fibonacci hasing ,輸入一個 key 然後得到 bucket 的位置,這裡我們直接使用 `num` 當 key 值。 `hash_for_each_possible` 會遍歷對應 key 值的 bucket ,這裡替換原本的 `list_for_each_entry` 和 hash 計算。
```c
static struct seq_node *find(int num, struct hlist_head *heads)
{
struct seq_node *node;
/* use nums[i] as key directly */
hash_for_each_possible (heads, node, link, num) {
if (num == node->num)
return node;
}
return NULL;
}
```
最後是主函式,需要建立一個空湊雜表,原本是用迴圈和 `INIT_LIST_HEAD` 初始化,在 Linux 裡則是有 `__hash_init` 可以使用,其湊雜表的大小固定為 $2$ 的冪,所以要先算一下湊雜表的大小,至少是大於 `n_size` 的。補充說明一下,為何不用 `HASH_SIZE` 巨集,因為我們要使用動態配置記憶,這樣導致 `heads` 不是陣列,所以無法使用。另外因為同樣的理由,沒有使用 `hash_init` 和 `DEFINE_HASHTABLE` 。
```c
int longestConsecutive(int *nums, int n_size)
{
int length = 0, bits = 4, ht_size;
struct seq_node *node;
struct hlist_head *heads;
/* hashtable with size 2^bits */
while (n_size > (1 << bits))
++bits;
ht_size = 1 << bits;
heads = malloc(sizeof(struct hlist_head) * ht_size);
__hash_init(heads, ht_size);
for (int i = 0; i < n_size; ++i) {
/* if not found, create and add it in */
if (!find(nums[i], heads)) {
node = malloc(sizeof(struct seq_node));
node->num = nums[i];
/* use nums[i] as key directly */
hash_add(heads, &node->link, nums[i]);
}
}
for (int i = 0; i < n_size; ++i) {
int num, len = 0;
node = find(nums[i], heads);
while (node) {
++len;
num = node->num;
hash_del(&node->link);
int left = num, right = num;
/* search leftward */
while (node = find(--left, heads)) {
++len;
hash_del(&node->link);
}
/* search rightward */
while (node = find(++right, heads)) {
++len;
hash_del(&node->link);
}
length = len > length ? len : length;
}
}
return length;
}
```
剩下的部份有兩個函式有替換,第一個是 `list_add` 插入湊雜表元素,這裡換為 `hash_add` ,不一樣的地方是要多輸入 key 值,也就是他會在裡面算對應的 bucket 位置。而另一個函式是 `list_del` ,利用雙向連結結構, `hash_del` 一樣只需要輸入要移除的元素即可。