# 2025q1 Homework2 (quiz1+2) contributed by < TonyLinX > ## 2025q1 第 1 週測驗題 ### 測驗 `1` 這段程式碼定義單向 linklist 的 `node` 以及串列整個 `linklist` 的 `list_t` : ```c #include <stddef.h> typedef struct list_item { int value; struct list_item *next; } list_item_t; typedef struct { struct list_item *head; } list_t; ``` 測試函式 `list_insert_before` 這個函式的功能是在指定的 `before` 節點前插入新的 `node`,並且會遍歷 `linklist` 來找到 `before` 節點的位置。 ![螢幕擷取畫面 2025-03-17 190254](https://hackmd.io/_uploads/S1vOWYHn1l.png) 先來討論另個想法,實際上如果只是要遍歷整個 `linklist` 不需要 `**p`,只需要 `*p`,因為只要創建一個指向 `list_item_t` 的指標並讓它指向 `l->head`,並一路透過 `next` 即可往後遍歷。 ```c void list_traverse(list_t *l) { list_item_t *p = l->head; while (p != NULL) { printf("%d\n", p->value); p = p->next; } } ``` 那為什麼我們需要雙重指標 (`**p`),因為我們要修改結構,如果說是單指標就會在 `p = item` 的地方出問題。因為 `p` 本身是局部變數,`p = item` 只是讓 `p` 去指向另個位址,但是整個 `linklist` 結構並不會改變,所以說我們需要 `**p`。 > 這裡其實可以想像成,如果傳送過去的指標是要被改變去指向另一個位址,那就必須要用雙重指標,因為你需要去改變這個指標的值,但是如果你只是要透過指標操作它所指向的內容,而不需要改變它本身的值(指向位址),那只需要單指標就夠了。 ```c void list_insert_before(list_t *l, list_item_t *before, list_item_t *item) { list_item_t *p; for (p = l->head; p != before; p = p->next) ; p = item; item->next = before; } ``` 假設鏈結串列是:`head -> A -> B -> C -> NULL`,而 `before` 是 `B`,你想在 `B` 前插入 `item`,目標是讓鏈結串列變成:`head -> A -> item -> B -> C -> NULL`。 `list_insert_before` 的流程會先透過 `for loop` 尋找到 `before` 前面的節點,所以先初始化 `p` 為 `l->head` 的地址,然後比對 (*p) 是否為 `before`,如果不是 `before` 就往下一個節點移動, `p = &(*p)->next` 中的 `*p` 代表的是指向某一個 `node` 的指標,所以我們可以用他的成員 `next` 進行移動。 當遇到 `before` 時,p為 `A->next` 的地址,所以 `*p` 就是 `A->next`,那我們就可以透過 `*p = item` 插入 `item`,最後再讓 `item` 的 `next` 指向 `before` 就可完成這個 `linklist`。 ```c void list_insert_before(list_t *l, list_item_t *before, list_item_t *item) { list_item_t **p; for (p = &l->head; *p != before; p = &(*p)->next) ; *p = item; item->next = before; } ``` :::spoiler 詳細步驟 * 初始化: `p = l->head`,所以 `p` 指向 `A`(地址 `0x1000`)。 * 迴圈執行: * 第一次:`*p != before(0x1000 != 0x2000)`,`p = &((*p)->next)`: * `*p` 是` A(0x1000)`,`(*p)->next` 是 `A->next`,也就是 `B(0x2000)`。 * `&((*p)->next)` 是 `A->next` 的地址(假設是 `0x1004`,因為 `next` 是 `A` 結構中的成員)。 * 所以` p` 更新為` &A->next(0x1004)`。 * 第二次:`*p == before(0x2000 == 0x2000)`,條件不成立,離開迴圈。 * 結果:迴圈結束時,`p` 指向 `A->next` 的地址`(0x1004)`。 * `*p = item`: * `p` 是 `&A->next(0x1004)`,`*p` 是 `A->next` 的值(原本是` 0x2000`,指向 `B`)。 * `*p = item` 把 `A->next` 修改成 `item` 的地址(`0x4000`)。 * 現在:`l->head -> A -> item`,鏈結串列結構改變了! * `item->next = before`: * `item->next` 被設為 `B(0x2000)`。 * 結果:`l->head -> A -> item -> B -> C`,插入成功! ::: #### 撰寫合併排序操作 : Commit [`2b418c3`](https://github.com/TonyLinX/linux_hw2/commit/2b418c3aa138831fd42de9cbe40b26dcfc24b019) 首先,設計 `merge sort` 的 `test case`,這邊的設計方式透過 `list_insert_before` 創建一個 `999 -> 998 -> ... -> 0` 的 `linklist`,期望再透過 `merge sort` 之後可以變成 `0 -> 1 -> ... -> 999`。而 `merge sort` 的方式採用 `top-down` 的方式實現,也就是遞迴的方式。 ### 測驗 `2` 這段 code 主要是在做,當某個 free block 被 malloc() 拿去用了,要將 BST 上的 free block 移除。 ```c // 先從 BST 上找到 target node. block_t **node_ptr = find_free_tree(root, target); ``` 由於要維護 BST 的順序,在刪除上會有三種 case,每一種 case 要做的事情都不同,分成以以下三種: - 有左右子樹 `if ((*node_ptr)->l && (*node_ptr)->r)` => - 找目標節點的左子樹中最大的值 ```c block_t **pred_ptr = &(*node_ptr)->l; while (EEEE) pred_ptr = FFFF; ``` :::info 所以說 `EEEE` 為 `(*pred_ptr)->r` , `FFFF` 為 `&(*pred_ptr)->r` ::: - 找目標節點的右子樹中最小的值 - 只有左子樹或右子樹 `else if ((*node_ptr)->l || (*node_ptr)->r) {}` => 直接把左子樹或右子樹補上去 - 沒有任何子樹 => 直接設定成 `NULL` 下面這段 code 主要的設計是考慮到 `pred_ptr` 的位置,如果說 `pred_ptr` 是目標節點左子樹的根,那可以直接替換上去,但如果是在非常深處的 leaf ,由於沒辦法得知 `pred_ptr` 的父節點,所以只能透過遞迴的方式把它找出來並且移除,然後再把它拿去替換。 ```c /* If the predecessor is the immediate left child. */ if (*pred_ptr == (*node_ptr)->l) { block_t *old_right = (*node_ptr)->r; *node_ptr = *pred_ptr; /* Replace target with its left child. */ (*node_ptr)->r = old_right; /* Attach the original right subtree. */ assert(*node_ptr != (*node_ptr)->l); assert(*node_ptr != (*node_ptr)->r); } else { /* The predecessor is deeper in the left subtree. */ block_t *old_left = (*node_ptr)->l; block_t *old_right = (*node_ptr)->r; block_t *pred_node = *pred_ptr; /* Remove the predecessor from its original location. */ remove_free_tree(&old_left, *pred_ptr); /* Replace the target node with the predecessor. */ *node_ptr = pred_node; (*node_ptr)->l = old_left; (*node_ptr)->r = old_right; assert(*node_ptr != (*node_ptr)->l); assert(*node_ptr != (*node_ptr)->r); } ``` ### 測驗 `3` 這個題目主要是在做 QuickSort,常見的 QuickSort 是會透過 swap 以及 recursive 的方式實現排序。在這裡不會使用以往 recursive 的方法,而是改成 stack 來模擬原本的遞迴呼叫。 * begin[i], end[i] 分別用來存放單一排序分割的起始點和終點,i 為分割編號 * left 用來存放小於等於 pivot 的節點 * right 用來存放大於 pivot 的節點 * result 用來存放排序完的分割 具體來說,他會先找一個 `pivot`,並取比較剩下的 `node_t` 的 value,如果比 `pivot` 還小就接在`left`的尾部,如果比較大就分給 `right` 的尾部,所以當處理完所有的 `node_t`,你就會得到三組分割 `left`,`pivot`, `right`。接下來,這三組分別對應 `i`,`i+1`,`i+2`,各自將自己的第一個 `node_t` 加入到 `begin[i]`, `begin[i+1]`, `begin[i+2]`,並也將自己的最後一個 `node_t` 加入到 `end[i]`, `end[i+1]`, `end[i+2]`。 最後,就從最後面的部分開始排序。 這段 code 主要是當不只一個 `node_t`,必須根據 `pivot` 將所有的 `node_t` 分成 `left` 以及 `right`, :::info * `value` 代表的是 `pivot` 的值,由於 `pivot` 是 `list_head`,所以要先用 `list_entry` 找到 `node_t`,才可以得到 `value`,所以 `HHHH` = `list_entry(pivot,node_t,list)->value`。 * `n_value`代表的是 `n` 的值,所以 `IIII` = `list_entry(n,node_t,list)->value` * `begin[i]`,`begin[i + 1]`,`begin[i + 2]`,對應到的是三組分割 `left`,`pivot`,`right`,所以 `JJJJ` = `pivot`, `KKKK` = `right`。 ::: ```c if (L != R) { struct list_head *pivot = L; value = /* HHHH */ struct list_head *p = pivot->next; pivot->next = NULL; /* break the list */ while (p) { struct list_head *n = p; p = p->next; int n_value = /* IIII */; if (n_value > value) { n->next = right; right = n; } else { n->next = left; left = n; } } begin[i] = left; begin[i + 1] = /* JJJJ */; begin[i + 2] = /* KKKK */; left = right = NULL; i += 2; } ``` 這段 code 主要是維護環狀的 double-linklsit。 :::info `GGGG = head->prev=prev` ::: ```c static void rebuild_list_link(struct list_head *head) { if (!head) return; struct list_head *node, *prev; prev = head; node = head->next; while (node) { node->prev = prev; prev = node; node = node->next; } prev->next = head; /* GGGG */; } ``` ## 2025q2 第 2 週測驗題 ### 測驗 `1` 這段 code 主要是在對 `values` 進行賦值以及打亂。`j` 代表是 index ```c #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0])) static uint16_t values[256]; static inline uint8_t getnum(void) { static uint16_t s1 = UINT16_C(2); static uint16_t s2 = UINT16_C(1); static uint16_t s3 = UINT16_C(1); s1 *= UINT16_C(171); s1 %= UINT16_C(30269); s2 *= UINT16_C(172); s2 %= UINT16_C(30307); s3 *= UINT16_C(170); s3 %= UINT16_C(30323); return s1 ^ s2 ^ s3; } static uint16_t get_unsigned16(void) { uint16_t x = 0; size_t i; for (i = 0; i < sizeof(x); i++) { x <<= 8; x |= getnum(); } return x; } static inline void random_shuffle_array(uint16_t *operations, uint16_t len) { for (uint16_t i = 0; i < len; i++) { /* WARNING biased shuffling */ uint16_t j = get_unsigned16() % (i + 1); operations[i] = operations[j]; operations[j] = i; } } ``` 這個部分是實作 `quicksort`,這裡的 code 跟第一周測驗三有點雷同,因為都會切成三塊,這裡的 `less`, `pivot`, `greater` 對應到那第一周測驗三的 `left`, `pivot`, `right`,整體流程為: - 先提取 `pivot` 的 `listietm`,並將 `pivot` 分割出來。 - 根據 `pivot` 將小於的點接在 `list_less` 後面,而大於的點接在 `list_greater` 後面。 - 各自將 `list_less` 以及 `list_greater` 進行遞迴排序。 - 最後,再將這三塊接回 head。 :::info - 用第一個 `listitem` 做為`pivot`,所以 `AAAA` = `list_first_entry` 。 - 將 `pivot` 分割出來,所以 `BBBB` = `list_del`。 - 將大於的 `pivot` 的 `listitem` 接在 `list_greater` 後面,所以 `CCCC` = `list_move_tail`。 - 由於 `pivot` 是一個點,所以一定是 `list_add`,所以 `DDDD` = `list_add` (但我覺得 `list_add_tail` 也可以)。 - 由於 `list_less` 是比 `pivot` 還小的值,且可能為多個點,所以 `EEEE` = `list_splice`。 - 由於 `list_greater` 是比 `pivot` 還大的值,且可能為多個點,所以 `FFFF` = `list_splice_tail`。 ::: ```c { struct list_head list_less, list_greater; struct listitem *pivot; struct listitem *item = NULL, *is = NULL; if (list_empty(head) || list_is_singular(head)) return; INIT_LIST_HEAD(&list_less); INIT_LIST_HEAD(&list_greater); pivot = AAAA(head, struct listitem, list); BBBB(&pivot->list); list_for_each_entry_safe (item, is, head, list) { if (cmpint(&item->i, &pivot->i) < 0) list_move_tail(&item->list, &list_less); else CCCC(&item->list, &list_greater); } list_quicksort(&list_less); list_quicksort(&list_greater); DDDD(&pivot->list, head); EEEE(&list_less, head); FFFF(&list_greater, head); } ``` #### 待完成 延伸問題 ### 測驗 `2` clz2() 這支函式用遞迴的方式計算 32 bit 無號整數 x 的前導零個數:它把輸入逐層一分為二,c = 0 代表先把 32 bit 切成高 16 bit (upper) 與低 16 bit (lower),若 upper 全為 0,就可以直接把 16 累加到答案並改遞迴處理 lower;反之,若 upper 非 0,表示那 16 bit 內尚未遇到 1,就只遞迴 upper 繼續細分成 8/4/2 bit 區段。這個過程隨 c 增大而重複,區段長度依序是 16, 8, 4, 2 bit(所以 c 最大值為 3);到最底層 (c == 3) 時,upper 只剩 2 bit,可能是 00, 01, 10, 11,對應要加的前導零個數可透過 magic[] 查表一次取得,同時若 upper 為 0 則還需把 magic[lower] 加進去。mask[] 是遮罩,是為了從 x 取出正確長度的 lower;magic[] 是當 lower 只剩下 2 bit 用來查表,由於只會是 00, 01, 10, 11,剛好可以對應為 {2, 1, 0, 0}。如果最一開始 x == 0 且 c == 0,代表 32 bit 全零,直接回傳 32。 :::info - 由於 `lower` 最後只需要 2 bit,所以 `GGGG = 14`。 - 00, 01, 10, 11 對應 {2, 1, 0, 0},所以`HHHH = 2`, `IIII = 0`。 - 因為是 32 bit,區段長度依序是 16, 8, 4, 2 bit,所以 `JJJJ = 3`。 - 當 `upper` 以及 `lower` 為 2 bit 的時候,在 `upper == 0` ,才會觸發 `KKKK + magic[lower];`,所以 `KKKK = 2`。 - 當 `c != 3`,不管 `upper` 是不是 0,c 都要加 1,繼續計算 `clz`,所以 `LLLL = 1`。 ::: ```c static const int mask[] = {0, 8, 12, GGGG}; static const int magic[] = {HHHH, 1, 0, IIII}; unsigned clz2(uint32_t x, int c) { if (!x && !c) return 32; uint32_t upper = (x >> (16 >> c)); uint32_t lower = (x & (0xFFFF >> mask[c])); if (c == JJJJ) return upper ? magic[upper] : KKKK + magic[lower]; return upper ? clz2(upper, c + 1) : (16 >> (c)) + clz2(lower, c + LLLL); } ``` 這支 `sqrti()` 函式透過「二進位長除法」計算 64 bit 整數平方根。流程如下:先用 `clz64(x)` 找出最高的 1 所在位元索引 h = `63 - clz64(x)`,再把 LSB 清 0(`shift = h & ~1ULL`),確保起始位階是偶數,因為開平方一次處理的是 2 bit ,所以起始位置一定要是偶數。 在迴圈中,每回合先計算出除數 `b = y + m`,接著把 `y` 右移 1(從 2 r 還原為 r);若 `x ≥ b` 就說明這個 bit 應設 1 ,把 `x -= b` 並將 `y += m`;無論成敗,移到下一組 2 bit,繼續判斷。當 `m` 減到 0,`y` 即為 ⌊√x⌋。 :::info - 由於起始位置要是偶數,所以要將 LSB 清成 0 ,所以 `MMMM` = `~1` - 由於這裡 `y` 代表的是 2r,要變成 r ,所以 `NNNN` = `1`。 - 每次做完移到下組 2 bit,所以 `PPPP` = `2`。 ::: ```c uint64_t sqrti(uint64_t x) { uint64_t m, y = 0; if (x <= 1) return x; int total_bits = 64; /* clz64(x) returns the count of leading zeros in x. * (total_bits - 1 - clz64(x)) gives the index of the highest set bit. * Rounding that index down to an even number ensures our starting m is a * power of 4. */ int shift = (total_bits - 1 - clz64(x)) & MMMM; m = 1ULL << shift; while (m) { uint64_t b = y + m; y >>= NNNN; if (x >= b) { x -= b; y += m; } m >>= PPPP; } return y; } ``` #### 待完成 延伸問題 ### 測驗 `3` 測驗 3 是要用 hash table 的方式完成 Two sum 這個題目。 這段 code 是在建立一個 hash table,假設 `bits` = $10$,代表這個表會有 $2^{10}$ = $1024$ 個 buckets。每一個 bucket 對應一個 `struct hlist_head`,它的結構中包含一個 `struct hlist_node *first`,這是該 bucket 所屬 double linklist 的開頭指標,用來儲存碰撞時的多個 key-value 節點。我們透過 `malloc(sizeof(struct hlist_head) * MAP_HASH_SIZE(bits))` 一次配置出整塊 bucket 陣列的空間。雖然 `map->ht` 是一個指標,但它實際上指向的是一段連續的記憶體空間,因此可以使用陣列方式操作(例如 `map->ht[i]`)。接下來透過迴圈逐一初始化每個 bucket 的 `first` 成員為 `NULL`,表示目前所有 buckets 都是空的、尚未儲存任何資料。 ```c 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; } ``` `map_deinit()` 是用來釋放整個雜湊表 `map_t` 所使用的記憶體資源,其核心流程如下: - 每個 bucket 是一個 `struct hlist_head`,其中的 `first` 成員指向該 bucket 的鏈結串列起點。 - 使用 `MAP_HASH_SIZE(bits)` 計算出雜湊表大小(實際上是 $2^{\text{bits}}$ 個 buckets)。 - 若 bucket 為空(`first == NULL`),代表此 bucket 沒有資料,會被自動略過。 - 若 bucket 非空,則遍歷整條鏈: - 使用 `container_of()` 從 `hlist_node` 取回對應的 `struct hash_key`。 - 將節點從 `hlist` 中移除:這透過 `next` 與 `pprev` 指標操作來實現。 - 若 `n->pprev == NULL`,代表此節點尚未被插入(unhashed),會直接跳過斷鏈處理。 - 每個節點的 `data` 與節點本身都會被 `free()` 掉。 - 迴圈結束後,釋放 `map` 本身的記憶體 ```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); } ``` 這段 code 主要是去搜尋 hash table 中有沒有該 `key` 值。 ```c void *map_get(map_t *map, int key) { struct hash_key *kn = find_key(map, key); return kn ? kn->data : NULL; } ``` 有了 `key` 值,先透過 hash function 計算出是哪一個 bucket,得到 bucket 的 index 後,用 for 迴圈去走訪整個 hlist,這個 hlist 是由一堆 `hash_key` 組成,不是 `hlist_node`,`hlist_node` 是 `hash_key` 的成員,用來串接所有 `hash_key`,所以為了知道該 `hash_key` 中的 `key`值,必須先透過 `container_of` 得到該 `hash_key` 。若有找到就回傳該 `hash_key`,沒有就回傳 `NULL`。 ```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; } ``` 這個 hash function 的設計方式是將 key 值乘上黃金比例常數,藉此打亂其位元分布、減少碰撞,這是來自 Donald Knuth 在《The Art of Computer Programming》中的建議。`bits` 則用來控制雜湊表的大小,決定 bucket 的數量。例如當 `bits = 10` 時,雜湊表會有 $2^{10} = 1024$ 個 buckets。整個 hash 的結果會將 key 乘上黃金比例後,再將 32-bit 結果**右移** $(32 - \text{bits})$ 位,也就是右移 22 位,讓 index 落在 `[0, 1023]` 的範圍內。 ```c // Hash function #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 table 中找到,在 hash table 中建立該 key 值的 hash_key。 ```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; } ``` :::info * 由於都是呼叫 hash function 得到 bucket index,所以 `AAAA = map->bits` 以及 `BBBB = map->bits`。 * 為了維護整個 double linklist 的結構,所以 `CCCC = first->pprev` 以及 `DDDD = n->pprev`。 * 為了從 hlist 上移除 `n`,要先取得 `n` 的前一個以及下一個 `hash_key`,`EEEE = n->pprev`。 ::: #### 待完成 延伸問題