contributed by <`Shiang1212`>
## [第一週測驗題](https://hackmd.io/@sysprog/linux2024-quiz1)
### 測驗 `1`
#### 先輩知識
結構體與函式介紹:
```C
typedef struct __node {
struct __node *left, *right; // Doubly linked list
struct __node *next;
long value;
} node_t;
```
* `void list_add(node_t **list, node_t *node_t)`
將 `node_t` 加入至 `list` 的前端。
* `node_t *list_tail(node_t \*\*left)`
回傳一個指向 left 最末節點的指標。
* `int list_length(node_t \*\*left)`
回傳輸入串列的長度。
* `node_t *list_construct(node_t *list, int n)`
建立一個 `value` 為 `n` 的節點,並將其 `next` 指向 `list`。
* `void list_free(node_t \*\*list)`
走訪每個節點,並將釋放其記憶體,直到 `list` 內沒有結點。
* `void shuffle(int *array, size_t n)`
隨機交換 `array` 裡的值,完成亂序。
其中,`shuffle` 的程式碼如下:
```C=
void shuffle(int *array, size_t n)
{
if (n <= 0)
return;
for (size_t i = 0; i < n - 1; i++) {
size_t j = i + rand() / (RAND_MAX / (n - i) + 1);
int t = array[j];
array[j] = array[i];
array[i] = t;
}
}
```
第 6、7 行可見,宣告一個 `size_t i` 走訪每個元素,並用第 `i` 個元素跟第 `i ~ n` 個元素交換,完成亂序操作。
介紹完基本操作,接下來介紹 `main` 函式在做什麼:
```c
int main(int argc, char **argv)
{
node_t *list = NULL;
size_t count = 100000;
int *test_arr = malloc(sizeof(int) * count);
for (int i = 0; i < count; ++i)
test_arr[i] = i;
shuffle(test_arr, count);
while (count--)
list = list_construct(list, test_arr[count]);
quick_sort(&list);
assert(list_is_ordered(list));
list_free(&list);
free(test_arr);
return;
}
```
建立一個陣列 `test_arr`,大小為 100000,內容為 0, 1, 2, ..., 100000, `shuffle` 對其進行亂序操作後,用 `list_construct` 建立一個亂序內容的鏈結串列,最後使用 `quick_sort`,對該鏈結串列進行排序。
#### 如何實作 quick_sort
此處為用於排序鏈結串列的非遞迴 (non-recursive; iterative) 快速排序法 (QuickSort),以下為程式碼:
```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);
while (i >= 0) {
node_t *L = begin[i], *R = end[i];
if (L != R) {
node_t *pivot = L;
value = pivot->value;
node_t *p = pivot->next;
pivot->next = NULL;
while (p) {
node_t *n = p;
p = p->next;
list_add(n->value > value ? &right : &left, n);
}
begin[i] = left;
end[i] = &list_tail(left);
begin[i + 1] = pivot;
end[i + 1] = pivot;
begin[i + 2] = right;
end[i + 2] = &list_tail(right);
left = right = NULL;
i += 2;
} else {
if (L)
list_add(&result, L);
i--;
}
}
*list = result;
}
```
1. 挑選第一個節點作為 pivot,並將 `list` 的首尾節點分別指派給 `begin[0]` 和 `end[0]`。
(此圖修改自 [vestata](https://hackmd.io/@tony268596/linux2024-homework2))
```graphviz
digraph S{
rankdir=LR
node[shape=box]
head[color=white, fontcolor=red]
null[label="NULL", color=white]
4->1->3->5->2->null
head->4{rank=same;head;4}
}
```
(此圖修改於 [YangYeh-PD](https://hackmd.io/-iW9qu9MQYuaky39rwBi3A#Problem-1-Quicksort-Non-recursive))
```graphviz
digraph "Linked List" {
node3[shape = circle, label = "3"];
node5[shape = circle, label = "5"];
node4[shape = circle, label = "4"];
node1[shape = circle, label = "1"];
node2[shape = circle, label = "2"];
begin [shape = none, label=<
<TABLE BORDER="1" CELLBORDER="1" CELLSPACING="0"> <TR>
<TD PORT="a0"> begin[0]</TD>
</TR> </TABLE>>];
end[shape = none, label=<
<TABLE BORDER="1" CELLBORDER="1" CELLSPACING="0"> <TR>
<TD PORT="a0"> end[0]</TD>
</TR> </TABLE>>];
end:a0 -> 2;
begin:a0 -> node4 -> node1-> node3 -> node5 -> node2;
}
```
3. 走訪每個節點,將小於 pivot 的節點存進 `left`,大於 pivot 的節點存進 `right`。
4. `begin[i]` 存放 `left`,`begin[i + 1]` 存放 pivot,`begin[i + 2]` 存放 `right`。`end[i]` ~ `end[i + 2]` 存放 `begin[i]` ~ `begin[i + 2]` 的末端節點。
```graphviz
digraph "Linked List" {
node3[shape = circle, label = "3"];
node5[shape = circle, label = "5"];
node4[shape = circle, label = "4"];
node1[shape = circle, label = "1"];
node2[shape = circle, label = "2"];
begin [shape = none, label=<
<TABLE BORDER="1" CELLBORDER="1" CELLSPACING="0"> <TR>
<TD PORT="a0"> begin[0]</TD>
<TD PORT="a1"> begin[1]</TD>
<TD PORT="a2"> begin[2]</TD>
</TR> </TABLE>>];
end[shape = none, label=<
<TABLE BORDER="1" CELLBORDER="1" CELLSPACING="0"> <TR>
<TD PORT="a0"> end[0]</TD>
<TD PORT="a1"> end[1]</TD>
<TD PORT="a2"> end[2]</TD>
</TR> </TABLE>>];
end:a0 -> 1;
end:a1 -> 3;
end:a2 -> 5;
begin:a0 -> node2 -> node1;
begin:a1 -> node3;
begin:a2 -> node4 -> node5;
}
```
5. 從 `begin[i + 2]` 開始,也就是 `begin[]` 最末端的串列,繼續找 pivot 切割成 `left` 和 `right`。
```graphviz
digraph "Linked List" {
node3[shape = circle, label = "3"];
node5[shape = circle, label = "5"];
node4[shape = circle, label = "4"];
node1[shape = circle, label = "1"];
node2[shape = circle, label = "2"];
null[shape = none, label = "Null"];
begin [shape = none, label=<
<TABLE BORDER="1" CELLBORDER="1" CELLSPACING="0"> <TR>
<TD PORT="a0"> begin[0]</TD>
<TD PORT="a1"> begin[1]</TD>
<TD PORT="a2"> begin[2]</TD>
<TD PORT="a3"> begin[3]</TD>
<TD PORT="a4"> begin[4]</TD>
</TR> </TABLE>>];
begin:a0 -> node2 -> node1;
begin:a1 -> node3;
begin:a2 -> null;
begin:a3 -> node4;
begin:a4 -> node5;
}
```
6. 當 `begin[i]` == `end[i]` 時,代表串列已經無法再切割,就可以把該節點加入 `result`,當作排序完成的節點。
```graphviz
digraph "Linked List" {
node1[shape = circle, label = "1"];
node2[shape = circle, label = "2"];
begin [shape = none, label=<
<TABLE BORDER="1" CELLBORDER="1" CELLSPACING="0"> <TR>
<TD PORT="a0"> begin[0]</TD>
</TR> </TABLE>>];
begin:a0 -> node2 -> node1;
}
```
```graphviz
digraph S{
rankdir=RL
node[shape=box]
result[color=white, fontcolor=red]
null[label="NULL", color=white]
5->4->3->null
result->5
}
```
:::warning
問題
1. why max_level = 2 * n
:::
### 測驗 `2`
> 待完成
---
## [第二週測驗題](https://hackmd.io/@sysprog/linux2024-quiz2)
### 測驗 `1`
用 preoreder 和 inorder 的排序,要求回傳重建的二元樹。
#### Hash Table
使用雙向鏈結串列處理 Hash Table 發生碰撞 (collision) 的情況。
```c
struct hlist_head {
struct hlist_node *first;
}
```
```c
struct hlist_node {
struct hlist_node *next, **pprev;
}
```
```graphviz
digraph G {
rankdir=LR;
node[shape=record];
map [label="hlist_head.first |<ht0> |<ht1> |<ht2> |<ht3> |<ht4> |<ht5> |<ht7> |<ht8> "];
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];
hn3 [label="hlist_node | {<prev>pprev | <next>next}"];
label="hash_key 2"
}
map:ht1 -> hn1
hn1:next -> null1
map:ht5 -> hn3
hn3:next -> null2
hn1:prev:s -> map:ht1
hn3:prev:s -> map:ht5
}
```
參考 [Linux 核心的 hash table 實作](https://hackmd.io/rpIi8FeqQ9eBJ-UTFgb--A)
`next` 指向相鄰的**節點本身**,而 `pprev` 指向**指著自身節點的指標**。
+ `hlist_add_head`
```c
static inline void hlist_add_head(struct hlist_node *n, struct hlist_head *h)
{
if (h->first)
h->first->pprev = &n->next;
n->next = h->first;
n->pprev = &h->first;
h->first = n;
}
```
這個函式執行 hash table 中,將 `n` 插入雙向鏈結串列。將 `hlist_node n` 插入於 `hlist_head h` 的前端。
```graphviz
digraph G {
rankdir=LR;
node[shape=record];
map [label="hlist_head.first |<ht0> |<ht1> |<ht2> |<ht3> |<ht4> |<ht5> |<ht7> |<ht8> "];
node[shape=none]
null1 [label=NULL]
null2 [label=NULL]
subgraph cluster_1 {
style=filled;
color=lightgrey;
node [shape=record, color = red];
hn1 [label="hlist_node | {<prev>pprev | <next>next}"];
label="hash_key new"
}
subgraph cluster_2 {
style=filled;
color=lightgrey;
node [shape=record];
hn2 [label="hlist_node | {<prev>pprev | <next>next}"];
label="hash_key 1"
}
subgraph cluster_3 {
style=filled;
color=lightgrey;
node [shape=record];
hn3 [label="hlist_node | {<prev>pprev | <next>next}"];
label="hash_key 2"
}
map:ht1 -> hn1[color = red]
hn1:next -> hn2[color = red]
hn2:next -> null1
hn2:prev:s -> hn1:next:s[color = red]
map:ht5 -> hn3
hn3:next -> null2
hn1:prev:s -> map:ht1[color = red]
hn3:prev:s -> map:ht5
}
```
#### TreeNode、order_node
```c
struct TreeNode {
int val;
struct TreeNode *left, *right;
}
```
```c
struct order_node {
struct hlist_node node;
int val;
int idx;
}
```
#### 建樹以及相關操作
+ `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 = list_entry(p, struct order_node, node);
if (num == on->val)
return on->idx;
}
return -1;
}
```
用傳入的 `num` 值尋找 hash table 中的位置索引。宣告 `hash` 得知該節點放於 hash table 中的哪個槽,用 `hlist_for_each` 走訪每個節點,尋找節點並回傳索引。
+ 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;
}
```
> 待完成
+ `node_add`
```c
static inline void node_add(int val,
int idx,
int size,
struct hlist_head *heads)
{
struct order_node *on = malloc(sizeof(*on));
on->val = val;
on->idx = idx;
int hash = (val < 0 ? -val : val) % size;
hlist_add_head(&on->node, &head[hash]);
}
```
這裡執行的是建立一個 node 並將其存放至 hash table。建立一個新節點,宣告 `hash` 決定該節點要存放於 hash table 的哪個槽裡,最後使用 `hlist_add_head` 將該節點加入 hash table
### 測驗 `2`
> 待完成
### 測驗 `3`
實作 `find_nth_bit` 可在指定的記憶體空間找出第 N 個設定的位元。
+ `__ffs`
find first bit set in a word
```c
static inline unsigned long __ffs(unsigned long word)
{
int num = 0;
#if BITS_PER_LONG == 64
if ((word & 0xffffffff) == 0) {
num += 32;
word >>= 32;
}
#endif
if ((word & 0xffff) == 0) {
num += 16;
word >>= 16;
}
if ((word & 0xff) == 0) {
num += 8;
word >>= 8;
}
if ((word & 0xf) == 0) {
num += 4;
word >>= 4;
}
if ((word & 0x3) == 0) {
num += 2;
word >>= 2;
}
if ((word & 0x1) == 0)
num += 1;
return num;
}
```
依循 binary search 的概念,用 mask 做 bit-wise and 找出首個 bit 為 1 的位置 (由右至左),有了這個函式就可以往下繼續做 fns。
+ `fns`
find N'th bit set in a word
```c
static inline unsigned long fns(unsigned long word, unsigned int n)
{
while (word) {
unsigned int bit = __ffs(word);
if (n-- == 0)
return bit;
__clear_bit(bit, &word);
}
return BITS_PER_LONG;
}
```
使用 `__fns` 從 LSB 找出首個 bit 為 1 的位置,並判斷是否為該 word 中第 `n` 個 1 (由右至左),若不是的話就把該 bit 清除掉,用 `__fns`繼續找下一個 bit 為 1 的位置。
+ `__clear_bit`
clear certain bit
```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;
}
```
功能:將 `addr` 的第 `nr` 個 bit 改成 0。
```
清除第 4 個 bit
p = 0111 1011
mask = 0000 1000
~mask = 1111 0111
p & ~mask = 0111 0011
```
其中的 `BIT_MASK(nr)` 和 `BIT_WORD(nr)` 巨集程式碼如下:
```c
#define BIT_MASK(nr) (1UL << ((nr) % BITS_PER_LONG))
```
`1UL` 代表無號長整數 1,也就是
0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 000**1**
`1UL << ((nr) % BITS_PER_LONG)` 將 `1UL` 向左位移 `(nr) % BITS_PER_LONG` 個 bit。
例如:`BIT_MASK(5)` 的結果
0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 00**10 0000**
```c
#define BIT_WORD(nr) ((nr) / BITS_PER_LONG)
```
計算 word 的 offset。
:::warning
不知道第 4 行為什麼要加 `BIT_WORD(nr)`,如果想要清除大於 64 的 bit,那不就代表要清除到該 word 以外的 bit 嗎?為什麼要允許這樣的行為?
:::
+ `small_const_nbits(size)`
判定 `size` 是否為正常的常數且長度大於 0、小於 BIT_PER_LONG (64)。
```c
#define small_const_nbits(nbits) \
(__builtin_constant_p(nbits) && (nbits) <= BITS_PER_LONG && (nbits) > 0)
```
+ `GENMASK(h, l)`
產生一個第 `l+1` 到 `h+1` 個 bit 皆為 1,其餘為 0 的遮罩。
```c
#define GENMASK(h, l) \
(((~0UL) - (1UL << (l)) + 1) & (~0UL >> (BITS_PER_LONG - 1 - (h))))
```
用一個例子快速了解 `GENMASK` 在幹嘛:
```
BIT_PER_LONG = 8
GENMASK(5, 2):
left : 1111 1111 - (0000 0001 << 2) + 1
=1111 1111 - 0000 0100 + 1
=1111 1100
right : 1111 1111 >> (8 - 1 - 5)
=0011 1111
left & right : 0011 1100
```
+ `FIND_NTH_BIT(FETCH, size, num)`
用來在指定的 bitmap 中,找尋第 n 個 set bit的 index。
```c
#define FIND_NTH_BIT(FETCH, size, num)
({
unsigned long sz = (size), nr = (num), idx, w, tmp;
for (idx = 0; (idx + 1) * BITS_PER_LONG <= sz; idx++) {
if (idx * BITS_PER_LONG + nr >= sz)
goto out;
tmp = (FETCH);
w = hweight_long(tmp);
if (w > nr)
goto found;
nr -= w;
}
if (sz % BITS_PER_LONG)
tmp = (FETCH) & BITMAP_LAST_WORD_MASK(sz);
found:
sz = min(idx * BITS_PER_LONG + fns(tmp, nr), sz);
out:
sz;
})
```
#### `find_nth_bit`
find N'th set bit in a memory region
```c=
static inline unsigned long find_nth_bit(const unsigned long *addr,
unsigned long size,
unsigned long n)
{
if (n >= size)
return size;
if (small_const_nbits(size)) {
unsigned long val = *addr & GENMASK(size - 1, 0);
return val ? fns(val, n) : size;
}
return FIND_NTH_BIT(addr[idx], size, n);
}
```
輸入參數:
+ addr:欲搜尋的起始記憶體位置
+ size:最大搜尋範圍 (bit)
+ n:欲查找的設定位元個數 (set bit)
接下來讓我們逐行檢視:
第 3~4 行:若欲查找的設定位元數超出最大搜尋範圍,則判定為尋找失敗,回傳 `size`。
第 8~15 行:透過 `small_const_nbits(size)` 檢查 `size` 是否為正常的常數且長度大於 0、小於 BIT_PER_LONG (64)。
+ 若成立,代表要尋找的 index 就落在該字節內。
+ 用 `GENMASK()` 設定一個遮罩,找出要處理的範圍。然後將遮罩後的值傳進 `fns()` 來找第 n 個被設置的位元的索引位置。
+ 若不成立,則代表要尋找的 index 就不在該字節內。
+ 呼叫 `FIND_NTH_BIT()`,來處理跨字節邊界的情況。
> 待完成