# 2025q1 Homework2 (quiz1+2) contributed by < [HeLunWu0317 ](https://github.com/HeLunWu0317)> {%hackmd NrmQUGbRQWemgwPfhzXj6g %} ## quiz1 ### 測驗一 引入架構list.h ``` #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 ``` { list_item_t **p; for (p = AAAA; *p != BBBB; p = CCCC); *p = item; DDDD = before; } ``` 以 `p` 做 pointer to pointer 指向`list` l, l從`head`開始,故: AAAA 為 `&l->head` 接下來, 設置結束條件,也就是當找到 insert 目標 node `before`,因此: BBBB 為 'before' 最後做 node 移動,因為 p 為 pointer to pointer 做 p 指向的指標的移動,所以: CCCC 為 '&(*p)->next' 最後結果 `*p` 指向 `before` 之前的 node ,將 item 做 insert , 並將 `item->next` 指向 `before`,因此: DDDD 為 `item->next` ### 延伸問題:合併排序 ``` list_item_t *list_split(list_item_t *head) { if(!head || !head->next){ return NULL; } //快慢指標尋找中間值 list_item_t *slow = head; list_item_t *fast = head->next; while(fast->next && fast){ slow = slow->next; fast = fast->next->next; } list_item_t *t2 = slow->next; slow->next = NULL; return t2; } list_item_t *list_merge(list_item_t *t1, list_item_t *t2) { if (!t1){ return t2; } if(!t2){ return t1; } list_item_t *final; //比較並且放入final list if (t1->value <= t2->value) { final = t1; final->next = list_merge(t1->next, t2); } else { final = t2; final->next = list_merge(t1, t2->next); } return final; } list_item_t *list_merge_sort(list_item_t *head) { if (!head || !head->next) { return head; } // 分割 list_item_t *t2 = list_split(head); // 遞迴排序 head = list_merge_sort(head); t2 = list_merge_sort(t2); // 合併 return list_merge(head, t2); } ``` ### 測驗2 觀察程式碼,EEEE和FFFF所在程式碼區塊都是為了尋找可以使用的 target node ,當target node 擁有 children ,則要尋找替代 parent。 根據註解,尋找替代是透過 inorder 方式,找到左子樹中的最右 node ,`pred_ptr`指向左子樹,程式碼進入: ``` while(EEEE) pred_ptr = FFFF; ``` 推測: 停止條件為,當找不到右 node 時,表示前一步 subtree 擁有最右 node。 因此: * EEEE (迭代尋找):(*pred_ptr)->r * FFFF (目標): &(*pred_ptr)->r #### 補全程式碼 目前需要補全的程式碼包括: 1. `find_free_tree` : 透過迭代搜尋 BST 中的 target node 的指標位置。 2. `find_predecessor_free_tree`: 透過迭代尋找左子樹中最大的節點(可在尋找替代時使用)。 3. `insert_free_node`:使用 BST 插入法,插入空閒節點。 #### find_free_tree 建製 根據 target size 大小決定要去左子樹或是右子樹,直到找到匹配的 size 大小。 ``` if(target->size < (*root)->size) { root = &(*root)->l; } else if(target->size > (*root)->size) { root = &(*root)->r; } ``` #### find_predecessor_free_tree建製 首先,需要透過迭代走到左子樹 再來並需找到左子樹中的最右節點。 ``` block_t *find_predecessor_free_tree(block_t **root, block_t *node) { block_t *pred = node->l; while (pred && pred->r) { pred = pred->r; } return pred; } ``` #### insert_free_node 建製 透過比較 size 大小,決定該去左子樹或是右子樹,直到遇到`*root = NULL `節點,並且將新節點之就左右子樹 node 作清空。 ``` void insert_free_tree(block_t **root, block_t *node) { while (*root) { if (node->size < (*root)->size) root = &(*root)->l; else root = &(*root)->r; } *root = node; node->l = node->r = NULL; } ``` #### 參閱 memory-allocators ### 測驗3 ## quiz2 ### 測驗1 使用 Linux 核心風格 List API 的簡化標頭檔 list.h 來開發快速排序程式碼。 trace 之後,這個演算法以第一個 node 為 pivot: 1. 將 pivot 從原 list 移除。 2. 一次線性掃描把剩餘節點按照與 pivot 的大小關係移動至兩暫存 sublist (list_less 與 list_greater) 過程中,使用函式`list_move_tail`確保每個子串內部維持原先相對順序,掃描後,對兩個 sublist 遞迴呼叫同一函式完成排序,最後透過 `list_add`、`list_splice` 與 `list_splice_tail` 將「小於 pivot 的 sublist + pivot + 大於等於 pivot 的 sublist」重新串回原串位置。 #### 完善程式碼 * AAAA: `list_first_entry` 快速排序中,為了直接取第一個節點當 pivot。 * BBBB: `list_del` 把 pivot 從原 list 移除,避免跟其他元素混在一起。 * CCCC: `list_move_tail` 掃描原順序把 node 接到 sublist 尾端。(不使用`list_move`,否則順序反轉 ) * DDDD: `list_add` 目前 head 為空,把 pivot 放進去。 * EEEE: `list_splice` 把「小於 pivot」sublist 接到 head 前端(pivot 之前)。 * FFFF : `list_splice_tail` 把「大於 pivot」sublist 接到 head 尾端(pivot 之後)。 #### 解釋上方程式碼運作原理 流程如下: 1. 擷取 第一個 node 為 pivot,並且從主 list 中移除。 2. 一次線性掃描,以大小判斷剩餘節點並分組: * 走訪剩餘節點,cmpint() < 0 → 丟到 `list_less` 尾端;否則丟到 `list_greater` 尾端。 * **丟**必須透過`list_move_tail`,才可保持原順序 (lab0作業犯過類似錯誤) 3. 遞迴排序兩個 sublist: * list_quicksort(list_less)、list_quicksort(list_greater)。 4. 三組做拼接: * 先把 pivot 接回 head,再把 `list_less splice` 到前面、`list_greater splice` 到尾端。 #### 為什麼 list_move_tail 不能換成 list_move list_move() 把節點插到目標 list 的第一個元素之**前** 例如: 原本掃描順序:A→B→C 使用 `list_move`,內部順序變成 C→B→A,則無法滿足 stable sorting ### 測驗2 #### 觀察clz2 ``` 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); } ``` * 將 bits 切對半,分為 upper 以及 lower。 * e.g: 0000 0000 0000 0001 1111 0000 0000 0000 -> upper: 0000 0000 0000 0001 lower: 1111 0000 0000 0000 * 以遞迴方式開始計算 leading zero: * 規則: * 若 upper 等於 0,則下一次遞迴呼叫應檢查 lower 部分,並用計數器(counter)記錄目前已累計的 leading zero(等於 upper 位元數)。 * 若 upper 不等於 0,則下一次遞迴呼叫應繼續檢查 upper 部分。 * x : 要 check 的 32-bit 整數 * c : 遞迴的層數,以此決定切割位數(將 bits 切對半用的) * mask[] = {0, 8, 12, GGGG}(不太了解):根據 code 是推導出 lower 的,且將 c 帶入推測其與遞迴有關: 1. 因此當 c 為 0,則 mask[0] = 0,也就是說 0x0000FFFF 不用做右移,與 x 做 AND 的話,upper被清零(變16bits)。 2. 當 c 為 1 則 mask[1] = 8,x此時已變成清0的lower值(16bits),0x0000FFFF被右移 8bits,則變成 0x000000FF,與 x 做 AND ,upper被清零(變8bits)。 3. 當 c 為 2 則 mask[2] = 12,x此時已變成清0的lower值(8bits),0xFFFF被右移 12bits,則變成 0x0000000F,與 x 做 AND ,upper被清零(變4bits)。 4. 當 c 為 3 則 mask[3] = GGGG,x此時已變成清0的lower值(4bits),0xFFFF被右移 GGGG bits,則變成 ??,與 x 做 AND ,upper被清零(變??bits)。 * mask的作用就是找出 lower ?,當 c 等於3的時候,x 有4bits,而這4bits要分割為 2bits upper + 2bits lower,也就是有 2 bits的 lower 要分割,原本 lower 有16bits,因此16-14=2 * **GGGG**為 14。 * magic[] = {HHHH, 1, 0, IIII}(不太了解):推測在遞迴到某一程度時加入`if (c == JJJJ) `觸發magic。 1. lower最多16bits要取,因此變化如下:16, 8, 4, 2。 2. 最後lower只有2bits,lower 可能是十進位的 0、1、2、3,所以有可能是 2、1、0、0 的 lower leading zero。 3. 猜測magic只需要4個值的原因在於只需要遞迴4次,最後一次只需查詢 magic 即可知道 leading zero數量,不用再做 mask。 * **HHHH** : 2 * **IIII** : 0 * **JJJJ** : 3 * 當遞迴到一個階段觸發這段程式碼: ``` if (c == JJJJ) return upper ? magic[upper] : KKKK + magic[lower]; ``` 當遞迴到第3次,此時 x 有 4bits,upper為2bits、lower為2bits,此時有兩種情況: 1. upper非全0,此時查 magic 找 upper 的 leading zero數量(magic[upper] )。 2. upper全0,則必定有兩個 leading zero 了,因此可先加上去2接下來查 magic 找 lower 的leading zero數量 magic[lower]。 * **KKKK** : 2 * 遞迴結尾程式碼: ``` return upper ? clz2(upper, c + 1) : (16 >> (c)) + clz2(lower, c + LLLL); ``` 每次遞迴結尾,已經將x切半,判斷 upper 是否為0: * upper ~~=~~ 0: 下一次遞迴進入`clz2(upper, c + 1)`。 * upper = 0: 前半bits皆為0,可以直接將0的數量加上去 `(16 >> (c))`,只須看lower部分 `clz2(lower, c + LLLL)` * c只會往下一層走,因此為 c+1。 * **LLLL** : 1 #### 觀察clz64 ``` static inline int clz64(uint64_t x) { /* If the high 32 bits are nonzero, count within them. * Otherwise, count in the low 32 bits and add 32. */ return (x >> 32) ? clz32((uint32_t) (x >> 32)) : clz32((uint32_t) x) + 32; } ``` 可以看出就是將 64bits再切扮成 32bits + 32bits的 upper+lower,兩種情況: * upper 非 0 : 將其丟入 `clz32()`。 * upper 為 0 : 直接 加上32,並且做 lower 的 leading zero 確認。 #### 觀察 sqrti() 計算 64-bit 無符號整數的整數平方根 ##### 初始條件判斷 ``` if (x <= 1) return x; ``` 平方根 0 = 0 平方根 1 = 1 ##### 開始做平方根 ``` int shift = (total_bits - 1 - clz64(x)) & MMMM; m = 1ULL << shift; ``` **如果將開根號以二進位觀察,則是將2bits算一格**,例如: 4 = 0010 -> 將2bits算一格 -> 01 -> 2 **而 2bits 為 4的冪** 假如 x = 50, clz64(50) = 58,shift = (64 - 1 - 58) & MMMM = `5 & MMMM` 既然需要是4的冪,則不能有奇數次方,將第一位bit削掉,就是 5 & 111...10 = 5 & ~1 **m** = `1 << (5 & ~1)` (做向左移位) = 1 << 4 = 16、**x** = 50、**y** = 0 進入迴圈第0次: 1. **b** = 0 + 16 = 16 2. `y >>= NNNN `:已確定的根左移一欄,空出下一位。故 **NNNN** = 1, y = 0 3. 50 >= 16 -> true -> 進入 `if (x >= b)` 4. **x** -= b -> x = 50 - 16 = 34 5. **y** += m -> **y** = 16 6. **m** >>= PPPP = 4,將2bits算一格,也就是說此次遞迴做完2bits,故往右走2bits,**PPPP** = 2。 7. **(x , y , m)** = (34, 16, 4) 進入迴圈第1次: .....**(x , y , m)** = (14, 12, 1) 進入迴圈第2次: .....**(x , y , m)** = (1, 7, 0) m 為 0 跳出,得到答案 y = 7。 > b = y + m 的意義是什麼? **m :** * 從最高位元,每次處理2bits的一組數值後往下2bits處理,類似掃描。 **x :** * 一開始是輸入的整數 x * 隨著每次確認好**根bits**後,就會更新。 * 表「剩下可以讓平方根繼續加的量」。 **b :** `b = y + m;` ,也就是類似一個預測目前算好的根bits加上下一組要處理的bits時,是否會有問題。 ``` if (x >= b) { x -= b; y += m; } ``` 進入比較,看剩下的 x 值是否夠放得下這些位元,如果夠就可以留下來。 **y :** 是目前處理完的累積下來的根,也是最後的解答。 #### 擴充為向上取整數實作 #### 不依賴除法指令的 sqrtf #### 「藉由查表的平方根運算」,以 C 語言實作,並比較不同手法的整數開平方根效能 ### 測驗3 #### map_init() 基本建立 hash_table。 配置記憶體空間,為 map_t 結構大小: ``` map_t *m = malloc(sizeof *m); ``` map_t結構: ``` typedef struct { int bits; struct hlist_head *ht; } map_t; ``` 存入得到的bits,bits 決定雜湊表大小:bucket 數量為 2^bits。 ``` m->bits = bits; ``` 配置 bucket 陣列: ``` m->ht = calloc(MAP_HASH_SIZE(bits), sizeof(struct hlist_head)); ``` * 使用 calloc的原因,根據C99規格書中,關於`calloc()`函式: > 7.20.3.2 The free function > The calloc function allocates space for an array of nmemb objects, each of whose size is size. The space is initialized to all bits zero. 推測是為了讓所有 bucket 初始為 0 bits。 * `MAP_HASH_SIZE(bits) `是一個巨集,定義為: ``` #define MAP_HASH_SIZE(bits) (1u << (bits)) ``` 也就是設置 2^bits 個 bucket。 * 每個 bucket 是 `struct hlist_head`,也就是原始碼中 type.h: ``` struct hlist_head { struct hlist_node *first; }; ``` * 初始化 table 完成後,回傳指向這張 map 的指標。 #### map_add() 插入 key→data * `if (find_key(m, key)) return; ` : 防止覆蓋 * 存入 data (陣列位置) 以及 key (值) * `struct hlist_head *h = &map->ht[hash(key, BBBB)];` : 需要知道 map 到哪一個 bucket,而 table 是透過 bits 做建構,因此需要的是 bits,故 **BBBB** = `map->bits` 。 ``` n->next = first; if (first) CCCC = &n->next; ``` * 目前這個 bucket 的第一個節點是 `first`。 * 將新的 node 插入最前面,則first變成舊的,也就是`n->next`,則也要更新 prev,因此 **CCCC** = `first->pprev`。 ``` h->first = n; DDDD = &h->first; ``` * 當 n 成為 bucket 的第一個節點,它的 pprev 就該指向 bucket 本身的 first 欄位。 * **DDDD** = n->pprev #### map_deinit() 目的應該是釋放整個 hash_table 的記憶體。 **掃描每個 bucket :** ``` for (int i = 0; i < MAP_HASH_SIZE(map->bits); i++) { struct hlist_head *head = &map->ht[i]; ``` * hash_table 是一組 bucket 陣列,而每個 bucket 是一條 hlist 的 list。 * ` i < MAP_HASH_SIZE(map->bits)` : 也就是從 0 到 2^bits - 1 ,每個 bucket 逐次整理。 * `map->ht[i]` 就是一條單向鏈(`struct hlist_head`)的起點。 **掃描此 bucket 的 hlist :** ``` 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; ``` * 目的 : 進入 bucket 後,開始掃描此 bucket 的 hlist。 * 利用 `container_of() `巨集從 node 指回 paerent 結構。 > 巨集 container_of 則在 offsetof 的基礎上,擴充為「給定成員的位址、struct 的型態,及成員的名稱,傳回此 struct 的位址」 * parent 結構: ``` struct hash_key { int key; void *data; struct hlist_node node; }; ``` * 透過 node 就能知道整個 kn 結構。 **移除節點前置 :** ``` struct hlist_node *next = n->next, **pprev = EEEE; ``` * 推測是為了輔助知道目前要釋放的 bucket 的位置,可以以此推斷是在中間或是開頭。 * `pprev` 是「指向上一個節點的 `next` 欄位」的位址 * 因此 **EEEE** = n->pprev ![image](https://hackmd.io/_uploads/rylXw8LXgg.png) **開始移除節點 :** ``` *pprev = next; if (next) next->pprev = pprev; ``` * 修改目前節點的前一節點的 data 值為目前節點的下一 node。 ![image](https://hackmd.io/_uploads/BJ-6vU8mgx.png) **清除資料,釋放記憶體 :** ``` bail: free(kn->data); free(kn); free(map); ``` #### two_sum() ``` map_t *map = map_init(10); *returnSize = 0; int *ret = malloc(sizeof(int) * 2); if (!ret) goto bail; ``` * 設置 10 bits,也就是 2^10 = 1024 個 bucket。 * `target`,要湊出的目標和。 * `returnSize`,答案陣列的長度,預設為 0。 * 先設置好 malloc `ret` 存回傳結果。 ``` for (int i = 0; i < numsSize; i++){ ... } ``` * 進入迴圈開始掃描湊數字和為 `target`。 * 根據測驗3說明,如果 `target` 是 a + b,那麼只要曾經看過 a,現在出現 b 就能湊對。 ``` int *p = map_get(map, target - nums[i]); ``` 其中 `target - nums[i]`: * 確認扣除已經掃描的部分,還剩下的數值。 * 例如,看到第 i 的數,則要與目前的數字 a 湊對的話,需要掃描 a 之後,也就是 target - nums[i]。 則整段程式碼就是,以 map_get 查表,查`target - nums[i]` 1. 假如查到了: ``` if (p) { /* found */ ret[0] = i, ret[1] = *p; *returnSize = 2; break; } ``` * 代表第 i 個數就是我們所求湊對,將 `ret[0] = i`,以及之前查到遇過的值`*p`,設置`ret[1] = *p` 2. 假如沒查到: ``` p = malloc(sizeof(int)); *p = i; map_add(map, nums[i], p); ``` * 把現在這個數字(`nums[i]`)的索引裝進指標裡,加入 hash_table。