# 2025q1 Homework2(quiz1+2) contribute by [dingsen-Greenhorn](https://github.com/dingsen-Greenhorn/lab0-c) {%hackmd NrmQUGbRQWemgwPfhzXj6g %} --- ## Week1_Quiz ### 測驗1-1 #### 首先定義要用到的程式 ``` #include <stddef.h> typedef struct list_item { int value; struct list_item *next; } list_item_t; typedef struct { struct list_item *head; } list_t; ``` #### 參考示意圖如下: ![download](https://hackmd.io/_uploads/rJsjmTfnJl.png) #### 此處實做 list_insert_before ``` 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; } ``` list_insert_before在單向鏈結串鏈中,在指定節點的前面插入新節點 而迴圈中,讓 p 持續向右移動,當 *p == before 時,代表此時 **p 是指向前一個節點的 *next 也就是停止處。再讓 item->next 指向 before 此時示意圖如下便將節點插入指定節點之前。 ![download1](https://hackmd.io/_uploads/ryPtJCMh1x.png) --- 假設我們有一個鏈結串列,如下所示: ``` head -> 0 -> 1 -> 2 -> 3 -> NULL ``` 我們要在節點 2 之前插入一個新節點 99。 初始化指標 p:p 開始指向 head。 遍歷鏈結串列:p 依次指向節點 0, 1, 2,直到找到要插入的位(2)。 插入節點 99:當 p 指向節點 2 時,我們將 99 插入到它之前,將 p 指向 99,並將 99->next 指向原來的節點 2。 結果會變成: ``` head -> 0 -> 1 -> 99 -> 2 -> 3 -> NULL ``` 這樣,99 節點就被成功插入到 2 節點之前了。 #### 在現有的程式碼基礎上,撰寫合併排序操作 --- ### 測驗1-2 現在我們刪除 30: ``` [50] / \ [30]* [70] / \ / \ [20] [40] [60] [80] ``` #### 步驟 1:找到 30 使用 find_free_tree(&root, 30): ``` block_t **find_free_tree(block_t **root, block_t *target) { block_t **current = root; while (*current) { if (*current == target) { return current; // 找到目標,返回指標 } else if (target->size < (*current)->size) { current = &((*current)->l); // 向左子樹搜尋 } else { current = &((*current)->r); // 向右子樹搜尋 } } return NULL; // 沒找到 } ``` 延續以上的例子說明: target = 30 current = &root(指向 50) 30 < 50,往左走 → current = &(50->l)(指向 30) 找到 30,返回 &(50->l) #### 步驟 2:刪除 30 ``` if ((*node_ptr)->l && (*node_ptr)->r) 若 target 節點同時有左、右子樹,則: 找到「中序前驅 (in-order predecessor)」,即左子樹中最大的節點。用這個前驅節點來取代 target。 block_t **pred_ptr = &(*node_ptr)->l; while ((*pred_ptr)->r != NULL) pred_ptr = pred_ptr = &(*pred_ptr)->r; ``` 30 有兩個子節點 (20 和 40),所以需要找到 前驅節點(左子樹的最大值)。20 是 30 的左子樹,20 沒有右子節點,因此 20 就是 30 的前驅。 #### 步驟 3:用 20 取代 30 20 取代 30 40(原本 30 的右子節點)保持不變 刪除原本的 20 節點 ``` [50] / \ [20] [70] \ / \ [40] [60] [80] ``` 30 成功被刪除,BST 結構仍然有效! #### target 的前驅更深(在左子樹更深處) 當 target 左子樹更深時,我們需要找到左子樹最右側的節點: ``` else { block_t *old_left = (*node_ptr)->l; block_t *old_right = (*node_ptr)->r; block_t *pred_node = *pred_ptr; /* 先遞迴刪除前驅 */ remove_free_tree(&old_left, *pred_ptr); /* 用前驅來替換 target */ *node_ptr = pred_node; (*node_ptr)->l = old_left; (*node_ptr)->r = old_right; } ``` 範例 (前驅節點更深): ``` [50] / \ [30] [70] / \ [10] [40] \ [20] ``` 刪除 30: target = 30 前驅 = 20(30 左子樹的最大值) 遞迴刪除 20 用 20 取代 30 #### 參閱 memory-allocators,針對你補完 (及後續改進) 的記憶體配置器,進行效能評比,並解讀其效能表現 ### 測驗1-3 使用指標 L 和 R 指向當前串列分割區間的兩端,當 L 和 R 尚未交會時,進行內部排序。 將最左側的節點設為 pivot,並讓指標 p 指向 pivot 的下一個節點,同時將 pivot.next 設為 NULL,以切斷與後續節點的連結。 指標 p 向右移動,遍歷剩餘節點: 若 p 指向的值大於 pivot,則將該節點加入 right 子串列。 若 p 指向的值小於 pivot,則將該節點加入 left 子串列。 完成遍歷後,得到三個部分:left、pivot 和 right,其對應的分割編號分別為 i、i+1 和 i+2。 優先對 right(分割編號 i+2)進行排序,直至右側排序完成後,將排序結果存入 result。 最後,開始對 left(分割編號 i)進行排序,直到所有部分都排序完成。 #### 研讀〈A comparative study of linked list sorting algorithms〉,分析針對雙向鏈結串列的排序演算法考量,並運用 Linux 核心風格的 List API 予以實作 --- ### 測驗2-1 這是一個基於 Linux 核心 list.h API 的 穩定版快速排序 (QuickSort),適用於雙向鏈表 (list_head)。 它的主要目標是 保持相同數值的順序 (stable sorting),並且不需要額外的數組存儲資料,而是直接操作鏈表。 ##### 1. 終止條件 這是一個基於 Linux 核心 list.h API 的 穩定版快速排序 (QuickSort),適用於雙向鏈表 (list_head)。 ``` if (list_empty(head) || list_is_singular(head)) return; ``` 當 head 是空的 (list_empty(head)) 或只有一個節點 (list_is_singular(head)),則已經是排序好的,不需要繼續遞迴。 ##### 2. 初始化兩個子鏈表 ``` INIT_LIST_HEAD(&list_less); INIT_LIST_HEAD(&list_greater); ``` 這兩個鏈表分別用來存放: list_less → 存放小於 pivot 的節點。 list_greater → 存放大於或等於 pivot 的節點。 ##### 3. 選擇 Pivot ``` pivot = list_first_entry(head, struct listitem, list); ``` 使用 list_first_entry() 取得 head 的第一個節點作為 pivot (基準值)。 ##### 4. 移除 Pivot ``` list_del(&pivot->list); ``` 將 pivot 暫時移除,避免影響排序過程。 ##### 5. 遍歷 head,將節點移動到 list_less 或 list_greater ``` list_for_each_entry_safe (item, is, head, list) { if (cmpint(&item->i, &pivot->i) < 0) list_move_tail(&item->list, &list_less); else list_move_tail(&item->list, &list_greater); } ``` 這裡使用 Linux 核心 list.h 提供的 宏函數 來遍歷 head,並根據 cmpint() 的比較結果: 小於 pivot 的節點移動到 list_less。 大於或等於 pivot 的節點移動到 list_greater。 這裡保持穩定排序: cmpint() 只在 < 0 時才移動到 list_less,其餘情況都留在 list_greater。 這樣相同數值的元素會維持它們原本的順序。 ##### 6. 遞迴排序 list_quicksort(&list_less); list_quicksort(&list_greater); 將 list_less 和 list_greater 各自再進行 list_quicksort(),直到排序完成。 ##### 7. 合併排序結果 ``` list_add_tail(&pivot->list, head); list_splice(&list_less, head); list_splice_tail(&list_greater, head); ``` 排序完成後,將 list_less、pivot、list_greater 按順序合併回 head: list_splice(&list_less, head); 先把小於 pivot 的數值加回 head,這樣 pivot 仍然位於中間。 list_add_tail(&pivot->list, head); 加回 pivot,讓它處於正確的位置。 list_splice_tail(&list_greater, head); 最後把大於或等於 pivot 的數值加回 head。 執行流程範例 假設我們有一組隨機排列的鏈表: ``` [3] → [1] → [4] → [1] → [5] → [9] → [2] → [6] → [5] ``` 選擇 pivot: pivot = 3 重新分配: list_less = [1] → [1] → [2] list_greater = [4] → [5] → [9] → [6] → [5] 遞迴排序 list_less: pivot = 1 重新分配: list_less = [] list_greater = [1] → [2] 排序 list_greater: pivot = 1 list_less = [] list_greater = [2] list_less 和 list_greater 併入 head 遞迴排序 list_greater: pivot = 4 重新分配: list_less = [] list_greater = [5] → [9] → [6] → [5] 排序 list_greater pivot = 5 list_less = [] list_greater = [9] → [6] → [5] 排序 list_greater pivot = 9 list_less = [6] → [5] list_greater = [] 排序 list_less pivot = 6 list_less = [5] list_greater = [] 最終合併,排序好的鏈表: ``` [1] → [1] → [2] → [3] → [4] → [5] → [5] → [6] → [9] ``` #### 延伸問題1 > 解釋上方程式碼運作原理,改進 `random_shuffle_array` 也探討 `list_for_each_entry_safe` 建構的迴圈中,若將 `list_move_tail` 換為 `list_move`,為何會無法滿足 stable sorting #### 延伸問題2 > 將程式碼整合進 [lab0](https://hackmd.io/@sysprog/linux2025-lab0) 提及的 `qtest`,並探討環狀雙向鏈結串列的排序效能,並善用 [perf](https://hackmd.io/@sysprog/linux-perf) 一類的效能分析工具,探討包含 Linux 核心 [list_sort](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) 在內排序實作的效能表現並充分解釋 ### 測驗2-2 #### 首先我們需要深入了解『clz2』函式 目標是計算一個 32 位元整數的 leading zeros,即數字前面有多少個 0 ``` static const int mask[] = {0, 8, 12, 14}; static const int magic[] = {2, 1, 0, 0}; ``` mask 和 magic 是靜態常數陣列,應該是用來輔助計算的, mask 似乎與 c 相關,影響低位元的遮罩 (0xFFFF >> mask[c])。 magic 可能是查表用來快速計算特定位數的前導零。 ``` clz2(uint32_t x, int c) ``` 這是計算 x 的前導零數的遞迴函式: ``` unsigned clz2(uint32_t x, int c) { if (!x && !c) return 32; ``` 如果 x == 0 並且 c == 0,則 x 完全沒有有效位,直接回傳 32 (32 位元的數字全為 0)。 ``` uint32_t upper = (x >> (16 >> c)); ``` 取得 x 的高位部分。 16 >> c 的作用是根據 c 的不同,把 x 逐步切割,初始時取前 16 位,然後 8 位,4 位,直到最小單元。 ``` uint32_t lower = (x & (0xFFFF >> mask[c])); ``` 取得 x 的低位部分,mask[c] 控制低位遮罩。 ``` if (c == JJJJ) return upper ? magic[upper] :2+ magic[lower]; ``` c == JJJJ 時,這應該是基底情況 (停止遞迴的條件)。 若 upper 不為 0,則回傳 magic[upper] (透過查表獲取結果)。 否則,回傳 2 + magic[lower] (可能是補充偏移量)。 ``` return upper ? clz2(upper, c + 1) : (16 >> (c)) + clz2(lower, c + 1); ``` 若 upper 不為 0,則繼續對 upper 遞迴計算 clz2(upper, c + 1)。 若 upper == 0,則計算 (16 >> c) + clz2(lower, c + 1),表示高位部分全為 0,所以加上對 lower 的遞迴結果。 #### 程式實做 ``` 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)) & ~1; m = 1ULL << shift; while (m) { uint64_t b = y + m; y >>= 1; if (x >= b) { x -= b; y += m; } m >>= 2; } return y; ``` 其中我自己的疑惑是以及我認為的關鍵是 ``` int shift = (total_bits - 1 - clz64(x)) & ~1; m = 1ULL << shift; ``` clz64(x) 代表 "Count Leading Zeros",是計算 64 位元整數 x 前面有幾個連續的 0。 例如,若 x = 0b0000...00101(即只有低位是 5),那麼 clz64(x) 會是 61,因為前面有 61 個 0。 ``` total_bits - 1 - clz64(x) 是什麼? ``` total_bits 在 64-bit 系統通常是 64(或是根據 context 決定)。 total_bits - 1 是 63。 這樣子 63 - clz64(x),會得到 x 中最高位的 1 所在的 bit index。 也就是說: 如果 x = 0b000...01000(只有第 3 位是 1),那麼 clz64(x) 是 60(因為前面 60 個 0),shift 中間的運算是 63 - 60 = 3。 所以這裡得到的數字就是 x 的最高 set bit 的位元位置。 ``` & ~1 是什麼? ``` ~1 就是將 1 的位元取反,也就是 0x...FE(二進位最後一位變 0,其他都是 1)。 & ~1 意思是「將結果強制成偶數」:如果 shift 是奇數,扣掉 1;如果是偶數,保持不變。 舉例: 如果 shift = 5(0b101),shift & ~1 = 4(0b100) 如果 shift = 6(0b110),shift & ~1 = 6(0b110) 簡單說:shift & ~1 = 讓 shift 變成最接近小一點或相同的偶數。 或我稱為偶數地板函數? ``` m = 1ULL << shift 是什麼? ``` 1ULL 是無號 64-bit 整數 1(ULL = unsigned long long)。 << shift 是「左移 shift 位元」,也就是做 2^shift。 所以 m 變成 一個只有一個 1,且在 shift 這個位子上的數字。 #### 小結論 整體來說,這段程式碼的目的是: 找出 x 的最高 set bit 的位置 把那個位置修正成「最接近的偶數」 產生一個數字 m,這個數字只有那個偶數位上是 1,其餘是 0。 以上這個技巧很常在其他地方看到 *位元優化(bit manipulation) *快速搜尋(例如,找最接近 power-of-2 的值) *樹狀結構(比如 radix tree、prefix sum tree #### m 的用途 逐位逼近計算平方根 ``` while (m) { uint64_t b = y + m; y >>= 1; if (x >= b) { x -= b; y += m; } m >>= 2; } ``` 初始化 m 為 4^k ,從最高位開始測試。 每次迴圈: b = y + m,嘗試將 m 加到 y 中。 y >>= 1,將 y 右移一位,準備更新平方根的估計值。 如果 x >= b,說明 b*b 不超過 x,則: x -= b,更新剩餘數值。 y += m,將 m 加到 y 中,表示這一位是有效的平方根部分。 m >>= 2,每次 m 右移 2 位,因為平方數每次變化是 4^n 。 #### 範例 範例分析 ``` x = 36 (0b100100) ``` 初始化 ``` clz64(36) = 58 // 36 的二進位是 `0000...00100100` shift = 4 // 64 - 1 - 58 = 5 // 5 & ~1 = 4` m = 16 (0b10000) // m = 1 << 4 = 16 y = 0 ``` 第一輪 ``` b = y + m // 0 + 16 = 16 y >>= 1 // y 仍為 0 x >= b // (36 >= 16) → x -= 16 → x = 20 y += m // y = 16 m >>= 2 // m = 4 ``` 第二輪 ``` b = y + m // 16 + 4 = 20 y >>= 1 // y = 8 x >= b // (20 >= 20) → x -= 20 → x = 0 y += m // y = 12 m >>= 2 // m = 1 ``` 第三輪 ``` b = y + m // 12 + 1 = 13 y >>= 1 // y = 6 x < b // (0 < 13) → x 不變 m >>= 2 // m = 0(迴圈結束) ``` 最後 y = 6,即 sqrt(36) = 6 ### 測驗2-3