contributed by < `chloe0919` >
## 第 1 週測驗題
### 測驗一 : 實作非遞迴 (non-recursive; iterative) 的快速排序法
#### 運作原理
主要流程是先利用 `L` 與 `R` 決定出序列的頭尾,再以序列中的第一個節點作為 `pivot`,然後從 `pivot->next` 開始逐一走訪佇列,判斷節點和 `pivot` 之間的大小關係,而 `begin[]` 與 `end[]` 作為堆疊,用來紀錄比較的範圍
根據 `list_construct` 可以建立出以下的單向鏈串列
```graphviz
digraph list
{
graph [fontsize=12 fontname="Verdana" compound=true];
node [shape="record"];
rankdir = "LR"
subgraph cluster_a
{
label = "node_1";
node1 [label="<head>value = 4|*next|{<left> *left |<right> *right}"];
}
subgraph cluster_b
{
label = "node_2";
node2 [label="<head>value = 1|*next|{<left> *left |<right> *right}"];
}
subgraph cluster_c
{
label = "node_3";
node3 [label="<head>value = 7|*next|{<left> *left |<right> *right}"];
}
subgraph cluster_d
{
label = "node_4";
node4 [label="<head>value = 5|*next|{<left> *left |<right> *right}"];
}
NULL1[label=NULL,shape=plaintext]
node1:"*next"->node2:"*next" [lhead=cluster_b];
node2:"*next"->node3:"*next" [lhead=cluster_c];
node3:"*next"->node4:"*next" [lhead=cluster_d];
node4:"*next" -> NULL1
}
```
使用以下序列作為範例 :
```graphviz
digraph graphname{
node[shape=box]
{
rankdir = LR
A[label=4]
B[label=1]
C[label=7]
D[label=5]
NULL[label=NULL,shape=plaintext]
A->B->C->D->NULL
}
rankdir = "LR"
// L[shape=plaintext,fontcolor=red]
// R[shape=plaintext,fontcolor=red]
// 0[shape=plaintext,fontcolor=white]
// 1[shape=plaintext,fontcolor=white]
// L->0[color=red,style=dotted];
// 0->1[color=white];
// 1->R[dir=back,color=red,style=dotted];
}
```
接下來逐步解釋 `quick_sort` 的實作流程,首先以下列程式碼初始化指標 :
```c
void quick_sort(node_t **list)
{
int n = list_length(list);
int value;
int i = 0;
int max_level = 2 * n;
node_t *begin[max_level], *end[max_level];
node_t *result = NULL, *left = NULL, *right = NULL;
begin[0] = *list;
end[0] = list_tail(list);
```
假設目前 `i = 0` :
進入 `while` 將 `L` 和 `R` 指向該子序列的頭和尾,並且以 `L` 也就是序列的起始當作 `pivot` 開始進入第一次迭代
```graphviz
digraph graphname{
node[shape=box]
rankdir = LR
A[label=4]
B[label=1]
C[label=7]
D[label=5]
NULL[label=NULL,shape=plaintext]
A->B->C->D->NULL
{
rank="same";
L[label="L",shape=plaintext,fontcolor=red]
pivot[label="pivot",shape=plaintext,fontcolor=blue]
pivot->L[color=blue]
L->A[color=red];
}
{
rank="same";
R[label="R",shape=plaintext,fontcolor=red]
R->D[color=red];
}
}
```
接下來執行 `pivot->next = NULL` 打斷 `pivot` 和其他節點的鏈結,並且使用指標 `n` 執行下列程式碼
```diff
while (p) {
node_t *n = p;
- p = CCCC;
+ p = p->next;
list_add(n->value > value ? &right : &left, n);
}
```
指標 `n` 每次都會往前一步去判斷該節點和 `pivot` 之間的大小關係
```graphviz
digraph graphname{
node[shape=box]
rankdir = LR
A[label=4]
B[label=1]
C[label=7]
D[label=5]
NULL[label=NULL,shape=plaintext]
A->B[color=white,arrowsize=0.00000000000000001]
B->C->D->NULL
{
rank="same";
pivot[label="pivot",shape=plaintext,fontcolor=blue]
pivot->A[color=blue];
}
{
rank="same";
n[label="n",shape=plaintext,fontcolor=red]
n->B[color=red];
}
0[shape=plaintext,fontcolor=white]
n->0[color=red,style=dotted];
}
```
再執行 `list_add(n->value > value ? &right : &left, n)` ,將大於 `pivot` 的點放入 `right` 中,其餘放入 `left`
```graphviz
digraph graphname{
node[shape=box]
rankdir = LR
A[label=4]
B[label=1]
C[label=7]
D[label=5]
A->B[color=white,arrowsize=0.00000000000000001]
NULL1[label=NULL,shape=plaintext]
NULL2[label=NULL,shape=plaintext]
{
rank="same";
pivot[label="pivot",shape=plaintext,fontcolor=blue]
pivot->A[color=blue];
}
left[label=left,shape=plaintext]
{rank=same left->B->NULL1};
right[label=right,shape=plaintext]
{rank=same right->D->C->NULL2};
left->right[color=white]
}
```
之後將 `left` 的頭尾分別放入 `begin[0]` 和 `end[0]` 之中,`right` 的頭尾分別放入 `begin[2]` 和 `end[2]` ,而 `begin[1]` 和 `end[1]` 則是都放入 `pivot`
進入下一次迭代 `i = 2` ,因為 `node_t *L = begin[2], *R = end[2]`,代表先從堆疊的最上方,也就是上述 `right` 指到的子序列開始進行處理 :
```graphviz
digraph graphname{
node[shape=box]
rankdir = LR
C[label=7]
D[label=5]
NULL2[label=NULL,shape=plaintext]
D->C->NULL2;
{
rank="same";
L[label="L",shape=plaintext,fontcolor=red]
L->D[color=red];
}
{
rank="same";
R[label="R",shape=plaintext,fontcolor=red]
R->C[color=red];
}
}
```
因為 `L != R` 所以以第一個節點當作 `pivot`,並且和之前的流程一樣進行 `left` 和 `right` 的分類,並且把 `left` 的頭尾分別放入 `begin[2]` 和 `end[2]` 之中,`right` 的頭尾分別放入 `begin[3]` 和 `end[3]` ,而 `begin[4]` 和 `end[4]` 則是都放入 `pivot`
```graphviz
digraph graphname{
node[shape=box]
rankdir = LR
C[label=7]
D[label=5]
NULL1[label=NULL,shape=plaintext]
NULL2[label=NULL,shape=plaintext]
{
rank="same";
pivot[label="pivot",shape=plaintext,fontcolor=blue]
pivot->D[color=blue];
}
{
rank="same";
left[label="left",shape=plaintext]
left->NULL2;
}
{
rank="same";
right[label="right",shape=plaintext]
right->C->NULL1;
}
pivot->left->right [color=white]
}
```
此時因為 `L == R` 所以不會進入 `while` ,目前堆疊存放的節點情形為以下 :
```graphviz
digraph graphname{
node[shape=box]
rankdir = LR
A[label=4]
B[label=1]
C[label=7]
D[label=5]
NULL[label=NULL,shape=plaintext]
A->B->C->D->NULL[color=white,arrowsize=0.00000000000000001]
begin0[label="begin[0]",shape=plaintext, group = g2]
begin1[label="begin[1]",shape=plaintext, group = g2]
begin2[label="begin[2]",shape=plaintext, group = g2]
begin3[label="begin[3]",shape=plaintext, group = g2]
begin4[label="begin[4]",shape=plaintext, group = g2]
{rank="same"; begin0->B}
{rank="same"; begin1->A}
{rank="same"; begin3->D}
{rank="same"; begin4->C}
{rank="same"; begin2->NULL}
end0[label="end[0]",shape=plaintext, group = g1]
end1[label="end[1]",shape=plaintext, group = g1]
end2[label="end[2]",shape=plaintext, group = g1]
end3[label="end[3]",shape=plaintext, group = g1]
end4[label="end[4]",shape=plaintext, group = g1]
{rank="same"; end0->B}
{rank="same"; end1->A}
{rank="same"; end3->D}
{rank="same"; end4->C}
{rank="same"; end2->NULL}
}
```
之後執行下列程式碼從 `i = 4` 開始向下遞減,每一次都將 `L` 也就是堆疊中 `begin[i]` 以指令 `list_add` 將節點加入至 `result` 中,直到所有的堆疊都被加入完成,也就是 `i = 0` 的時候
```c
else {
if (L)
list_add(&result, L);
i--;
}
```
最後完成非遞迴的快速排序法的實作
```graphviz
digraph graphname{
node[shape=box]
rankdir = LR
A[label=1]
B[label=4]
C[label=5]
D[label=7]
NULL[label=NULL,shape=plaintext]
A->B->C->D->NULL
}
```
#### 改進方案 :
##### 針對堆疊空間 :
###### 參考筆記 :[csotaku0926](https://hackmd.io/@csotaku0926/linux2024-homework2)
在 `quick_sort()` 加入 `count_i` 計算最大的 i ,而 `count + 1` 則代表堆疊使用到的總數
```diff
left = right = NULL;
i += 2;
+ count_i = max(count_i, i);
```
我的思考方式是先考慮最差的空間使用情況,因為程式的操作為:**將大於 `pivot` 的點放入 `right` 中,其餘放入 `left`**,若每次 pivot 都是選到最小的,則會使 `left` 每次都為空,其餘資料都會放入 `right` ,因為堆疊的判斷條件是先考慮最上面的資料,也就是 `right` 是否為只剩一個點,來決定是不是繼續擴增堆疊的高度,考慮以下資料:
```graphviz
digraph graphname{
node[shape=box]
rankdir = LR
A[label=0]
B[label=2]
C[label=4]
G[label=3]
H[label=1]
NULL[label=NULL,shape=plaintext]
A->B->C->G->H->NULL
}
```
`pivot` 隨著迭代輪流順序會是 0 -> 1 -> 2 -> 3 ,總共有 4 個節點當過 `pivot` ,此時最大的 i 為 $2\times4 = 8$,所以總共需要 9 個空間
因此可以計算出最差的空間使用情形為 `int max_level = 2 * n - 1`
##### 針對 `pivot` 的選擇 避免最差情況 :
另外目前在 `pivot` 的選擇上採取的方式是**選擇第一筆的資料**,若能採用隨機採取的方式選擇 `pivot` ,能避免上述例子的情況,定義以下函式,使用 `rand_idx` 決定所選擇的 `pivot` 節點所在的索引,並且使用指標 `prev` 指向 `pivot` 的前一點,之後截斷 `prev` 和 `curr` 的鏈結,最後利用 `list_add` 將 `curr` 移到最前面
```c
void rand_p(node_t **b) {
node_t *prev = *b;
node_t *curr = prev;
int c = list_length(b);
int rand_idx = rand() % c;
while (rand_idx > 1) {
prev = prev->next;
rand_idx--;
}
if (rand_idx != 0) {
curr = prev->next;
prev->next = curr->next;
list_add(b, curr);
}
}
```
使用經過 `rand_p` 函式決定的 `pivot`,這邊要注意的是若只有兩個點則一律採取開頭的點為 `pivot` ,因為只有兩節點不管選擇哪個並不會影響效能,可以減少一次 rand_p() 函式的應用 :
```diff
while (i >= 0) {
node_t *L = begin[i], *R = end[i];
if (L != R) {
+ if (L->next != R)
+ rand_p(&L);
node_t *pivot = L;
value = pivot->value;
```
接下來使用 `lab0-c` 的 `cpucycles()` 在 `quick_sort(&list)` 的前後,看 cpucycles 的變化
```diff
+ int64_t before = cpucycles();
quick_sort(&list);
+ int64_t after = cpucycles();
```
以下分別為 count 由 10 到 100000,有無使用 `rand_p` 時實際有使用到的堆疊層數 (max lengh) 和 elasped cycles :
```
//rand_p
count : 10
max lengh : 9
elasped cycles : 8280
---------------------------------------------
count : 100
max lengh : 17
elasped cycles : 34704
---------------------------------------------
count : 1000
max lengh : 29
elasped cycles : 416376
---------------------------------------------
count : 10000
max lengh : 35
elasped cycles : 6179688
---------------------------------------------
count : 100000
max lengh : 47
elasped cycles : 119624148
```
```
//No rand_p
count : 10
max lengh : 5
elasped cycles : 10188
---------------------------------------------
count : 100
max lengh : 15
elasped cycles : 29592
---------------------------------------------
count : 1000
max lengh : 27
elasped cycles : 272448
---------------------------------------------
count : 10000
max lengh : 39
elasped cycles : 4144320
---------------------------------------------
count : 100000
max lengh : 49
elasped cycles : 73245168
```
可以看到若有選擇到好的 `pivot` ,所需要的堆疊空間也會減少,隨著 max lengh 的下降,elasped cycles 也明顯下降許多,尤其是在 count 數多的時候減少一格就會減少很多 cycles
#### 使用 Linux 核心風格的 List API :
程式碼在 [41c3f48](https://github.com/chloe0919/linux_hw2/commit/41c3f48c758914c55422cb5d9b11f09063c392d2?diff=unified&w=0) ,使用和 `lab0-c` 一樣的結構:
```graphviz
digraph G {
rankdir=LR;
node[shape=record];
subgraph cluster_b
{
label = "element_t";
node1 [label="value"];
node2 [label="{<prev> prev |<next> next}"];
}
}
```
### 測驗二 : Timsort
考慮以下資料 :
```graphviz
digraph graphname{
node[shape=box]
rankdir = LR
A[label=1]
B[label=2]
C[label=3]
D[label=5]
E[label=4]
F[label=7]
G[label=8]
NULL[label=NULL,shape=plaintext]
A->B->C->D->E->F->G->NULL
}
```
首先使用 `find_run` 找出每一個 run ,並且如果是降序排列就將此 list 反轉成升序排列,以下呈現所有 run 的都被找出來的示意圖
```graphviz
digraph graphname{
node[shape=box]
rankdir = LR
A[label=1]
B[label=2]
C[label=3]
D[label=4,color=blue]
E[label=5,color=blue]
F[label=7,color=red]
G[label=8,color=red]
NULL[label=NULL,shape=plaintext]
NULL1[label=NULL,shape=plaintext]
NULL2[label=NULL,shape=plaintext]
A->B->C
{rank = same C->NULL1}
C->D[color=white]
D->E
{rank = same E->NULL2}
E->F[color=white]
F->G
{rank = same G->NULL}
}
```
操作的流程為每次 `find_run` 後會找到一個 run , 接下來使用 `result.head` 和 `result.next` 記錄整個 run 的頭尾,並且會記錄該 run 的長度在 `head->next->prev` 當中
```graphviz
digraph graphname{
node[shape=box]
rankdir = LR
A[label=1]
B[label=2]
C[label=3]
D[label=5]
NULL[label=NULL,shape=plaintext]
A->B->C
{rank = same C->NULL}
C->D[color=white]
"result.head"[fontcolor=red,color=red];
"result.next"[fontcolor=red,color=red];
{rank=same "result.head"->A}
{rank=same "result.next"->D}
"result.head"->"result.next"[color=white]
}
```
之後使用建立一個結構`list_head` 的指標 `tp` 並且使用 `merge_collapse` 檢查 3 段 run 的長度是否滿足以下原則:
1. A 的長度要大於 B 和 C 的長度總和。
2. B 的長度要大於 C 的長度。
`merge_collapse` 的檢查流程如下 :
假設有三個 run ,長度分別為 A : 3 、B : 2 、C : 2,此時 `tp` 會記錄每個 run 的起始節點,而且每個 `tp` 指向最新個的 run
```graphviz
digraph graphname{
node[shape=box]
rankdir = LR
1[label=1,color=forestgreen]
2[label=2,color=forestgreen]
3[label=3,color=forestgreen]
4[label=4,color=dodgerblue4]
5[label=5,color=dodgerblue4]
7[label=7,color=darkgoldenrod3]
8[label=8,color=darkgoldenrod3]
A[label=A,shape=plaintext]
B[label=B,shape=plaintext]
C[label=C,shape=plaintext]
A->1->2->3
B->4->5
C->7->8
// A->B->C[color=white]
"tp"[fontcolor=red,color=red];
"tp->prev"[fontcolor=red,color=red];
"tp->prev->prev"[fontcolor=red,color=red];
"tp"->"tp->prev"->"tp->prev->prev"
{rank=same "tp"->C}
{rank=same "tp->prev"->B}
{rank=same "tp->prev->prev"->A}
}
```
當最新的兩個 run (即 B、C )相加長度大於最舊的那個 ( A ),且 C 的長度大於 A 就將 A、B 合併,否則 B、C 合併,也就是合併最小的兩個 run
```graphviz
digraph graphname{
node[shape=box]
rankdir = LR
1[label=1,color=forestgreen]
2[label=2,color=forestgreen]
3[label=3,color=forestgreen]
4[label=4,color=dodgerblue4]
5[label=5,color=dodgerblue4]
7[label=7,color=dodgerblue4]
8[label=8,color=dodgerblue4]
A[label=A,shape=plaintext]
B[label=B,shape=plaintext]
A->1->2->3
B->4->5->7->8
// A->B->C[color=white]
"tp"[fontcolor=red,color=red];
"tp->prev"[fontcolor=red,color=red];
"tp"->"tp->prev"
{rank=same "tp"->B}
{rank=same "tp->prev"->A}
}
```
這時候因為 `run_size(tp->prev) <= run_size(tp)` 所以還會再執行一次 `tp = merge_at(priv, cmp, tp)` 進行合併
```graphviz
digraph graphname{
node[shape=box]
rankdir = LR
1[label=1,color=forestgreen]
2[label=2,color=forestgreen]
3[label=3,color=forestgreen]
4[label=4,color=forestgreen]
5[label=5,color=forestgreen]
7[label=7,color=forestgreen]
8[label=8,color=forestgreen]
A[label=A,shape=plaintext]
A->1->2->3->4->5->7->8
"tp"[fontcolor=red,color=red];
{rank=same "tp"->A}
}
```
最後使用 `merge_force_collapse` 合併所有的 run 到總數 < 3,最後用 `stk0` 和 `stk1` 表示最後剩下的 run ,若只剩 1 個 run 就直接使用 `build_prev_link` 建立回原本的鏈結,若還有兩個 run 就使用 `merge_final` 合併即完成 `Timsort`
```c
/* The final merge; rebuild prev links */
struct list_head *stk0 = tp, *stk1 = stk0->prev;
while (stk1 && stk1->prev)
stk0 = stk0->prev, stk1 = stk1->prev;
if (stk_size <= 1) {
build_prev_link(head, head, stk0);
return;
}
merge_final(priv, cmp, head, stk1, stk0);
```
## 第 2 週測驗題
### 測驗一 : dfs
根據 [Linux 核心的 hash table 實作](https://hackmd.io/rpIi8FeqQ9eBJ-UTFgb--A?view) `hlist_node` 用以處理 hash 數值碰撞,其中 `pprev` 指向**指著自身節點的指標**
```c
struct hlist_node {
struct hlist_node *next, **pprev;
};
```
#### hlist_add_head
操作流程如下 :
1. 先將 `n->next` 指向原始未插入節點前的第一個點,也就是 `h->first`
2. 再將 `n->pprev` 指向 `&h->first`
3. 最後再將 n 點插入到 `h->first`
```graphviz
digraph G {
rankdir = LR;
splines = false;
node[shape = "record"]
list_head[label = "<m>list_head | <n>first"]
node_1[label = "<m>n | {<p>pprev | <n>next}", group = list];
node_2[label = "<m>hlist_node_1 | {<p>pprev | <n>next}", group = list];
node_3[label = "<m>hlist_node_2 | {<p>pprev | <n>next}", group = list];
NULL[shape = plaintext, label = "NULL", group = list]
{rank = "min" list_head}
list_head -> node_1 -> node_2 -> node_3[
weight = 10, style = invis
]
list_head:n -> node_1:m[color=red,label="3",fontcolor=red];
node_1:p -> list_head:n[color=red,label="2",fontcolor=red];
node_1:n -> node_2:m[color=red,label="1",fontcolor=red];
node_2:p -> node_1:n;
node_2:n -> node_3:m;
node_3:p -> node_2:n;
node_3:n -> NULL;
}
```
#### find
```c
static int find(int num, int size, const struct hlist_head *heads)
{
struct hlist_node *p;
int hash = (num < 0 ? -num : num) % size;
hlist_for_each (p, &heads[hash]) {
struct order_node *on = container_of(p, struct order_node, node);
if (num == on->val)
return on->idx;
}
return -1;
}
```
首先計算出雜湊值 hash,再執行 `hlist_for_each` 根據 hash 找到對應的鏈結串列串列,並且使用 `container_of` 算出記憶體位置,以 `if` 判斷是否為正在尋找的數字
#### dfs
```c
static struct TreeNode *dfs(int *preorder,
int pre_low,
int pre_high,
int *inorder,
int in_low,
int in_high,
struct hlist_head *in_heads,
int size)
{
if (in_low > in_high || pre_low > pre_high)
return NULL;
struct TreeNode *tn = malloc(sizeof(*tn));
tn->val = preorder[pre_low];
int idx = find(preorder[pre_low], size, in_heads);
tn->left = dfs(preorder, pre_low + 1, pre_low + (idx - in_low), inorder,
in_low, idx - 1, in_heads, size);
tn->right = dfs(preorder, pre_high - (in_high - idx - 1), pre_high, inorder,
idx + 1, in_high, in_heads, size);
return tn;
}
```
以 `tn->val` 記錄前序的第一筆資料作為樹的根節點,並使用 `find` 找到對應在中序的索引,在各以 `tn->left` 和 `tn->right` 去遞迴尋找左子樹和右子樹
左右子樹各自需考慮的 `pre_low`、`pre_high`、`in_low`、`in_high` 為 :
* 左子樹 :
* `pre_low` : 為原本的 `pre_low` $+1$ ( 從根節點下一點開始 )
* `pre_high` : 先算出 `idx - in_low` 代表根節點左邊的元素個數,之後 `pre_low + (idx - in_low)` 為前序考慮的最高索引
* `in_low` : 原本的最低索引
* `in_high` : 根節點的左邊
```graphviz
digraph graphname{
node[shape=box]
rankdir = LR
i3[label=3,shape=circle,color=red]
i9[label=9,shape=circle]
i20[label=20,shape=circle]
i15[label=15,shape=circle]
i7[label=7,shape=circle]
inorder->i9->i3->i15->i20->i7;
preorder[label=preorder,shape=plaintext]
inorder[label=inorder,shape=plaintext]
p3[label=3,shape=circle,color=red]
p9[label=9,shape=circle]
p20[label=20,shape=circle]
p15[label=15,shape=circle]
p7[label=7,shape=circle]
preorder->p3->p9->p20->p15->p7;
pre_low[label=pre_low,shape=plaintext,fontcolor=deepskyblue2]
{rank = same pre_low->p9}
pre_high[label=pre_high,shape=plaintext,fontcolor=deepskyblue2]
{rank = same pre_high->p9}
in_low[label=in_low,shape=plaintext,fontcolor=deepskyblue2]
{rank = same in_low->i9}
in_high[label=in_high,shape=plaintext,fontcolor=deepskyblue2]
{rank = same in_high->i9}
}
```
* 右子樹 :
* `pre_low` : 一樣先算出 `in_high - idx` 代表根節點右邊元素個數,再以 `pre_high - (in_high - idx) + 1` 算出最前序考慮的最低範圍的索引
* `pre_high` : 原本的最高索引
* `in_low` : 根節點的右邊
* `in_high` : 原本的最高索引
```graphviz
digraph graphname{
node[shape=box]
rankdir = LR
i3[label=3,shape=circle,color=red]
i9[label=9,shape=circle]
i20[label=20,shape=circle]
i15[label=15,shape=circle]
i7[label=7,shape=circle]
inorder->i9->i3->i15->i20->i7;
preorder[label=preorder,shape=plaintext]
inorder[label=inorder,shape=plaintext]
p3[label=3,shape=circle,color=red]
p9[label=9,shape=circle]
p20[label=20,shape=circle]
p15[label=15,shape=circle]
p7[label=7,shape=circle]
preorder->p3->p9->p20->p15->p7;
pre_low[label=pre_low,shape=plaintext,fontcolor=deepskyblue2]
{rank = same pre_low->p20}
pre_high[label=pre_high,shape=plaintext,fontcolor=deepskyblue2]
{rank = same pre_high->p7}
in_low[label=in_low,shape=plaintext,fontcolor=deepskyblue2]
{rank = same in_low->i15}
in_high[label=in_high,shape=plaintext,fontcolor=deepskyblue2]
{rank = same in_high->i7}
}
```
### 測驗二 : LRU
#### 資料結構 :
使用 `LRUCache` 儲存 LRUCache 的結構
* `capacity` : 快取的容量大小
* `count` : 目前存放的數量
* `dhead` : 串接所有資料的 `list_head`
* `hhead` : 處理碰撞的 `hlist_head`
```c
typedef struct {
int capacity;
int count;
struct list_head dhead;
struct hlist_head hhead[];
} LRUCache;
```
示意圖 :
```graphviz
digraph G {
rankdir=LR;
node[shape=record];
subgraph cluster_a
{
label = "LRUCache";
node1 [label="<head>capacity|count|{<prev> dhead |<next> hhead[]}"];
}
map [label="<head>hlist_head.first |<ht0> |<ht1> |<ht2> |<ht3> |<ht4> "];
node[shape=none]
null1 [label=NULL]
null2 [label=NULL]
subgraph cluster_1 {
style=filled;
color=lightgrey;
node [shape=record];
hn1 [label="hlist_node | {<prev>pprev | <next>next}"];
label="hash_key 1"
}
subgraph cluster_2 {
style=filled;
color=lightgrey;
node [shape=record];
hn2 [label="hlist_node | {<prev>pprev | <next>next}"];
label="hash_key 2"
}
subgraph cluster_3 {
style=filled;
color=lightgrey;
node [shape=record];
hn3 [label="hlist_node | {<prev>pprev | <next>next}"];
label="hash_key 3"
}
map:ht0 -> hn1
hn1:next -> hn2
hn2:next -> null1
hn2:prev:s -> hn1:next:s
map:ht4 -> hn3
hn3:next -> null2
hn1:prev:s -> map:ht0
hn3:prev:s -> map:ht4
node1:"next"->map:head;
}
```
使用 `LRUNode` 儲存節點的結構
* `node` : 用來儲存碰撞的串列元素
* `link` : 記錄在 `dhead` 中的位置
```c
typedef struct {
int key;
int value;
struct hlist_node node;
struct list_head link;
} LRUNode;
```
#### lRUCacheGet
首先使用 key 計算出所對應的雜湊值,然後用 `hlist_for_each` 逐一走訪 `obj->hhead[hash]` 所有元素並且以 `cache` 記錄每個元素 link 類型指向的結構體,當尋找到所對應 key 值節點後,需要將 `&cache->link` 也就是找到節點的串接往前挪移,更新該節點的使用情形,代表他才剛被使用過,因此在 `dhead` 當中存放的位置也就是節點的更新順序,越後面的節點代表越久沒有被使用到的節點
```c
if (cache->key == key) {
list_move(&cache->link, &obj->dhead);
return cache->value;
}
```
#### lRUCachePut
一開始和 `lRUCacheGet` 的操作類似,只不過是先去尋找 cache 中是否已經有存在相同 key 的節點了,如果有的話一樣要更新節點在 `dhead` 的順序,假設現在要尋找 key 3 ,而且已經存在在 hash table 中,所以需要將 `dhead` 中 hey 3 的順序往前挪動 :
```graphviz
digraph G {
rankdir=LR;
node[shape=record];
subgraph cluster_a
{
label = "LRUCache";
node1 [label="<head>capacity|count|{<prev> dhead |<next> hhead[]}"];
}
map [label="<head>hlist_head.first |<ht0> |<ht1> |<ht2> |<ht3> |<ht4> "];
node[shape=none]
null1 [label=NULL]
null2 [label=NULL]
subgraph cluster_1 {
style=filled;
color=lightgrey;
node [shape=record];
hn1 [label="hlist_node | {<prev>pprev | <next>next}"];
label="key 1"
}
subgraph cluster_2 {
style=filled;
color=lightgrey;
node [shape=record];
hn2 [label="hlist_node | {<prev>pprev | <next>next}"];
label="key 2"
}
subgraph cluster_3 {
style=filled;
color=lightgrey;
node [shape=record];
hn3 [label="hlist_node | {<prev>pprev | <next>next}"];
label="key 3"
}
map:ht0 -> hn1
hn1:next -> hn2
hn2:next -> null1
hn2:prev:s -> hn1:next:s
map:ht4 -> hn3
hn3:next -> null2
hn1:prev:s -> map:ht0
hn3:prev:s -> map:ht4
node1:"next"->map:head;
node [shape=record];
dh1 [label="<h>key1 | {<prev>pprev | <next>next}"];
dh2 [label="<h>key2 | {<prev>pprev | <next>next}"];
dh3 [label="<h>key3 | {<prev>pprev | <next>next}",color=red];
// {rank = "min" list_head}
dh1 -> dh2 -> dh3[
weight = 10, style = invis
]
dh1:next -> dh2[dir=both];
dh2:next -> dh3[dir=both];
dh3:s->dh1:sw[color=red];
node1:"prev"->dh1
}
```
如果 cache 為空,代表該 key 並不存在在 cache 中,此時會有下列兩種情況 :
1. **cache 容量使用==已達到==限制**,需要使用 `list_last_entry` 尋找 `dhead` 中最後面的節點 ( 代表最不常被使用到 ),並且使用 `list_move` 移到最前面後再將他移除,最後使用 `hlist_add_head` 將要加入的節點加入到最前面,原因是該節點才剛被加入,所以是最近最常被使用到的
```c
if (obj->count == obj->capacity) {
cache = list_last_entry(&obj->dhead, LRUNode, link);
list_move(&cache->link, &obj->dhead);
hlist_del(&cache->node);
hlist_add_head(&cache->node, &obj->hhead[hash]);
}
```
2. **cache 容量使用==未達==限制**,可以直接加入,使用 `malloc` 建立一個新的記憶體空間並且分別使用 `hlist_add_head` 和 `list_add` 將 cache 放入到對應 hash 的 head 以及 `dhead` 最前面
```c
else {
cache = malloc(sizeof(LRUNode));
hlist_add_head(&cache->node, &obj->hhead[hash]);
list_add(&cache->link, &obj->dhead);
obj->count++;
}
```
最後兩種情況分別處理完成後,還需要將 key 和 value 分別放入 `cache->key` 和 `cache->value` 中
### 測驗三 : 在指定的記憶體空間找出第 N 個設定的位元
此測驗的內建巨集和函式比較多,所以首先要了解以下的巨集 :
#### small_const_nbits
定義如下,操作方法為使用 `__builtin_constant_p` 檢查 `nbits` 是否在編譯時是否為常數,另外還有 `nbits` 是否介於 0 ~ BITS_PER_LONG 之間
```c
#define small_const_nbits(nbits) \
(__builtin_constant_p(nbits) && (nbits) <= BITS_PER_LONG && (nbits) > 0)
```
`GENMASK(h, l)` 作用是依據給定的範圍 $(h,l)$,產生連續的 bitmask ,所以 `GENMASK(size - 1, 0)` 的結果會是右邊 `size` 個 bit 為 1,其餘的位元都設成 0
#### __ffs
目的是找到 word **從右邊數來的第一個 set bit 的位置**,首先會先計算出 `word & 0xffffffff` 並且判斷是否為 0,這個動作是為了從高位元開始尋找是否有位於向右數來第 32 位的 bit
特別要注意的是要先以 `#if BITS_PER_LONG == 64` 判斷是否是 64 位元架構,之後一樣由高往低位元判斷
```c
if ((word & 0xffffffff) == 0) {
num += 32;
word >>= 32;
}
```
#### BIT_MASK
獲得只有第 nr 個 bit 為 1 的 mask
```c
#define BIT_MASK(nr) (1UL << ((nr) % BITS_PER_LONG))
```
#### BIT_WORD
計算 nr 在第幾個 word
```c
#define BIT_WORD(nr) ((nr) / BITS_PER_LONG)
```
#### __clear_bit
再來嘗試理解 `__clear_bit` 的操作,首先使用 `BIT_MASK` 設定一個 mask而且只有第 nr 個 bit 為 1,再來定義一個指標 p 並使用 `BIT_WORD` 去計算 nr 是屬於第幾個 word 且將 p 指向 addr 中 nr 所屬的 word,取 ~mask 是為了獲得只有第 nr 為 0 其他為 1 的遮罩,最後執行 `*p &= ~mask` 就能將 addr 中,第 nr 個 bit 清除成 0
```c
static inline void __clear_bit(unsigned long nr, volatile unsigned long *addr)
{
unsigned long mask = BIT_MASK(nr);
unsigned long *p = ((unsigned long *) addr) + BIT_WORD(nr);
*p &= ~mask;
}
```
理解 `__ffs` 和 `__clear_bit` 後可以嘗試追蹤一次 `fns` 的操作
###### 參考筆記 : [HenryChaing](https://hackmd.io/@Henry0922/linux2024-homework2#%E6%B8%AC%E9%A9%97%E4%B8%89%E4%BD%9C%E7%AD%94%E5%8D%80)
```
假設 nr 為 4
word = 10010111
------iteration 1-------------------
bit = 0
n = 3
__clear_bit(0, &word) -> 10010110
------iteration 2-------------------
bit = 1
n = 2
__clear_bit(1, &word) -> 10010100
------iteration 3-------------------
bit = 2
n = 1
__clear_bit(0, &word) -> 10010000
------iteration 4-------------------
bit = 4
n = 0
return 4
```