---
tags: linux2022
---
# 2022q1 Homework1 (quiz1)
contributed by < [`qwe661234`](https://github.com/qwe661234) >
[測驗題目](https://hackmd.io/@sysprog/linux2022-quiz1)
## 測驗一 Two Sum
### Hash Table Structure
![](https://i.imgur.com/5FQZ6Lo.png)
hash table bucket 以 non-circular doubly-linked list 來儲存
* hlist_head: 指向 bucket list 的第一個節點
* hlist_node: bucket list 中的節點
`map_t`
* bits: hash table 的 size.
* ht: array of hlist_head
```c
struct hlist_head {
struct hlist_node *first;
};
struct hlist_node {
struct hlist_node *next, **pprev;
};
typedef struct {
int bits;
struct hlist_head *ht;
} map_t;
```
### map_init
Create hash table
```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;
}
```
### container_of
給定結構體成員的位址, 回推該結構體的起始位置
reference: https://hackmd.io/@sysprog/linux-macro-containerof
```c
#define container_of(ptr, type, member) \
({ \
void *__mptr = (void *) (ptr); \
((type *) (__mptr - offsetof(type, member))); \
})
```
### hash function
```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: value of array element
* data: array index
```c
struct hash_key {
int key;
void *data;
struct hlist_node node;
};
```
### find_key
由於不同的 key value 經過 hash funciton 後可能得到一樣的 value, 所以經過 hash 後需要到對應的 bucket 中尋找 `node->key == key` 的 node 並回傳
```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
在 hash table 中尋找對應 key value 的 node
```c
void *map_get(map_t *map, int key)
{
struct hash_key *kn = find_key(map, key);
return kn ? kn->data : NULL;
}
```
### map_add
一開始會在 hash table 中尋找對應 key value 的 node, 若已存在就不重複加入
若不存在, 先配置記憶體給 new node 並設好 key 與 data, 接著以 hash funciton 得到的 value 去找出 bucket 對應的 `hlist_head`,`first` 為 bucket 中的第一個 node (或是 NULL), 接下來的操作其實就是把原本的 `first` 接在 new node 後面, 把 new node 變成 `first`.
```graphviz
digraph structs {
rankdir="LR"
node[shape=record];
struct1 [label="0|<1>1|2|3|4|5|..."]
node[shape=record];
struct2 [shape=record, label="{first|{<p>pprev|<n>next}}"];
node[shape=record];
struct3 [label= "{new node|{<p>pprev|<n>next}}"];
struct1:1 -> struct2;
struct2:p -> struct1:1;
struct2:n -> NULL;
}
```
```graphviz
digraph structs {
rankdir="LR"
node[shape=record];
struct1 [label="0|<1>1|2|3|4|5|..."]
node[shape=record];
struct2 [shape=record, label="{<f>first|{<p>pprev|<n>next}}"];
node[shape=record];
struct3 [label= "{<f>new node|{<p>pprev|<n>next}}"];
struct1:1 -> struct3:f;
struct2:p -> struct3:n;
struct3:n -> struct2:f
struct3:p -> struct1:1
struct2:n -> NULL;
}
```
:::info
1. AAA = `n->next = first`
將 new node 的 next 指向原本的 `first`
2. BBB = `n->pprev = &h->first`
將 new node 的 pprev 指向此 bucket 的 hlist_head
:::
```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;
if (first)
first->pprev = &n->next;
h->first = n;
n->pprev = &h->first;
}
```
### map_deinit
清除 hash table
```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);
}
```
### main funciton
1. 首先建立 hash table 並配置 memory 給 return array
2. 接著 iterate array nums
3. 尋找 key vlaue 為 target - nums[i] 的 node 是否有在 hash table 中
* 找到就將 return array 的第一跟第二個 element 設為這兩個值的 array index, returnSize 設為 2 並 return
* 找不到就建立 new node, key 為 nums[i], data 為 array index, 放入 hash table 中
4. 如果 nums 中任兩個 element 相加都不為 target, 則 returnSize = 0
```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;
}
```
## Hash funciton
Hash funciton: hash function 的功能是用來將 key 值轉換成對應的 index
### **Perfect function**
當所有的 key value 經過 hash funciton 轉換後, 得到 index 都不一樣時, 我們稱此 hash funciton 為 **perfect funciton**。 **Perfect funciton** 可以做到完美的 1-to-1, 也就是一組 key 對應到一組 value。 假如 hash function 的時間複雜度是 O(1), 這樣我們僅僅只需要時間複雜度 O(1) 的時間就可以查找到 hash table 中的資料。
但實際上 **perfect funciton** 很難做到, 我們不知道 key value 的 range 有多大, 所以不知道 bucket 的數量要多少才能滿足 1-to-1, 如果 hash table size 很大, 但其中有很多空間都沒有使用到也會造成不必要的浪費。由於多個 key value 可能對應到同一個 index, 所以一個 index 會對應到一個可以存數筆 data 的 bucket。
綜合以上, 我們只能透過設計 hash function, 使其盡可能接近 **perfect funciton**。
### Hash function 考慮因素
#### 1. 計算所需的時間複雜度要低
希望 hash funciton 計算的時間複雜度為 O(1)。
#### 2. Collision 減少
Collision 是指不同的 key value 對應到相同的 index, collision 越少越接近 **perfect funciton**。
#### 3. 避免 Clustering 現象
Clustering 是指某些 bucket 存放很多筆資料, 而某些 bucket 存放的很少, 應該盡量讓 bucket 存放的資料筆數平衡, 否則容易造成 `Overflow`, 這樣會造成存取效率變差。
`Overflow` 是指 bucket 的記憶體空間滿了, 需要刪除一筆資料才能存入新的資料, `Overflow` 發生時, 決定哪一筆資料要被刪除也是重要的議題。
### 常見 hash function 作法
#### 1. Division method
h(k) = k % N
以 mod 運算後的餘數作為 index, 由於 key 值得範圍無法得知, 所以如何挑選適合的 N 很關鍵。
#### 2. Mid-square
h(k) = $bits_{i,i + r - 1}(k^2)$
將 key 值平方後, 取其 bit 第 $i$ ~ $i + r - 1$ 位, 得到的數字範圍會是 0 ~ $2^{r - 1}$, 所以 bucket 數量為 $2^r$ 即可。
例如:
key value = 6, $6^2 = 36$, 假設 i = 2, r = 3, 所以取其 bit 第 2 ~ 4 位
所以將 $36_{10}$ = $100100_2$, 取第 2 ~ 4 位得 $010_2$, 所以 index = $2_{10}$。
#### 3. Folding addition
先將 key value 切割成片段後相加, 亦可以對相加後的結果做其他運算
例如:
key value = 3823749374, 將此 value 每三個數字分成一段
382 | 374 | 937 | 4, 再將這四個數字相加
index = 382 + 374 + 937 + 4 = 1697
可以進一步對 1697 進行其他運算例如: mod, 反轉...
#### 4. Multiplication Method
由於大部分的情況下, 我們不知道 key value 的範圍以及分佈情形, 這樣的情形適用 `Multiplication Method`
`Multiplication Method` 步驟:
![](https://raw.githubusercontent.com/alrightchiu/SecondRound/master/content/Algorithms%20and%20Data%20Structures/HashTable%20series/Intro/f8.png)
key value 乘上 constant A -> 取小數點部分 -> 再乘上 m -> 再取整數部分的一系列步驟讓 hash function 能夠儘量把更多的 key value 中的 bit 納入考慮而增加隨機性。 (原因參考[這篇](http://alrightchiu.github.io/SecondRound/hash-tableintrojian-jie.html#dm))
K: key value
A: a constant, 且 0 < A < 1
m: bucket 數量, 且 m = $2^p$
## Linux hash function
在 linux 的 [hash.h](https://github.com/torvalds/linux/blob/master/tools/include/linux/hash.h) 中, hash funciton 的設計是使用上面所提到的 `Multiplication Method`, 但上面提到的 hash funciton 應該是要長得像 $h(K) = \lfloor m * (KA - \lfloor KA \rfloor) \rfloor$ 這個樣子, 但在 [hash.h](https://github.com/torvalds/linux/blob/master/tools/include/linux/hash.h) 中卻用到了 bit-shifting 的方式, 這是由於上述的 funciton 與 $h(K) = K * 2^w * A >> (w - p)$ 是等價的, 而且 bit-shifting 的效率比較好, 所以以這種方式來實作。
K: key value
A: a constant, 且 0 < A < 1
m: bucket 數量, 且 m = $2^p$
$w$: 一個 word 有幾個 bit
### 為什麼兩個 function 等價
我們假設一個 word 是 8 個 bit, 將 KA 的結果以 8 bit 整數配上 8 bit 浮點數表示
```graphviz
graph structs {
node[shape=record];
struct1 [label="0|0|0|0|0|1|0|1"]
node[shape=point]
dot
node[shape=record];
struct2 [label="1|1|1|0|1|0|0|0"]
}
```
根據 $h(K) = \lfloor m * (KA - \lfloor KA \rfloor) \rfloor$, 我們只需要 KA 的浮點數部分, 也就是後面的 8 個 bits, 再假設 m 是 8 = $2^3$, 所以乘上 m, 所以以上步驟等同於將前8個 bit 清除再往左移三位
```graphviz
graph structs {
node[shape=record];
struct1 [label="0|0|0|0|0|1|1|1"]
node[shape=point]
dot
node[shape=record];
struct2 [label="0|1|0|0|0|0|0|0"]
}
```
最後做下高斯只留下整數部分
```graphviz
graph structs {
node[shape=record];
struct1 [label="0|0|0|0|0|1|1|1"]
node[shape=point]
}
```
而這樣的操作等同於 KA 的結果先左移 8 位, 這樣前面 8 個 bit 為 KA 結果的浮點數部分, 依照前面的操作我們知道浮點數部分其實只要留下前 p 個 bit, 所以要右移 (w - p) 個 bit
KA
```graphviz
graph structs {
node[shape=record];
struct1 [label="0|0|0|0|0|1|0|1"]
node[shape=point]
dot
node[shape=record];
struct2 [label="1|1|1|0|1|0|0|0"]
}
```
K * $2^w$ * A, 乘上 $2^w$$ 等同於左移 8 個 bit
```graphviz
graph structs {
node[shape=record];
struct1 [label="1|1|1|0|1|0|0|0"]
node[shape=point]
dot
node[shape=record];
struct2 [label="0|0|0|0|0|0|0|0"]
}
```
接著右移 (w - p), 也就是 8 - 3 = 5 個 bit, 因為在 linux 的實用中是用 uint, 所以只會留下整數部分
```graphviz
graph structs {
node[shape=record];
struct1 [label="0|0|0|0|0|1|1|1"]
node[shape=point]
}
```
### linux 程式碼
我們擷取 32 bit 的 hash funciton 的程式碼 (64 bit 的 hash funciton 邏輯相同, 只是 word size 為 64)
source: [hash.h](https://github.com/torvalds/linux/blob/master/tools/include/linux/hash.h)
#### 程式碼解釋
* GOLDEN_RATIO_32: A * $2^{32}$ 後的結果
* __hash_32_generic(u32 val)
* val: key value K
* 這個 function 做的事情是算出 $K * 2^w * A$ 的結果
* hash_32(u32 val, unsigned int bits)
* val: key value K
* bits: 其實就是 p, $2^{bits}$ = bucket 數量
* 這個 function 做的事情是將 `__hash_32_generic(u32 val)` 算出的結果 $K * 2^w * A$ 右移 (32 - p)
因此, val * GOLDEN_RATIO_32 >> (32 - bits) $\equiv$ K * A * $2^w$ >> (w - p)
```c
#define GOLDEN_RATIO_32 0x61C88647
#ifndef HAVE_ARCH__HASH_32
#define __hash_32 __hash_32_generic
#endif
static inline u32 __hash_32_generic(u32 val)
{
return val * GOLDEN_RATIO_32;
}
static inline u32 hash_32(u32 val, unsigned int bits)
{
/* High bits are more random, so use them. */
return __hash_32(val) >> (32 - bits);
}
```
### GOLDEN_RATIO
至於 A 為什麼使用 [golden ratio](https://zh.wikipedia.org/wiki/%E9%BB%84%E9%87%91%E5%88%86%E5%89%B2%E7%8E%87), A = $(\sqrt{5} - 1) / 2$ = 0.6180339887..., 這個數字是由 [Donald Knuth](https://en.wikipedia.org/wiki/Donald_Knuth) 所建議的, 在這份 [hunter college 的課程講義](http://www.compsci.hunter.cuny.edu/~sweiss/course_materials/csci335/lecture_notes/chapter05.pdf) 中有提出實驗結果圖, 其中當 A = [golden ratio](https://zh.wikipedia.org/wiki/%E9%BB%84%E9%87%91%E5%88%86%E5%89%B2%E7%8E%87) 時, 此 hash function 稱為 `Fibonacci hashing`, 且 key value 經此 funciton 轉換得到的 index 是相當分散的, 意即 Collision 很低。
![](https://i.imgur.com/TU1DpWk.png)
### 比較 golden ratio 與非 golden ratio
這邊挑選了4個不同的數字作為 A * $2^{32}$, 並紀錄對 key value 0 ~ 1500 做 `hash_32(key value, 10)` 的結果
1. 0x61C88647
2. 0x80000000
3. 0x12345678
4. 0x54061094
圖的 x 軸是 key value, y 軸是 `hash_32(key value, 10)` 計算出的值
![](https://i.imgur.com/kP2RoI9.png =50%x)![](https://i.imgur.com/BFgUtcA.png =50%x)
![](https://i.imgur.com/xEmZWP7.png =50%x)![](https://i.imgur.com/X1D9qBi.png =50%x)
由實驗個果圖可發現, golden ratio 作為 A 結果是最均勻分散的, 另外結果也證明 A 的值的對於 Collision 有很大的影響, 例如 A * $2^{32}$ = 0x80000000 時, 0 ~ 1500 只有兩種可能, 不僅每兩個數字就發生一次 Collision, 由於 data 只會存放於兩個 bucket, 因此 Cluster 程度相當嚴重。
### Reference:
* https://meteorv.dev/Data-Structure/hashing/#Collision-Overflow-%E7%9A%84%E8%99%95%E7%90%86%E6%96%B9%E6%B3%95
* http://alrightchiu.github.io/SecondRound/hash-tableintrojian-jie.html#dm
* https://www.cs.hmc.edu/~geoff/classes/hmc.cs070.200101/homework10/hashfuncs.html
* http://www.cs.nthu.edu.tw/~wkhon/ds/ds12/lecture/lecture18.pdf
* http://www.compsci.hunter.cuny.edu/~sweiss/course_materials/csci335/lecture_notes/chapter05.pdf
## 測驗二 Remove Duplicates from Sorted List II
:::info
`COND1` = `COND2` = `head->next && head->val == head->next->val`
:::
邏輯是將值不重複的 node 的 next 指向下一個值不重複的 node, 忽略中間所有值重複的 node
先檢查 next node 是否存在, 接著檢查 next node 的值是否與 current node 的值重複。 遇到重複會進入 `while` loop 直到 next node 與 current node 的值不重複, 對 next node 做遞迴, 唯有值不重複的 node 才會被回傳
```c
#include <stddef.h>
struct ListNode {
int val;
struct ListNode *next;
};
struct ListNode *deleteDuplicates(struct ListNode *head)
{
if (!head)
return NULL;
if (head->next && head->val == head->next->val) {
/* Remove all duplicate numbers */
while (head->next && head->val == head->next->val)
head = head->next;
return deleteDuplicates(head->next);
}
head->next = deleteDuplicates(head->next);
return head;
}
```
### 非遞迴版本:
邏輯與遞迴版本類似, 以指標的指標操作可以省去檢查 current node 是否為 head
#### `*cur = (*cur)->next` 與 `cur = &(*cur)->next`差異
`*cur = (*cur)->next`: 將 head or 前一個 node 的 next 指向位置改為 next node
`cur = &(*cur)->next`: iterate next, 將 cur 設為 next node
```c
#include <stddef.h>
struct ListNode {
int val;
struct ListNode *next;
};
struct ListNode *deleteDuplicates(struct ListNode *head)
{
struct ListNode **cur;
cur = &head;
while (*cur) {
if ((*cur)->next && (*cur)->val == (*cur)->next->val) {
while ((*cur)->next && (*cur)->val == (*cur)->next->val)
*cur = (*cur)->next;
*cur = (*cur)->next;
} else {
cur = &(*cur)->next;
}
}
return head;
}
```
## circular doubly-linked list 改寫
### doubly-linked list structure
Doubly-linked list and its value is integer.
```c
struct list_head {
struct list_head *prev;
struct list_head *next;
};
typedef struct {
int value;
struct list_head list;
} element_t;
```
### 迭代 (iterative) version
由於 list_head 不會變, 所以不用回傳 list_head
由於 safe 會保存 next node, 所以直接刪除當前節點, `while` loop 終止時, current node 會指向一串存在重複值 node list 中的最後一個 node, 所以 `while` loop 結束後要刪除 current node
```c
void deleteDuplicates(struct list_head *head)
{
if (!head)
return;
struct list_head *cur, *safe;
list_for_each_safe (cur, safe, head) {
if (list_entry(cur, element_t, list)->value == list_entry(safe, element_t, list)->value) {
while(safe != head &&
list_entry(cur, element_t, list)->value == list_entry(safe, element_t, list)->value) {
list_del(cur);
cur = safe;
safe = safe->next;
}
list_del(cur);
}
}
}
```
### 遞迴 (recursive) version
如果 current node 有重複, 先遞迴 next node, 遞迴返回後再刪除 current node, 考慮到一串存在重複值的 node list 中的最後一個 node, 所以要檢查與 previous node 的值是否相同.
```c
void deleteDuplicates(struct list_head *cur, struct list_head *head)
{
if(cur == head)
return;
if ((cur->next != head && list_entry(cur, element_t, list)->value == list_entry(cur->next, element_t, list)->value)
|| (cur->prev != head && list_entry(cur, element_t, list)->value == list_entry(cur->prev, element_t, list)->value)) {
deleteDuplicates(cur->next, head);
list_del(cur);
}
deleteDuplicates(cur->next, head);
}
```
## 測驗三 [LeetCode 146. LRU Cache](https://leetcode.com/problems/lru-cache/)
[Least recently used (LRU)](https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU)) 是一種 cache 的置換機制, 主要是當 cache 記憶體已滿, 將最近最少使用的一筆資料與新資料替換, 這樣的機制是為了降低 cache miss rate, 在這題中要替換的是使用時間距離目前最遠的那筆資料。
### Cache Data Structure
在 cache 的實作類似測驗一的 hash table, hheads[index] 會指向對應此 index 的 bucket, 而 bucket 是以 circular doubly-linked list 來實作。
除了 hheads array 之外, 還有另外一個 dhead, 會指向一條用來紀錄的 circular doubly-linked list, 這邊紀錄的是 LRUNode 的使用情況, 最近使用的 node 會被擺在越前面, 越之前使用的 node 會被擺在越後面, 所以要替換時, 會選擇這一條 list 中最後面的 node 來做替換。
Cache 中的 node 和 dhead 所指向的 list 中的 node 是同一份。
```c
struct list_head {
struct list_head *prev;
struct list_head *next;
};
```
capacity: cache 容量, 即可存放的 data 筆數
count: data 目前存放了多少筆 data
```c
typedef struct {
int capacity, count;
struct list_head dhead, hheads[];
} LRUCache;
```
hlink 用來連接 cache 中的 bucket, dlink 則是用來連接紀錄 node 使用情形的 list。
```c
typedef struct {
int key, value;
struct list_head hlink, dlink;
} LRUNode;
```
### lRUCacheCreate
Create LRU cache, 並初始化 dhead and hhead array。
這邊的 `sizeof(*obj)` 印出來是 24, 代表的是 count(4 bytes) + capacity(4 bytes) + dhead(16 bytes) 的空間, 另外的 `capacity * sizeof(struct list_head)` 則是配置給 hheads array 的空間。
```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;
}
```
### lRUCacheFree
:::info
MMM1 = list_for_each_entry_safe
:::
dhead 所指向的 list 中代表目前存在 cache 中的所有 node, 所以將這條 list 中的所有 node 釋放等同於釋放 cache, 用 `list_for_each_entry_safe` 是因為有移除 node, 所以要先紀錄下一個 node 位置。
```c
void lRUCacheFree(LRUCache *obj)
{
LRUNode *lru, *n;
list_for_each_entry_safe (lru, n, &obj->dhead, dlink) {
list_del(&lru->dlink);
free(lru);
}
free(obj);
}
```
### lRUCacheGet
:::info
MMM2 = list_for_each_entry
:::
key 經過 hash funciton 後, 得到對應的 index, 再以此 index 找到對應的 bucket, 由於 不同的 key 經過 hash function 可能得到相同的 index, 所以要從 bucket 中所有的 node 找到 `node>key == key` 的 node。
```c
int lRUCacheGet(LRUCache *obj, int key)
{
LRUNode *lru;
int hash = key % obj->capacity;
list_for_each_entry (lru, &obj->hheads[hash], hlink) {
if (lru->key == key) {
list_move(&lru->dlink, &obj->dhead);
return lru->value;
}
}
return -1;
}
```
### lRUCachePut
:::info
MMM3 = list_for_each_entry
MMM4 = list_last_entry
**MM4 的正確答案給 list_for_each_entry, 但應該是 list_last_entry, 如果是 list_for_each_entry 參數對不上**
:::
key 經過 hash funciton 後, 得到對應的 index, 再以此 index 找到對應的 bucket, 接著到該 bucket 中找 `node->key == key` 的 node。
1. 有找到: 將 value 更新, 並將該 node 移到用來記錄使用情形的 list 中的第一個, 因為這個 node 當前被使用到。
2. 沒找到: 如果找不到代表需要新增 node 到 cache 中, 此時要檢查 cache 記憶體是否滿了。
* cache 記憶體已滿, 這邊的做法是先找到用來記錄使用情形的 list 中的最後一個 node, 因為這個 node 的使用時間是距離目前最遠的, 接著把他從 cache 和用來記錄使用情形的 list 中移除, 但是沒有 free 掉, 而是用同一份記憶體空間, 只是將其 key 和 value 更新, 從新放入 cache 中對應的 bucket 和用來記錄使用情形的 list 中。
* cache 記憶體還有剩餘空間的話, 則配置記憶體給新的 node 並設定好 key 和 value, 放入 cache 中對應的 bucket 和用來記錄使用情形的 list 中。
```c
void lRUCachePut(LRUCache *obj, int key, int value)
{
LRUNode *lru;
int hash = key % obj->capacity;
list_for_each_entry (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 = list_last_entry(&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;
}
```
### Test fucntion
測試 `lRUCacheCreate` 是否有成功
```c
bool testLRUCacheCreate(LRUCache *cache) {
if(!cache)
return false;
return true;
}
```
在 lRUCachePut 後測試 cache 是否有新增或更新該 key 所對應的 value。
```c
bool testCacheGet(LRUCache *cache, int key, int rightvalue) {
if(!cache) {
printf("cache is NULL");
return false;
}
int target = lRUCacheGet(cache, key);
if(target != rightvalue)
return false;
return true;
}
```
## 改進
觀察程式碼可以發現這邊所使用的 hash function 是 division method, 也就是直接把 key mod cache capacity, 可是者樣的作法可能導致 collision 和 cluster。
例如:
假設 cache capacity 是 10, 而插入的 key value pair 是 (1, 1), (11, 11), (21, 21), (31, 31), (41, 41), 經過 hash funciton `key mod 10` 後, 這五個 node 得到的 index 都是 1, 而且這 5 個 node 都會接在 `hheads[1]` 之後, 因而導致存取效率變差。
我們可以考慮將 hash funciton 改成在 hash.h 中的 `hash_32`, 並比較改變前後的執行時間。
### 實驗
本次實驗的 capacity 為 128 = $2^{7}$, bits = 7
#### hash_function_1
```c
int hash = key % obj->capacity(128);
```
#### hash_funciton_2
```c
int hash = hash_32(key, bits) % 128;
```
```c
#define GOLDEN_RATIO_32 0x61C88647
static inline u32 __hash_32_generic(u32 val)
{
return val * GOLDEN_RATIO_32;
}
static inline u32 hash_32(u32 val, unsigned int bits)
{
/* High bits are more random, so use them. */
return __hash_32(val) >> (32 - bits(7));
}
```
### 實驗步驟
取一個隨機整數, 接著到 cache 中尋找, 如果沒找到就將 key 和 value 都設為此隨機整數並加入 cache 中, 重複 n 次 ( n 從 0 ~ 2000000 )。
```c
int main(int argc, char** argv) {
for(int size = 0; size <= 20000000; size+= 100000) {
LRUCache *cache = lRUCacheCreate(128);
int rand_num;
clock_t start, end;
start = clock();
srand(time(NULL));
for(int i = 0; i < size; i++) {
rand_num = rand();
if(!lRUCacheGet(cache, rand_num))
lRUCachePut(cache, rand_num, rand_num);
}
end = clock();
printf("%d %lf\n", size, (end - start) / (double)(CLOCKS_PER_SEC) * 1000);
lRUCacheFree(cache);
}
}
```
### 實驗結果
實驗結果圖顯示這兩個 hash function 在效能上的表現並無明顯差異
![](https://i.imgur.com/6nGLiw8.png)
### 分析原因
我對亂數經過 hash_function 後得到的 index 紀錄並作圖, 結果圖為對 2000 個亂數做 hash funciton 後得到的 index, 對比兩張圖可發現, 其實兩個 hash function 對亂數計算後, 產生的結果分佈狀況差不多, 故推論其為效能無明顯差異的原因。
![](https://i.imgur.com/6C9OJQ1.png)
![](https://i.imgur.com/iln1OJZ.png)
## 測驗四 [LeetCode 128. Longest Consecutive Sequence](https://leetcode.com/problems/longest-consecutive-sequence/description/)
### Data Structure
這題的 data structure 實作類似測驗一的 hash table, heads[index] 會指向對應此 index 的 bucket, 而 bucket 是以 circular doubly-linked list 來實作。
```c
struct list_head {
struct list_head *prev;
struct list_head *next;
};
struct seq_node {
int num;
struct list_head link;
};
```
### find
hash table 的 hash function 是 abs(key) % size, size 是 input array 的大小, function `find` 就是在 hash table 中尋找是否存在 key = num 的 node。
```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;
}
```
### longestConsecutive
這個 function 拆成 3 個部分來看
#### part1
建立 hash table 並初始化
```c
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]);
```
#### part2
interate input array 並為 array 中的數字建立 node 加入對應的 bucket 中。
假設 inpur array = [98,1,3,2], size = 6
圖中的 head 分別有 prev 指向 bucket 的最後一個 node 和 next 指向 bucket 的第一個 node。
```graphviz
digraph structs {
rankdir="LR"
node[shape=record];
ht [label="0|<1>1|<2>2|<3>3|4|5"]
node[shape=record];
node1 [shape=record, label="{<f>1|{<p>pprev|<n>next}}"];
node[shape=record];
node2 [label= "{<f>2|{<p>pprev|<n>next}}"];
node3 [label= "{<f>3|{<p>pprev|<n>next}}"];
node98 [label= "{<f>98|{<p>pprev|<n>next}}"];
node1:p -> ht:1;
node1:n -> ht:1;
node3:p -> ht:3;
node3:n -> ht:3;
node2:p -> ht:2;
node2:n -> node98;
node98:p -> node2:n;
node98:n -> ht:2
}
```
```c
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]);
}
}
```
#### part3
iterate input array 中的所有 element, 對於每一個 element 都去 hash table 找其左半邊連續數字是否存在在 hash table 中, 再搜尋其右半邊。在查找的過程中, 會把走訪過的 node 移除, 避免重複走訪的情形。
例如:
當前 iterate 到的 element 是 5, 他就會先走 4 是否存在 hash table 中, 存在就 `len++`, 接著找 3, 直到該 key 在 hash table 找不到, 接著找 6, 7 ..., 找到做 `len++`, 找不到就結束, 繼續 iterate 下一個 element。
:::info
LLL = --left
RRR = ++right
:::
```c
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(--left, n_size, heads))) {
len++;
list_del(&node->link);
}
while ((node = find(++right, n_size, heads))) {
len++;
list_del(&node->link);
}
length = len > length ? len : length;
}
}
```
#### 完整 funciton
```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]);
}
}
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;
}
```
### 改進之處
原本的 fucntion `longestConsecutive` 在查找後將 node 從 hash table 中移除, 但移除後並未釋放記憶體, 以及在 funciton 結束後並未將配置給 heads 的記憶體釋放, 因此會造成 **memory leak**。
### 測試
我們以執行以下程式碼並透過 `Valgrind` 分析。
```c
int main(int argc, char** argv) {
int a[10] = { 0, 3, 7, 2, 5, 8, 4, 6, 0, 1 };
printf("Longest Consecutive Sequence = %d\n", longestConsecutive(a, 10));
return 0;
}
```
#### 分析結果
* `indirectly lost` 中的 1 個 blocks 是我們配置給 array of heads, 我們配置 10 個 `struct list_head`, 而一個 `struct list_head` 需要 16 bytes 的空間。
* `definitely lost` 是我們配置給 node 的空間, 我們總更配置了 9 個 node (key = 0, 1, 2, 3, 4, 5, 6, 7, 8, 9), 但奇怪的地方是 `struct list_head` 需要 16 bytes, 而 `int` 需要 4 個 bytes, 總共只需 20 個 bytes, 為何印出卻是 24 個 bytes?
透過 `offsetof(struct seq_node, link)` 可以發現, link 的位移量是 8 而不是預期中的 4, 這邊推測是 memory alignment 所導致這樣的結果。
![](https://i.imgur.com/Ixr7lF8.png)
為了驗證, 我們可以在 `struct seq_node` 的後方加上, 讓結構體的成員實際排列和空間佔用跟程式碼順序一致, 再次印出 `offsetof(struct seq_node, link)` 的結果, 可以發現結果為 4, 驗證我們的推測是正確的。
```c
struct seq_node {
int num;
struct list_head link;
} __attribute__((packed));
```
##### reference:
https://hackmd.io/@sysprog/linux-macro-containerof
#### 改進程式碼
在移除 node 之後, 將 node 記憶體釋放, 並在走訪完每個 node 後將配置給 head 的記憶體釋放。
```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]);
}
}
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);
free(node);
int left = num, right = num;
while ((node = find(--left, n_size, heads))) {
len++;
list_del(&node->link);
free(node);
}
while ((node = find(++right, n_size, heads))) {
len++;
list_del(&node->link);
free(node);
}
length = len > length ? len : length;
}
}
free(heads);
return length;
}
```
#### 分析結果
解決 memory leak 問題
![](https://i.imgur.com/TbjPHj9.png)
### 以 Linux 核心風格的 hash table 重新實作
```c
#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>
struct hlist_node { struct hlist_node *next, **pprev; };
struct hlist_head { struct hlist_node *first; };
typedef struct { int size; struct hlist_head *ht; } map_t;
struct hash_key {
int key;
struct hlist_node node;
};
map_t *map_init(int size) {
map_t *map = malloc(sizeof(map_t));
if (!map)
return NULL;
map->size = size;
map->ht = malloc(sizeof(struct hlist_head) * size);
if (map->ht) {
for (int i = 0; i < size; i++)
(map->ht)[i].first = NULL;
} else {
free(map);
map = NULL;
}
return map;
}
#define container_of(ptr, type, member) \
({ \
void *__mptr = (void *) (ptr); \
((type *) (__mptr - offsetof(type, member))); \
})
static struct hash_key *find_key(map_t *map, int key) {
int hash = key < 0 ? -key % map->size : key % map->size;
struct hlist_head *head = &(map->ht)[hash];
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;
}
void map_add(map_t *map, int key)
{
struct hash_key *kn = find_key(map, key);
if (kn)
return;
kn = malloc(sizeof(struct hash_key));
kn->key = key;
int hash = key < 0 ? -key % map->size : key % map->size;
struct hlist_head *h = &map->ht[hash];
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;
}
void map_del_node(struct hlist_node *n) {
struct hlist_node *next = n->next, **pprev = n->pprev;
*pprev = next;
if (next)
next->pprev = pprev;
n->next = NULL, n->pprev = NULL;
}
int longestConsecutive(int *nums, int n_size)
{
int length = 0;
struct hash_key *node;
map_t *map = map_init(n_size);
for (int i = 0; i < n_size; i++) {
if (!find_key(map, nums[i])) {
map_add(map, nums[i]);
}
}
for (int i = 0; i < n_size; i++) {
int len = 0;
int num;
node = find_key(map, nums[i]);
while (node) {
len++;
num = node->key;
map_del_node(&node->node);
free(node);
int left = num, right = num;
while ((node = find_key(map, --left))) {
len++;
map_del_node(&node->node);
free(node);
}
while ((node = find_key(map, ++right))) {
len++;
map_del_node(&node->node);
free(node);
}
length = len > length ? len : length;
}
}
free(map->ht);
free(map);
return length;
}
```