# 2024q1 Homework2 (quiz1+2)
contributed by < [`Lccgth`](https://github.com/Lccgth) >
## 第一週測驗題
### 測驗 `1`
#### `list_add()`
將一個新的節點添加到鏈結串列的開頭處。
`node_t` 為要加入到鏈結串列的節點,而 `*list` 為指向鏈結串列的開頭的指標。
```c
node_t->next = *list;
*list = node_t;
```
```graphviz
digraph list_add {
rankdir=LR;
node [shape=box, fontname = "Helvetica"];
label="Before Adding New Node";
"*list" -> node1;
subgraph cluster {
label = "";
node1 -> node2 -> null;
}
}
```
```graphviz
digraph list_add {
rankdir=LR;
node [shape=box, fontname = "Helvetica"];
label = "After Adding New Node";
"*list" -> node_t;
subgraph cluster {
label = "";
node_t -> node1 -> node2 -> null;
}
}
```
#### `list_tail()`
返回鏈結串列尾部的節點。
從 `*left` 開始逐一走訪鏈結串列中的節點,直到找到尾部節點就將其回傳。
```c
while ((*left) && (*left)->next)
left = &((*left)->next);
```
#### `list_length()`
返回鏈結串列的長度。
從 `*left` 開始逐一走訪鏈結串列中的節點,並同時計算長度,走訪完後回傳,此函式的 `**left` 需要指向鏈結串列的開頭,否則長度會計算錯誤。
```c
while (*left) {
++n;
left = &((*left)->next);
}
```
#### `list_construct()`
創建一個節點,並將其加到鏈結串列的開頭處。
使用 `malloc()` 分配記憶體空間,再將新節點的 `next` 指向鏈結串列的開頭,最後將新節點的 `value` 設成 `n`。
```c
node_t *node = malloc(sizeof(node_t));
node->next = list;
node->value = n;
```
#### `list_free()`
逐一釋放鏈結串列的節點所使用到的記憶體空間。
逐一走訪鏈結串列,並同時釋放目前走訪到的節點使用到的記憶體空間。
進行小幅改寫移除迴圈中的 `if` 判斷式,使程式碼更加簡潔。
```diff
- node_t *node = (*list)->next;
+ node_t *node;
while (*list) {
- free(*list);
- *list = node;
- if (node)
- node = node->next;
+ node = *list;
+ list = (*list)->next;
+ free(node);
}
```
#### `list_is_ordered()`
檢查鏈結串列是否已經排序完成。
逐一走訪鏈結串列中的節點,並和目前為止觀察到的最大值 (`value`) 比較,若當前節點小於 `value`,回傳 `false`,若當前節點大於 `value`,則更新 `value` 成目前節點的值。
我將一開始的 `value` 就設定為開頭節點的 `value`,在迴圈內就不用判斷是否為第一次進入迴圈,但需要先判斷一次 `list` 是否為 `NULL`。
```diff
- bool first = true;
- int value;
+ if (!list)
+ return true;
+ int value = list->value;
+ list = list->next;
while (list) {
- if (first) {
- value = list->value;
- first = false;
- } else {
- if (list->value < value)
- return false;
- value = list->value;
- }
+ if (list->value < value)
+ return false;
+ value = list->value;
list = list->next;
}
```
#### `shuffle()`
打亂鏈結串列中的節點順序。
逐一走訪鏈結串列的節點,並隨機選一個後續節點和目前節點交換。
此方法只在 `n < RAND_MAX` 時有效,因為 `rand()` 會返回一個 0 到 RAND_MAX 的隨機數,若 `n >= RAND_MAX` 時會導致無法產生足夠均勻的隨機數。
```c
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;
}
```
#### `quick_sort()`
使用非遞迴的方式實作快速排序。
每一組 `begin[i]` 和 `end[i]` 表示第 i 個鏈結串列,利用 `i` 選擇目前要處理的串列是哪一個。
迴圈會先判斷目前的 `begin[i]` 是否和 `end[i]` 相等,如果相等就直接將此串列加入 `result` 的開頭。
```c
if (L != R){
// ...
} else {
if (L)
list_add(&result, L);
}
```
接著將目前串列的開頭設為 `pivot`,並從下一個節點開始逐一走訪,如果走訪到的節點小於 `pivot`,就將其加入 `left`,反之則加入 `right`。
```c
while (p) {
node_t *n = p;
p = p->next;
list_add(n->value > value ? &right : &left, n);
}
```
然後更新 `begin` 和 `end`,將 `begin[i]` 設為 `left`,`begin[i + 1]` 設為 `pivot`,而 `begin[i + 2]` 設為 `right`。
```c
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);
```
最後 `begin` 中的每條串列都會只有一個節點,並會依序被加入到 `result` 中,完成排序。
#### 快速排序法圖解
例題
```graphviz
digraph list_add {
rankdir=LR;
pivot [label="pivot" shape=plaintext];
NULL [shape=none];
4 -> 1 -> 3 -> 5 -> 2 -> 7 -> NULL;
{ rank=same; pivot -> 4; }
}
```
Step 1:
將 `4` 設為 `pivot`,逐一走訪串列,將比 `pivot` 小的節點放到 `begin[0]`,比 `pivot` 大的放到 `begin[2]`,`pivot` 放在 `begin[1]`。
```graphviz
digraph list_add {
rankdir=LR;
begin0 [label="begin[0]", shape=none];
begin1 [label="begin[1]", shape=none];
begin2 [label="begin[2]", shape=none];
begin0 -> 2 -> 3 -> 1;
begin1 -> 4;
begin2 -> 7 -> 5;
}
```
Step 2:
將 `7` 設為 `pivot`,逐一走訪 `begin[2]`,將比 `pivot` 小的節點放到 `begin[2]`,`pivot` 放在 `begin[3]`。
```graphviz
digraph list_add {
rankdir=LR;
begin0 [label="begin[0]", shape=none];
begin1 [label="begin[1]", shape=none];
begin2 [label="begin[2]", shape=none];
begin3 [label="begin[3]", shape=none];
begin0 -> 2 -> 3 -> 1;
begin1 -> 4;
begin2 -> 5;
begin3 -> 7;
}
```
Step 3:
因為 `begin[3]`、`begin[2]`、`begin[1]` 都只有一個節點,所以依序加入到 `result` 中。
```graphviz
digraph list_add {
rankdir=LR;
result [label="result", shape=none];
begin0 [label="begin[0]", shape=none];
result -> 4 -> 5 -> 7;
begin0 -> 2 -> 3 -> 1;
}
```
Step 4:
將 `2` 設為 `pivot`,逐一走訪 `begin[0]`,將比 `pivot` 小的節點放到 `begin[0]`,比 `pivot` 大的放到 `begin[2]`,`pivot` 放在 `begin[1]`。
```graphviz
digraph list_add {
rankdir=LR;
result [label="result", shape=none];
begin0 [label="begin[0]", shape=none];
begin1 [label="begin[1]", shape=none];
begin2 [label="begin[2]", shape=none];
result -> 4 -> 5 -> 7;
begin0 -> 1;
begin1 -> 2;
begin2 -> 3;
}
```
Step 4
因為 `begin[3]`、`begin[2]`、`begin[1]` 都只有一個節點,所以依序加入到 `result` 中。
```graphviz
digraph list_add {
rankdir=LR;
result [label="result", shape=none];
result -> 1 -> 2 -> 3 -> 4 -> 5 -> 7;
}
```
### 測驗 `2`
#### Timsort
結合了 Merge sort 和 Insertion sort 的優點,適用於部分已經排序完成的串列,且為一種穩定 (stable) 的排序法。
#### `run_size()`
計算並返回一段以排序好的元素 (run) 大小,此大小被儲存在 `head->next->prev` 中
#### `merge()`
合併兩段已排序好的鏈結串列,會依序將目前兩個串列開頭處較小的節點加入到 `head` 後。
可使用 `while` 替換 `for`,並在其中一個串列已走訪完後跳出迴圈,將剩下的串列直接串接在尾部。
新增參數 `*last` 表示合併完的串列要插入的位置,將 `merge_final()` 中的功能進行整合,只要將 `*last` 設定為 `NULL` 就是原本 `merge()` 的功能,其他情況則會是 `merge_final()` 的功能。
```diff
struct list_head *head;
struct list_head **tail = &head;
- for (;;) {
+ while (a && b)
if (cmp(priv, a, b) <= 0) {
*tail = a;
tail = &a->next;
a = a->next;
- if (!a) {
- *tail = b;
- break;
- }
} else {
*tail = b;
tail = &b->next;
b = b->next;
- if (!b) {
- *tail = a;
- break;
- }
}
}
+ *tail = a ? a : b;
+ if (last)
+ build_prev_link(last, last->prev, head);
```
#### `build_prev_link()`
將 `tail` 和 `list` 拼接起來,並設定其中節點的 `prev`,使其成為環狀雙向鏈結串列。
```c
tail->next = list;
do {
list->prev = tail;
tail = list;
list = list->next;
} while (list);
tail->next = head;
head->prev = tail;
```
#### `merge_final()`
將最後兩段 run 進行合併,並使用 `build_prev_link()` 調整串列,使其成為環狀雙向鏈結串列。
此函式和 `merge()` 的重複性太高,可以進行重構使程式碼更精簡。
#### `find_run()`
找到一段排序好的串列,若為降序排列則反轉其為升序。
#### `merge_at()`
在特定位置 `at` 合併兩個串列,並同時更新 `stk_size` (run 的數量)。
#### `merge_force_collapse()` 和 `merge_collapse()`
當 run 的數量達到一定條件時就選擇合併一些 run,以提升後續合併時的效率。
#### `timsort()`
將一鏈結串列依照 `find_run()` 分成多段 run,並依照 `merge_force_collapse()` 和 `merge_collapse()` 的規則進行適當的合併,最後再使用 `build_prev_link()` 進行串列的調整以完成排序。
## 第二週測驗題
### 測驗 `1`
#### `INIT_HLIST_HEAD()`
藉由將 `h->list` 設為 `NULL` 來初始化 `hlist`。
#### `hlist_add_head()`
將一個的新的節點插入到 `hlist` 的開頭處。
如果 `hlist` 不為空,就將開頭處節點的 `pprev` 指向 `n->next` 的位址,接著依序更新新節點的 `next` 和 `pprev`,最後將開頭節點設為 `n`。
```c
if (h->first)
h->first->pprev = &n->next;
n->next = h->first;
n->pprev = &h->first;
h->first = n;
```
#### `find()`
在一組 `hlist` 中尋找 `num` 的 `idx`。
先通過計算 `hash` 得知此數在 `heads` 中的哪一個串列中,接著逐一走訪此串列的節點來和 `num` 做比對。
```c
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;
}
```
#### `dfs()`
深度優先搜尋,根據前序和中序建立出二元樹。
因前序中首節點為樹的根節點,再依據中序得知每個節點是在根節點的左子樹還是右子樹中,依序建立出獨一無二的二元樹。
#### `node_add()`
將新節點根據雜湊函式加入到 `heads` 中,這裡的雜湊函式會返回一個 0 到 size - 1 間的整數。
```c
on->val = val;
on->idx = idx;
int hash = (val < 0 ? -val : val) % size;
hlist_add_head(&on->node, &heads[hash]);
```
#### `buildTree()`
先根據中序節點數初始化對應數量的 `hlist`,接著將這些節點根據雜湊函式加入到對應的 `hlist` 中,最後使用 `dfs()` 來建立出二元樹。
### 測驗 `2`
#### `hlist_del()`
將 `hlist` 中的節點 `n` 刪除。
```c
struct hlist_node *next = n->next, **pprev = n->pprev;
*pprev = next;
if (next)
next->pprev = pprev;
```
#### `list_add()`
將新的節點插入到串列的開頭處。
#### `list_del()`
將特定的節點從串列中刪除。
#### `list_move()`
利用 `list_del()` 和 `list_add()` 將特定節點移到串列的開頭處。
#### `lRUCacheCreate()`
創立一個指定大小的快取,並初始化其 `dhead` 和 `hhead`。
#### `lRUCacheFree()`
逐一走訪串列中的節點,並依序釋放占用的記憶體空間。
```c
list_for_each_safe (pos, n, &obj->dhead) {
LRUNode *cache = list_entry(pos, LRUNode, link);
list_del(&cache->link);
free(cache);
}
free(obj);
```
#### `lRUCacheGet()`
從 LRU 快取中獲取 `key` 對應的值。
先透過取餘數的方法計算 `hash`,再逐一走訪對應的串列,當找到對應的值時,將其移到串列的開頭處,表示最近被訪問過。
```c
int hash = key % obj->capacity;
hlist_for_each (pos, &obj->hhead[hash]) {
LRUNode *cache = list_entry(pos, LRUNode, link);
if (cache->key == key) {
list_move(&cache->link, &obj->dhead);
return cache->value;
}
}
```
#### `lRUCachePut()`
在 LRU 快取中新增或更新 key-value。
此函式和 `lRUCacheGet()` 有高度相似的程式碼,可將其獨立成一個函式使程式碼更精簡。
```diff
+ LRUNode* findLRUNode(LRUCache *obj, int key)
+ {
+ int hash = key % obj->capacity;
+ struct hlist_node *pos;
+ hlist_for_each(pos, &obj->hhead[hash]) {
+ LRUNode *node = list_entry(pos, LRUNode, node);
+ if (node->key == key)
+ return node;
+ }
+ return NULL;
+ }
int lRUCacheGet(LRUCache *obj, int key)
{
- int hash = key % obj->capacity;
- struct hlist_node *pos;
- hlist_for_each (pos, &obj->hhead[hash]) {
- LRUNode *cache = list_entry(pos, LRUNode, link);
- if (cache->key == key) {
- list_move(&cache->link, &obj->dhead);
- return cache->value;
- }
- }
+ LRUNode *cache = findLRUNode(obj, key);
+ if (cache) {
+ list_move(&cache->link, &obj->dhead);
+ return cache->value;
+ }
return -1;
}
void lRUCachePut(LRUCache *obj, int key, int value)
{
- LRUNode *cache = NULL;
- int hash = key % obj->capacity;
- struct hlist_node *pos;
- hlist_for_each (pos, &obj->hhead[hash]) {
- LRUNode *c = list_entry(pos, LRUNode, link);
- if (c->key == key) {
- list_move(&cache->link, &obj->dhead);
- cache = c;
- }
- }
+ LRUNode *cache = findLRUNode(obj, key);
+ if (cache) {
+ list_move(&cache->link, &obj->dhead);
+ cache->value = value;
+ }
// ...
}
```
### 測驗 `3`
#### `__ffs()`
利用二分搜尋法的方式依序找到 `word` 第一個值為 1 的位元。
```c
if ((word & 0xffffffff) == 0) {
num += 32;
word >>= 32;
}
```
#### `__clear_bit()`
將特定位元的值設為 0。
`BITMASK(n)` 會返回一個只有第 n 個位元為 1 的數,將其作反轉就能得到只有第 n 個位元為 0 的數。
```c
unsigned long mask = BIT_MASK(nr);
unsigned long *p = ((unsigned long *) addr) + BIT_WORD(nr);
*p &= ~mask;
```
#### `fns()`
在 `word` 裡找到第 n 個值為 1 的位元。
#### `find_nth_bit()`
在一個記憶體區域中找到第 n 個值為 1 的位元。
#### `FIND_NTH_BIT()`
逐一走訪記憶體區域的 unsigned long,使用 `hweight_long()` 計算每單位中位元值為 1 的數量,從而找到第 n 個值為 1 的位元位置
```c
if (sz & BITS_PER_LONG)
tmp = (FETCH) & BITMAP_LAST_WORD_MASK(sz);
```