# 2024q1 Homework2 (quiz1+2)
contributed by <[marvin0102](https://github.com/marvin0102/lab0-c)>
# 第一周測驗
## 測驗 1:
### 結構體 `node_t`:
```c
typedef struct __node {
struct __node *left, *right;
struct __node *next;
long value;
} node_t;
```
```graphviz
digraph "__node"
{
node [shape="record"];
rankdir = "LR"
subgraph cluster_a
{
label = "node_t";
node1 [label="<head>value|next|{<left> left |<right> right}"];
}
}
```
此結構體作為陣列的單一節點 :
`*left` 、 `*right`的作用是當作為 pivot 時,分別記錄與比較比 pivot 大或小的節點。
`*next` 則是作為 linked list 的指標,指向下一個元素。
`value` 為該節點的值。
### 操作函式:
我們可以只用以下這些 API 來操作與建構陣列。
- `void list_add(node_t **list, node_t *node_t)`
- `node_t *list_tail(node_t **left)`
- `int list_length(node_t **left)`
- `node_t *list_construct(node_t *list, int n)`
- `void list_free(node_t **list)`
### 解題思路:
以知原始程式為非遞迴快速排序( quick sort )的實作。
```c
while (count--)
list = list_construct(list, test_arr[count]);
```
從函式`list_construct()`以及 main function 建構初始陣列的程式碼可知,此陣列為單向鏈接串列(linked list)。
```graphviz
digraph list
{
graph [fontsize=12 fontname="Verdana" compound=true];
node [shape="record"];
rankdir = "LR"
"*head";
subgraph cluster_a
{
label = "node_1";
node1 [label="<head>value|*next|{<left> *left |<right> *right}"];
}
subgraph cluster_b
{
label = "node_2";
node2 [label="<head>value|*next|{<left> *left |<right> *right}"];
}
subgraph cluster_c
{
label = "node_3";
node3 [label="<head>value|*next|{<left> *left |<right> *right}"];
}
"*head"->node1:value [lhead=cluster_a];
node1:"*next"->node2:"*next" [lhead=cluster_b];
node2:"*next"->node3:"*next" [lhead=cluster_c];
}
```
---
函式`quick_sort()` 中的參數`list`為指標的指標,其指向的位置為指向串列第一個節點的指標 head 之地址。
```graphviz
digraph list
{
graph [fontsize=12 fontname="Verdana" compound=true];
node [shape="record"];
rankdir = "LR"
"**list"[fontcolor=red ,color=red];
"*head";
subgraph cluster_a
{
label = "node_1";
node1 [label="<head>value|*next|{<left> *left |<right> *right}"];
}
subgraph cluster_b
{
label = "node_2";
node2 [label="<head>value|*next|{<left> *left |<right> *right}"];
}
subgraph cluster_c
{
label = "node_3";
node3 [label="<head>value|*next|{<left> *left |<right> *right}"];
}
"**list"->"*head" [color=red];
"*head"->node1:value [lhead=cluster_a];
node1:"*next"->node2:"*next" [lhead=cluster_b];
node2:"*next"->node3:"*next" [lhead=cluster_c];
}
```
```c
int list_length(node_t **left)
{
int n = 0;
while (*left) {
++n;
left = &(BBBB);
}
return n;
}
```
函式`list_length()`回傳的是一串列的長度,因此需要逐一走訪整個串列聘記錄其節點的個數,而`node_t **left`為指標的指標,因此,每一次迴圈,需要將`left`的值更新為指向下一個節點的指標之地址,也就是指標`next`的地址。
```diff
- left = &(BBBB);
+ left = &((*left)->next);
```
loop 0
```graphviz
digraph list
{
graph [fontsize=12 fontname="Verdana" compound=true];
node [shape="record"];
rankdir = "LR"
"**left"[fontcolor=red ,color=red];
"*head";
subgraph cluster_a
{
label = "node_1";
node1 [label="<head>value|*next|{<left> *left |<right> *right}"];
}
subgraph cluster_b
{
label = "node_2";
node2 [label="<head>value|*next|{<left> *left |<right> *right}"];
}
subgraph cluster_c
{
label = "node_3";
node3 [label="<head>value|*next|{<left> *left |<right> *right}"];
}
"**left"->"*head" [color=red];
"*head"->node1:value [lhead=cluster_a];
node1:"*next"->node2:"*next" [lhead=cluster_b];
node2:"*next"->node3:"*next" [lhead=cluster_c];
}
```
loop 1
```graphviz
digraph list
{
graph [fontsize=12 fontname="Verdana" compound=true];
node [shape="record"];
rankdir = "LR"
"**left"[fontcolor=red ,color=red];
"*head";
subgraph cluster_a
{
label = "node_1";
node1 [label="<head>value|*next|{<left> *left |<right> *right}"];
}
subgraph cluster_b
{
label = "node_2";
node2 [label="<head>value|*next|{<left> *left |<right> *right}"];
}
subgraph cluster_c
{
label = "node_3";
node3 [label="<head>value|*next|{<left> *left |<right> *right}"];
}
"**left"->node1:"*next" [color=red];
"*head"->node1:value [lhead=cluster_a];
node1:"*next"->node2:"*next" [lhead=cluster_b];
node2:"*next"->node3:"*next" [lhead=cluster_c];
}
```
loop 2
```graphviz
digraph list
{
graph [fontsize=12 fontname="Verdana" compound=true];
node [shape="record"];
rankdir = "LR"
"**left"[fontcolor=red ,color=red];
"*head";
subgraph cluster_a
{
label = "node_1";
node1 [label="<head>value|*next|{<left> *left |<right> *right}"];
}
subgraph cluster_b
{
label = "node_2";
node2 [label="<head>value|*next|{<left> *left |<right> *right}"];
}
subgraph cluster_c
{
label = "node_3";
node3 [label="<head>value|*next|{<left> *left |<right> *right}"];
}
"**left"->node2:"*next" [color=red];
"*head"->node1:value [lhead=cluster_a];
node1:"*next"->node2:"*next" [lhead=cluster_b];
node2:"*next"->node3:"*next" [lhead=cluster_c];
}
```
---
快速排序法在排序過程中,會將尚未排序好的子序列做排序,以子序列的第一個節點作為 pivot 、 L 會從左邊開始掃,紀錄有多少值小於 pivot,R 從右邊開始,紀錄有多少值大於 pivot。
而第一次排序,L 必為串列的第一個節點,R 為串列的最後一個節點。
```c
begin[0] = *list;
end[0] = list_tail(list);
```
因此,`end[0]`為串列的最後一個節點,而函式`list_tail()`的作用即為回傳一串列最後一個節點的指標。
```c
node_t *list_tail(node_t **left)
{
while ((*left) && (*left)->next)
left = &(AAAA);
return *left;
}
```
與函式`list_length()`相同,函式`list_tail()`需要逐一走訪串列中的所有節點才能夠得到最後一個節點的地址。
```diff
-left = &(AAAA);
+left = &((*left)->next);
```
---
快速排序法在完成一次排序後,會形成三個子串列,分別為,比 pivot 小的序列、 pivot 、比 pivot 大的序列,下一次排序則會對比 pivot 小的序列與比 pivot 大的序列做排序。
排序前
```graphviz
digraph list
{
graph [fontsize=12 fontname="Verdana" compound=true];
node [shape="record"];
rankdir = "LR"
n1[label = "4", group = g1];
n2[label = "1", group = g1];
n3[label = "3", group = g1];
n4[label = "5", group = g1];
n5[label = "2", group = g1];
n6[label = "7", group = g1];
n1->n2;
n2->n3;
n3->n4;
n4->n5;
n5->n6;
pivot[fontcolor=red,color=red];
pivot->n1;
head->n1;
L[fontcolor=green,color=green];
R[fontcolor=green,color=green];
L->n4;
R->n5;
}
```
排序後
```graphviz
digraph list
{
graph [fontsize=12 fontname="Verdana" compound=true];
node [shape="record"];
rankdir = "LR"
n1[label = "4", group = g1];
n2[label = "1", group = g1];
n3[label = "3", group = g1];
n4[label = "5", group = g1];
n5[label = "2", group = g1];
n6[label = "7", group = g1];
n5->n2;
n2->n3;
n3->n1;
n1->n4;
n4->n6;
pivot[fontcolor=red,color=red];
pivot->n1;
head->n5;
L[fontcolor=green,color=green];
R[fontcolor=green,color=green];
L->n4;
R->n5;
}
```
從結果可以看到,排序後 L 與 R 會指向子序列的第一個節點,即成為子序列的起始指標。
```c
while (p) {
node_t *n = p;
p = CCCC;
list_add(n->value > value ? &right : &left, n);
}
```
而實作程式碼則運用這個結果,先建立兩個空字串`left`、`right`,並且將比 pivot 小的節點加至`left`,比 pivot 大的節點加入`right`中,最後的結果會與快速排序法一致。
由此可以知道,此迴圈須逐一走訪串列並且與 pivot 比大小。
```diff
-p = CCCC;
+p = p->next;
```
- `void list_add(node_t **list, node_t *node_t)`:
`list_add` 的功能是將額外的節點插置陣列的開頭。
其運作的方式為,將新節點指向陣列的第一個節點,並且將指向陣列頭部指標的 `list` 更新為指向新節點。
```c
void list_add(node_t **list, node_t *node_t)
{
node_t->next = *list;
*list = node_t;
}
```
```graphviz
digraph add
{
node [shape="record"];
rankdir = "LR"
list;
subgraph cluster_b
{
label = "node_t";
node2 [label="<head>value|*next|{<left> *left |<right> *right}"];
}
subgraph cluster_a
{
label = "head of list";
node1 [label="<head>value|*next|{<left> *left |<right> *right}"];
}
list->node1:head;
node2:"*next"->node1:"*next";
}
```
```graphviz
digraph add
{
node [shape="record"];
rankdir = "LR"
list;
subgraph cluster_b
{
label = "node_t";
node2 [label="<head>value|*next|{<left> *left |<right> *right}"];
}
subgraph cluster_a
{
label = "head of list";
node1 [label="<head>value|*next|{<left> *left |<right> *right}"];
}
list->node2:head;
node2:"*next"->node1:"*next";
}
```
---
由於使用非遞迴的方式做快速排序,因此需要紀錄每個子序列的起始節點與結束節點,並用迴圈代替遞迴。
```c
begin[i] = left;
end[i] = DDDD;
begin[i + 1] = pivot;
end[i + 1] = pivot;
begin[i + 2] = right;
end[i + 2] = EEEE;
```
此段程式碼即為紀錄每個子序列的起始與結束節點,因此
```diff
begin[i] = left;
- end[i] = DDDD;
+ end[i] = list_tail(left);
begin[i + 1] = pivot;
end[i + 1] = pivot;
begin[i + 2] = right;
- end[i + 2] = EEEE;
+ end[i + 2] = list_tail(right);
```
### 改進方案
以遞迴實作中,遞迴的最底層,即每個以序列只用一個元素,因此`begin[]`與`end[]`的記憶體分配至多只需要 `n`個空間。
此外,每個子序列的`end[i]`可由`list_tail(begin[i])`取得,因此不需要使用額外的儲存空間。
```diff
- int max_level = 2 * n;
+ int max_level = n;
- node_t *begin[max_level], *end[max_level];
+ node_t *begin[max_level];
```
```diff
while (i >= 0) {
- node_t *L = begin[i], *R = end[i];
+ node_t *L = begin[i], *R = list_tail(begin[i]);
if (L != R) {
...
```
```diff
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);
```
### 以 linux List API 改寫
```c
void quick_sort(struct list_head **list)
{
struct list_head *head = (*list);
int n = list_length(head);
int value;
int i = 0;
int max_level = 2*n;
struct list_head *begin[max_level];
struct list_head *result = new_head(), *left = new_head(), *right = new_head();
begin[0] = head;
for(int j=1 ; j<max_level ; j++){
begin[j] = new_head();
}
while (i >= 0) {
struct list_head *L = begin[i]->next, *R = begin[i]->prev;
if (L != R) {
struct list_head *pivot = begin[i]->next;
value = list_entry(pivot, node_t, list)->value;
list_del(pivot);
node_t *entry, *safe;
list_for_each_entry_safe (entry, safe, begin[i],list) {
list_move(&(entry->list),entry->value > value ? right : left);
}
list_splice_init(left, begin[i]);
list_add(pivot,begin[i + 1]);
list_splice_init(right, begin[i+2]);
i += 2;
} else {
if (list_is_singular(begin[i]))
list_splice_init(begin[i],result);
i--;
}
}
*list = result;
for(int j=1 ; j<max_level ; j++){
free(begin[j]);
}
free(right);
free(left);
}
```
程式細節見[commit](https://github.com/marvin0102/M02/commit/04a2572cd242bc2e4afe4d33f64e95a86fbac48c?diff=unified&w=0)
## 測驗 2:
### Timsort 程式運作原理:
Timsort 透過只合併 2 runs 中大小關係重疊的區域、將以序列中已排序的元素分在同一 run ,降低了傳統合併排序法(merge sort)的記憶體開銷與比較次數。
考慮以下序列
```graphviz
digraph list
{
graph [fontsize=12 fontname="Verdana" compound=true];
node [shape="record"];
rankdir = "LR"
n1[label = "1", group = g1];
n2[label = "2", group = g1];
n3[label = "3", group = g1];
n4[label = "4", group = g1];
n5[label = "3", group = g1];
n6[label = "2", group = g1];
n7[label = "4", group = g1];
n8[label = "7", group = g1];
n9[label = "8", group = g1];
n1->n2;
n2->n3;
n3->n4;
n4->n5;
n5->n6;
n6->n7;
n7->n8;
n8->n9;
}
```
`find_run` 作用為找出序列中的下一個 run ,也就是需要合併的子序列。
```graphviz
digraph list
{
graph [fontsize=12 fontname="Verdana" compound=true];
node [shape="record"];
rankdir = "LR"
n1[label = "1", color = red, group = g1];
n2[label = "2", color = red, group = g1];
n3[label = "3", color = red, group = g1];
n4[label = "4", color = red, group = g1];
n5[label = "3", group = g1];
n6[label = "2", group = g1];
n7[label = "4", group = g1];
n8[label = "7", group = g1];
n9[label = "8", group = g1];
n1->n2;
n2->n3;
n3->n4;
n4->n5[style=dashed,color=gray];
n5->n6;
n6->n7;
n7->n8;
n8->n9;
"result.head"[fontcolor=red,color=red];
"result.head"->n1;
"result.next"[fontcolor=red,color=red];
"result.next"->n5;
}
```
執行完第一次 `find_run` 後, `result.head` 會紀錄此 run ,並且將其餘的序列紀錄在 `result.next`。
指標 `tp` 負責紀錄所有待合併的 runs,作法是使用 linked list 串連所有 runs 的 `head` 指標。
`merge_collapse` 在接收 runs 的鍊接串列後,負責判斷需要合併哪些 runs ,合併串列須達成兩個條件:
- 當欲合併的 runs 超過三個時,須判斷 `tp->prev->prev` 的長度是否小於等於 `tp->prev` 、 `tp`長度之合
- `tp->prev` 的長度是否小於等於 `tp`
`merge_at` 則負責將 `tp` 、 `tp->prev` 合併,其中 `merge` 使用指標的指標將兩序列合併,好處是不需要額外的記憶體。
```graphviz
digraph list
{
graph [fontsize=12 fontname="Verdana" compound=true];
node [shape="record"];
rankdir = "LR"
n1[label = "1", group = g1];
n2[label = "2", group = g1];
n3[label = "3", group = g1];
n4[label = "4", group = g1];
n5[label = "3", group = g1];
n6[label = "2", group = g1];
n7[label = "4", group = g1];
n8[label = "7", group = g1];
n9[label = "8", group = g1];
n1->n2;
n2->n3;
n3->n4;
n6->n5;
n7->n8;
n8->n9;
"tp->prev->prev"[fontcolor=red,color=red];
"tp->prev->prev"->n1;
"tp->prev"[fontcolor=red,color=red];
"tp->prev"->n6;
"tp"[fontcolor=red,color=red];
"tp"->n7;
"tp"->"tp->prev";
"tp->prev"->"tp->prev->prev";
}
```
上圖為合併前 `tp`所紀錄的 runs
```graphviz
digraph list
{
graph [fontsize=12 fontname="Verdana" compound=true];
node [shape="record"];
rankdir = "LR"
n1[label = "1", group = g1];
n2[label = "2", group = g1];
n3[label = "3", group = g1];
n4[label = "4", group = g1];
n5[label = "3", group = g1];
n6[label = "2", group = g1];
n7[label = "4", group = g1];
n8[label = "7", group = g1];
n9[label = "8", group = g1];
n1->n2;
n2->n3;
n3->n4;
n6->n5;
n5->n7;
n7->n8;
n8->n9;
"tp->prev"[fontcolor=red,color=red];
"tp->prev"->n1;
"tp"[fontcolor=red,color=red];
"tp"->n6;
"tp"->"tp->prev";
}
```
因為 `tp->prev->prev`<=`tp->prev`+`tp`,且`tp->prev`<`tp`,因此將`tp`、`tp->prev` 合併。
```graphviz
digraph list
{
graph [fontsize=12 fontname="Verdana" compound=true];
node [shape="record"];
rankdir = "LR"
n1[label = "1", group = g1];
n2[label = "2", group = g1];
n3[label = "3", group = g1];
n4[label = "4", group = g1];
n5[label = "3", group = g1];
n6[label = "2", group = g1];
n7[label = "4", group = g1];
n8[label = "7", group = g1];
n9[label = "8", group = g1];
n1->n2;
n2->n6;
n6->n3;
n3->n5;
n5->n4;
n4->n7;
n7->n8;
n8->n9;
"tp"[fontcolor=red,color=red];
"tp"->n1;
}
```
又,因為`tp->prev`<`tp` ,因此做合併
最後`merge_force_collapse`將剩餘的 runs 做合併。
### 改進實做:
此實作程式中的 `merge` 並不符合 Galloping mode 的方法敘述,而是依照linux merge的方法進行。
可以將 Galloping mode 進行實做,並比較其效率。
實驗結果:
在亂數測資的情況下,原始 merge 的比較次數為:
```c
==== Testing timesort ====
Comparisons: 9356
List is sorted
```
Galloping mode 的比較次數為:
```c
==== Testing timesort ====
Comparisons: 16855
List is sorted
```
在一般亂數的情況下, Galloping mode 的比較次數有明顯提昇。
---
# 第二周測驗
## 測驗 1
### 透過前序與中序建構樹程式運作原理
實作程式碼會使用到 hash table
[Linux 核心的 hash table 實作](https://hackmd.io/@sysprog/linux-hashtable)
在 linux 核心的 hash table 實作中,節點的資料結構被定義為:
```c
struct hlist_node {
struct hlist_node *next, **pprev;
};
```
而 table 是由資料結構 `hlist_head` 型別的陣列所構成:
```c
struct hlist_head {
struct hlist_node *first;
};
```
:::info
其連接示意圖如下:
:::
```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];
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:ht1 -> hn1
hn1:next -> hn2
hn2:next -> null1
hn2:prev:s -> hn1:next:s
map:ht5 -> hn3
hn3:next -> null2
hn1:prev:s -> map:ht1
hn3:prev:s -> map:ht5
}
```
---
在建構樹的時候,需要取得前序、中序、後序中的其中兩個,否則無法確定父節點與子節點的關係。
程式一開始需要建立一個空的 hash table ,並且使用`INIT_HLIST_HEAD()` 將 table 初始化。
`node_add()` 負責把樹的節點依照 inorder 順序插至 hash table 中
:::info
以 preorder: [3,9,20,15,7] ,inorder: [9,3,15,20,7] 為例:
:::
```graphviz
digraph G {
rankdir=LR;
node[fontsize="20" ,shape=record];
map [label="<ht0>hlist_head0.first |<ht1>hlist_head1.first |<ht2>hlist_head2.first |<ht3>hlist_head3.first |<ht4>hlist_head4.first "];
node[fontsize=""]
subgraph cluster_1 {
style=filled;
color=lightgrey;
node [shape=record];
hn1 [label="hlist_node | {<prev>pprev | <next>next}"];
label="hash_key 9"
}
subgraph cluster_2 {
style=filled;
color=lightgrey;
node [shape=record];
hn2 [label="hlist_node | {<prev>pprev | <next>next}"];
label="hash_key 3"
}
subgraph cluster_3 {
style=filled;
color=lightgrey;
node [shape=record];
hn3 [label="hlist_node | {<prev>pprev | <next>next}"];
label="hash_key 15"
}
subgraph cluster_4 {
style=filled;
color=lightgrey;
node [shape=record];
hn4 [label="hlist_node | {<prev>pprev | <next>next}"];
label="hash_key 20"
}
subgraph cluster_5 {
style=filled;
color=lightgrey;
node [shape=record];
hn5 [label="hlist_node | {<prev>pprev | <next>next}"];
label="hash_key 7"
}
map:ht0->hn3:hlist_node;
hn3:next->hn4:hlist_node;
hn4:pprev->hn3:next;
map:ht1->hn2:hlist_node;
map:ht2->hn5:hlist_node
map:ht4->hn1:hlist_node;
}
```
`dfs()` 使用遞迴重新建構樹,方法為:
將當前 preorder 的第一個數值作為 root ,並使用 `find()` 函式在 hash table 中找到此數值在 inorder 中的位置`idx`。
使用 `idx` 分割原前序、中序為左子樹與右子樹之前、中序,並呼叫 `dfs()`分別建構其左、右子樹。
### 改進想法:
在建構樹的過程中,hash table 中的每個節點只會只用一次,因此在節點被加數樹時,將節點從 hash table 中刪除可以有效降低每次的尋時間。
在原本的實作程式中,hash table 單純被用來查找 node index,在建構樹時仍須配置額外的記憶體,此過程明顯存在記憶體浪費,如果能將 hash table 的記憶體加以利用,移可增加記憶體的利用率。
```diff
struct order_node {
- struct hlist_node node;
- int val;
+ struct TreeNode node;
int idx;
};
```
基於這個想法,我將改變了原本 hash table 的資料結構,捨棄 `hlist_head` 、 `hlist_node` 改為使用建構樹時的資料結構 `TreeNode` 作為代替。
hash table 的建構改為與 linux linked list 相似的雙向鍊接串列進行實作。
```graphviz
digraph G {
rankdir=LR;
node[shape=record];
map [label="TreeNode.right |<ht0> |<ht1> |<ht2> |<ht3> |<ht4> |<ht5> |<ht7> |<ht8> "];
subgraph cluster_1 {
style=filled;
color=lightgrey;
node [shape=record];
hn1 [label="val | {<prev>left | <next>right}"];
label="TreeNode 1"
}
subgraph cluster_2 {
style=filled;
color=lightgrey;
node [shape=record];
hn2 [label="val | {<prev>left | <next>right}"];
label="TreeNode 2"
}
subgraph cluster_3 {
style=filled;
color=lightgrey;
node [shape=record];
hn3 [label="val | {<prev>left | <next>right}"];
label="TreeNode 3"
}
map:ht1 -> hn1
hn1:next:s -> hn2:val
hn2:prev:s -> hn1:val
map:ht5 -> hn3
hn1:prev:s -> map:ht1
hn3:prev:s -> map:ht5
}
```
```diff
-static int find(int num, int size, const struct hlist_head *heads)
+static struct TreeNode* find(int num, int size, const struct TreeNode *heads)
{
- struct hlist_node *p;
+ struct TreeNode *p;
int hash = (num < 0 ? -num : num) % size;
- hlist_for_each (p, &heads[hash]) {
+ list_for_each (p, &heads[hash]) {
struct order_node *on = list_entry(p, struct order_node, node);
- if (num == on->val)
- return on->idx;
+ if (num == on->node.val){
+ list_del(p);
+ return p;
+ }
}
- return -1;
+ return NULL;
}
```
```diff
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);
+ struct TreeNode *tn = find(preorder[pre_low], size, in_heads);
+ int idx = list_entry(tn, struct order_node, node)->idx;
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);
```
這樣更改的優點為:
1. 將 hash table 作為 `TreeNode` 的存放陣列,當特定節點需要被用於建構樹時,可以直接從 hash table 中提取而不需要額外配置記憶體。
2. 由於樹的節點會從 hash table 中移出,可以有效提昇 hash table 的搜索效率
詳細的更改內容見[commit](https://github.com/marvin0102/M02/commit/e90d52a512b42ccd11da58e1c520d09de80b8696#)
### Linux 核心的 cgroups 相關原始程式碼
[include/linux/cgroup.h](https://elixir.bootlin.com/linux/latest/source/include/linux/cgroup.h#L239)
在 Linux 核心中定義了巨集 `css_for_each_descendant_pre` 以 pre-order 順序走訪 `*css`,其中 root 會是第一個走訪的節點。
```c
#define css_for_each_descendant_pre(pos, css) \
for ((pos) = css_next_descendant_pre(NULL, (css)); (pos); \
(pos) = css_next_descendant_pre((pos), (css)))
```
在此巨集中,會利用函式 `css_next_descendant_pre` 找尋 `pos` 節點的下一個 pre-order 節點。
```c
struct cgroup_subsys_state *
css_next_descendant_pre(struct cgroup_subsys_state *pos,
struct cgroup_subsys_state *root)
{
struct cgroup_subsys_state *next;
cgroup_assert_mutex_or_rcu_locked();
/* if first iteration, visit @root */
if (!pos)
return root;
/* visit the first child if exists */
next = css_next_child(NULL, pos);
if (next)
return next;
/* no child, visit my or the closest ancestor's next sibling */
while (pos != root) {
next = css_next_child(pos, pos->parent);
if (next)
return next;
pos = pos->parent;
}
return NULL;
}
```
`css_next_descendant_pre` 找尋下一個節點的方法為:
- 若現在尚未走訪任何節點,則以 root 為第一個走訪的節點
- 若 `pos` 節點存在子節點,則利用 `css_next_child` 走訪第一個子節點
- 若 `pos` 節點不存在子節點,則找尋自己或是祖先的兄弟節點走訪
---
## 測驗 2
### LRU 資料結構:
```c
typedef struct {
int capacity;
int count;
struct list_head dhead;
struct hlist_head hhead[];
} LRUCache;
```
`LRUCache` 資料結構可以當作 Cashe 本身,其中:
- capacity: Cache 的容量
- count: 目前在 Cache 內的資料個數
- dhead: 為 linux linked list , 目的是紀錄 Cache 內各`LRUnode`的使用頻率
- hhead: 為 hash table,可以使用`key`來找尋欲寫入或讀取的`LRUnode`
```c
typedef struct {
int key;
int value;
struct hlist_node node;
struct list_head link;
} LRUNode;
```
`LRUNode`作為資料的存取單元,用於儲存放入 Cache 內的資料:
- key: hash table 的建值,用於在 hash table 中搜尋`LRUNode`
- value: 儲存於 Cache 內的數值
- node:連接於 hash table 中,用於在 hash table 中搜尋`LRUNode`
- link: 連接於 linked list 中,在寫入或讀取時,可以即時更新`LRUNode`的取用頻率
### LRU 程式運作原理:
`lRUCacheCreate()`函式的作用是創建一個容量為`capacity`的 `LRUCache` 並且初始化。
```graphviz
digraph G {
rankdir=LR;
subgraph cluster_LRU {
style=filled;
color=lightgrey;
node [shape=record];
hn5 [label="capacity |count | {<prev> *dhead | <next>*hhead}"];
label="LRUCache"
}
node[shape=record];
map [label="<head>hlist_head.first |<ht0> |<ht1> |<ht2> |<ht3> |<ht4> |<ht5> |<ht7> |<ht8> "];
hn5:next->map:head;
}
```
`lRUCacheFree()`則是負責釋放`LRUCache`裡所註冊的所有記憶體。
`lRUCacheGet()`取用目前已經在 cache 內的 `LRUnode`。
:::info
假設目前 cache 內已有三個 `LRUNode` 如下:
:::
```graphviz
digraph G {
graph [fontsize=12 compound=true];
node [shape="record"];
rankdir = "LR"
subgraph cluster_a
{
label = "LRUNode_key 3";
node1 [label="<head>list_head|{<prev> *prev |<next> *next}"];
}
subgraph cluster_b
{
label = "LRUNode_key 2";
node2 [label="<head>list_head|{<prev> *prev |<next> *next}"];
}
subgraph cluster_c
{
label = "LRUNode_key 1";
node3 [label="<head>list_head|{<prev> *prev |<next> *next}"];
}
"*dhead"->node1:head ;
node1:next->node2:head ;
node2:prev->node1:head ;
node2:next->node3:head ;
node3:prev->node2:head ;
rankdir=LR;
node[shape=record];
map [label="<head>hlist_head.first |<ht0> |<ht1> |<ht2> |<ht3> |<ht4> |<ht5> |<ht7> |<ht8> "];
"*hhead";
subgraph cluster_1 {
style=filled;
color=lightgrey;
node [shape=record];
hn1 [label="hlist_node | {<prev>pprev | <next>next}"];
label="LRUNode_key 1"
}
subgraph cluster_2 {
style=filled;
color=lightgrey;
node [shape=record];
hn2 [label="hlist_node | {<prev>pprev | <next>next}"];
label="LRUNode_key 2"
}
subgraph cluster_3 {
style=filled;
color=lightgrey;
node [shape=record];
hn3 [label="hlist_node | {<prev>pprev | <next>next}"];
label="LRUNode_key 3"
}
map:ht1 -> hn1
hn1:next -> hn2
hn2:prev:s -> hn1:next:s
map:ht5 -> hn3
hn1:prev:s -> map:ht1
hn3:prev:s -> map:ht5
"*hhead"->map:head;
}
```
:::info
則當我們想要存取`LRUNode_key 2`的 value 時,呼叫`lRUCacheGet()`, `LRUCache`內的 linked list會被更新。
:::
```graphviz
digraph G {
graph [fontsize=12 compound=true];
node [shape="record"];
rankdir = "LR"
subgraph cluster_a
{
color=red
label = "LRUNode_key 2";
node1 [label="<head>list_head|{<prev> *prev |<next> *next}"];
}
subgraph cluster_b
{
label = "LRUNode_key 3";
node2 [label="<head>list_head|{<prev> *prev |<next> *next}"];
}
subgraph cluster_c
{
label = "LRUNode_key 1";
node3 [label="<head>list_head|{<prev> *prev |<next> *next}"];
}
"*dhead"->node1:head ;
node1:next->node2:head ;
node2:prev->node1:head ;
node2:next->node3:head ;
node3:prev->node2:head ;
rankdir=LR;
node[shape=record];
map [label="<head>hlist_head.first |<ht0> |<ht1> |<ht2> |<ht3> |<ht4> |<ht5> |<ht7> |<ht8> "];
"*hhead";
subgraph cluster_1 {
style=filled;
color=lightgrey;
node [shape=record];
hn1 [label="hlist_node | {<prev>pprev | <next>next}"];
label="LRUNode_key 1"
}
subgraph cluster_2 {
style=filled;
color=lightgrey;
node [shape=record];
hn2 [label="hlist_node | {<prev>pprev | <next>next}"];
label="LRUNode_key 2"
}
subgraph cluster_3 {
style=filled;
color=lightgrey;
node [shape=record];
hn3 [label="hlist_node | {<prev>pprev | <next>next}"];
label="LRUNode_key 3"
}
map:ht1 -> hn1
hn1:next -> hn2
hn2:prev:s -> hn1:next:s
map:ht5 -> hn3
hn1:prev:s -> map:ht1
hn3:prev:s -> map:ht5
"*hhead"->map:head;
}
```
:::info
`LRUNode_key 2`被移到了 list 的最前面,這表示`LRUNode_key 2`為我們最近一次使用的`LRUNode`
:::
`lRUCachePut()`更新 `LRUNode->value`的值,若 `LRUNode` 不存在且 Cache 未滿,則新增`LRUNode` ,但若 Cache 已滿,則須將被閒置最久的`LRUNode`從hash table 中移除,並且將`LRUNode->key`、`LRUNod->value`更新後再重新插入 hash table。
:::info
加入`LRUNode_key 4`
:::
```graphviz
digraph G {
graph [fontsize=12 compound=true];
node [shape="record"];
rankdir = "LR"
subgraph cluster_a
{
label = "LRUNode_key 2";
node1 [label="<head>list_head|{<prev> *prev |<next> *next}"];
}
subgraph cluster_b
{
label = "LRUNode_key 3";
node2 [label="<head>list_head|{<prev> *prev |<next> *next}"];
}
subgraph cluster_c
{
label = "LRUNode_key 1";
node3 [label="<head>list_head|{<prev> *prev |<next> *next}"];
}
subgraph cluster_d
{
color=red
label = "LRUNode_key 4";
node4 [label="<head>list_head|{<prev> *prev |<next> *next}"];
}
"*dhead"->node4:head ;
node4:next->node1 ;
// node1:prev:s->node4:head ;
node1:next->node2 ;
// node2:prev:s->node1:head ;
node2:next->node3 ;
// node3:prev:s->node2:head ;
rankdir=LR;
node[shape=record];
map [label="<head>hlist_head.first |<ht0> |<ht1> |<ht2> |<ht3> |<ht4> |<ht5> |<ht7> |<ht8> "];
"*hhead";
subgraph cluster_1 {
style=filled;
color=lightgrey;
node [shape=record];
hn1 [label="hlist_node | {<prev>pprev | <next>next}"];
label="LRUNode_key 1"
}
subgraph cluster_2 {
style=filled;
color=lightgrey;
node [shape=record];
hn2 [label="hlist_node | {<prev>pprev | <next>next}"];
label="LRUNode_key 2"
}
subgraph cluster_3 {
style=filled;
color=lightgrey;
node [shape=record];
hn3 [label="hlist_node | {<prev>pprev | <next>next}"];
label="LRUNode_key 3"
}
subgraph cluster_4 {
style=filled;
color=lightgrey;
node [shape=record];
hn4 [label="hlist_node | {<prev>pprev | <next>next}"];
label="LRUNode_key 4"
}
map:ht1 -> hn1
hn1:next -> hn2
hn2:prev:s -> hn1:next:s
map:ht5 -> hn4
hn4:next->hn3
hn1:prev:s -> map:ht1
hn4:prev:s -> map:ht5
hn3:prev:s -> hn4:next
"*hhead"->map:head;
}
```
### 改進方法:
將最後一次存取的 cache 在 hash table 中的位置一致最前面,這樣做的好處是可以提昇搜索 cache 時的效率。
但是,在 hash table size 為最佳時,搜尋的時間趨近於常數,此種改進方法並沒有辦法提昇搜尋效率。
程式請見[commit](https://github.com/marvin0102/M02/commit/f3c0695f82d8f8c570b4db19d89c39c79aedffb4)
### Linux 核心 lru 相關程式:
[include/linux/lru_cache.h](https://elixir.bootlin.com/linux/latest/source/include/linux/lru_cache.h#L166) 中定義了 LRU cache 的資料型別,其中包含 `lc_element`、`lc_cache`。
其中會使用到 `kmem_cache` ,`kmem_cache` 主要的作用是管理 slub 等對象。([slub](https://en.wikipedia.org/wiki/SLUB_(software)))
而在 [lib/lru_cache.c](https://elixir.bootlin.com/linux/latest/source/lib/lru_cache.c#L40) 中,定義了 `lc_create()` 、 `lc_find()` 等新增及操作 lru cache 的函式。
---
## 測驗 3
考慮 find_nth_bit 可在指定的記憶體空間找出第 N 個設定的位元 (即 1)
以下為程式的實作原理:
### 程式實作原理:
巨集`small_const_nbits(nbits)` 的作用是判斷欲查詢的記憶體是否為陣列,其中:
- `__builtin_constant_p(exp)`:
判斷 exp 在編譯時是否為常量。
:::info
[The use of the GNU compilers. 6.61](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html)
Built-in Function: int __builtin_constant_p (exp):
- You can use the built-in function `__builtin_constant_p` to determine if the expression exp is known to be constant at compile time and hence that GCC can perform constant-folding on expressions involving that value.
- The argument of the function is the expression to test. The expression is not evaluated, side-effects are discarded.
- The function returns the integer 1 if the argument is known to be a compile-time constant and 0 if it is not known to be a compile-time constant.
- Any expression that has side-effects makes the function return 0. A return of 0 does not indicate that the expression is not a constant, but merely that GCC cannot prove it is a constant within the constraints of the active set of optimization options.
:::
---
如果`small_const_nbits(nbits)` 通過即表示欲檢查的記憶體小於等於一個 unsigned long 大小,這時就可以在設定的範圍內尋找 Nth set bit。
巨集`GENMASK(h, l)`為限制函式的找尋範圍為從 l th 到 h th 個 bit ,低於或超出這個範圍的 bit 都會因被 mask 遮擋而設為 0。
```c
n = 0xAAAA /*0b1010 1010 1010 1010*/
val = GENMASK(15, 4) /*val = 0x0AA0 = 0b0000 1010 1010 0000*/
```
---
如果`small_const_nbits(nbits)` 未通過即表示欲檢查的記憶體大於一個 unsigned long 大小。
此時需要透過巨集 `FIND_NTH_BIT()` 先鎖定 Nth set bit 位於陣列中的第幾個元素中,再藉由 `fns()` 找出其位置。
作法是,逐一走訪陣列中的每個 unsigned long ,透過`hweight_long()`計算其中的 set bits 個數,當累積的 set bits 個數超過 N 時,即代表 Nth set bit 位於目前的 undigned long 中。
---
函式`fns(word, n)`的作用為,在`unsigned long word`中,找出 nth set bit 的位置。
方法為透過`__ffs(word)`找出目前`word`中,第一個 set bit 的位置,如果此 set bit 並非我們所需的 nth set bit ,則使用函式`__clear_bit()`設為0,並且做`n--`,當`n--==0`成立時,此時的 set bit 即為 nth set bit 。
- `__ffs()`:透過分區檢查的方式,找出 unsigned long 中的第一個 set bit 位置
```c
/* 假設 word 值 */
word = 0xABCDEF00
/*word = 0b1010 1011 1100 1101 1110 1111 0000 0000*/
static inline unsigned long __ffs(unsigned long word)
{
int num = 0;
#if BITS_PER_LONG == 64
/* 檢查 first set bit 是否出現在前半*/
/*word = 0b1010 1011 1100 1101 1110 1111 0000 0000*/
/* & */
/* 0b1111 1111 1111 1111 1111 1111 1111 1111*/
if ((word & AAAA) == 0) {
num += 32;
word >>= 32;
}
#endif
/*word = 0b1010 1011 1100 1101 1110 1111 0000 0000*/
/* & */
/* 0b0000 0000 0000 0000 1111 1111 1111 1111*/
if ((word & 0xffff) == 0) {
num += 16;
word >>= 16;
}
/*word = 0b1010 1011 1100 1101 1110 1111 0000 0000*/
/* & */
/* 0b0000 0000 0000 0000 0000 0000 1111 1111*/
/*如果 & 結果為 0 ,代表檢查區域無 set bit 因此將 word
* 向右移清除此區域,並且 num 加上此區長度*/
if ((word & 0xff) == 0) {
num += 8;
word >>= 8;
}
/*word = 0b0000 0000 1010 1011 1100 1101 1110 1111 */
/* & */
/* 0b0000 0000 0000 0000 0000 0000 0000 1111*/
if ((word & 0xf) == 0) {
num += 4;
word >>= 4;
}
/*word = 0b0000 0000 1010 1011 1100 1101 1110 1111 */
/* & */
/* 0b0000 0000 0000 0000 0000 0000 0000 0011*/
if ((word & 0x3) == 0) {
num += 2;
word >>= 2;
}
/*word = 0b0000 0000 1010 1011 1100 1101 1110 1111 */
/* & */
/* 0b0000 0000 0000 0000 0000 0000 0000 0001*/
if ((word & 0x1) == 0)
num += 1;
return num;
}
```
- `__clear_bit(nr, addr)`:將 addr 中,第 nr 個 bit 設為 0
- `BIT_MASK(nr)`:回傳一段 bit mask ,只有第 nr 個 bit 為 1 ,用於改變第 nr 個 bit
- `BIT_WORD(nr)`:因為 addr 可能為陣列,因此需要計算 nr 位於陣列中的第幾個元素中。
最後只需要將 `p & ~mask`,就可以清除第 nr 個 bit。
### Linux 核心中 `find_nth_bit`的應用
在 [kernel/sched/core.c](https://elixir.bootlin.com/linux/latest/source/kernel/sched/core.c#L8436) 中分別定義了取得與設定 affinity 的函式,`sched_getaffinity` 、 `sched_setaffinity` 。
```c
extern long sched_setaffinity(pid_t pid, const struct cpumask *new_mask);
extern long sched_getaffinity(pid_t pid, struct cpumask *mask);
```
函式引數中的 `cpumask` 的每一個 bit 代表一個處理器,當第 n 個 bit 被設為 1 時,即代表此程式可以在第 n 個上運行,透過設定 `cpumask` 上的 bit 即可以指定程式可以在哪些固定的處理器上執行。([CPU Affinity](https://www.linuxjournal.com/article/6799))
位於 [/include/linux/cpumask.h](https://elixir.bootlin.com/linux/latest/source/include/linux/cpumask.h#L399) 的函式 `cpumask_nth()` 作用為取得 `cpumask` 當中第 n 個處理器
```c
static inline unsigned int cpumask_nth(unsigned int cpu, const struct cpumask *srcp)
{
return find_nth_bit(cpumask_bits(srcp), small_cpumask_bits, cpumask_check(cpu));
}
```
其實作的原理即為,使用 `find_nth_bit` 找出 `cpu_mask` 中的第 n 個 bit。
提問
---
使用巨集(macro)定義寒是的好處是什麼?
因為巨集是由前製處理器(preprocessor)預處理的,本質上算是字串代換,因此使用巨集定義函式時,不需考慮傳入參數的資料型別。
但如果此函式的傳入參數型別是固定的,以 `find_nth_bit` 實作程式為例,`FIND_NTH_BIT(addr[idx], size, n)`中,`addr`、`size`、`n`的資料型別必定為`unsigned long *`、`unsigned long`、`unsigned long`,此時使用巨集定義函式就沒有了上述的優勢。且`FIND_NTH_BIT`中定義了新的變數,如果重複使用`FIND_NTH_BIT`,會造成變數重複定義的問題,引發編譯時的錯誤。
因此,在`find_nth_bit` 實作程式中,將`FIND_NTH_BIT`定義為一般函式應該會比較安全,也方便出現錯誤時追蹤程式,那麼使用巨集定義函式的優勢為何?