# 2025q1 Homework2 (quiz1+2) contributed by <`leowu0411`> ## quiz 1-1 此題透過完成以下函式點出 **a pointer to a pointer** 的技巧 ```c static inline void list_insert_before( list_t *l, list_item_t *before, list_item_t *item ); ``` `list_insert_before` 函式負責在單向鏈結串列中的指定節點前插入新節點 * `list_t *1`:指向單向鏈結串列開頭 * `list_item_t *before`:指定節點 * `list_item_t *item`:欲插入之節點 以下為利用 **a pointer to a pointer** 之實作 ```c { list_item_t **p; for (p = &l->head; *p != before; p = &(*p)->next) ; *p = item; (*p)->next = before; } ``` 在單向鏈結串列中,插入節點時通常需要知道前一個節點 (prev),透過 `list_item_t **p` 能夠直接操作該節點的指標變數,無需額外存儲 prev * 傳統寫法 (需要 prev) ```c list_item_t *prev = NULL, *cur = l->head; while (cur != before) { prev = cur; cur = cur->next; } if (prev) { prev->next = item; } else { l->head = item; } item->next = before; ``` ### 測試邏輯 * 在初始化測資 `items` 時,使 `items[i]` 的值與索引值一致 (`items[i].value = i`),使插入後的順序清楚且可預測 ```c static list_t *list_reset(void) { for (size_t i = 0; i < N; i++) { items[i].value = i; items[i].next = NULL; } l.head = NULL; return &l; } ``` * 因上述初始方法,在測試插入到開頭的狀況時只需(檢驗插入結尾同理): 1. 使用一變數 `k = N - 1` 2. 遍歷 `l.head` 應為 `N-1, N-2, ..., 1, 0` 3. 依序 `k--` 檢查 `cur->value == k`,確保順序符合預期 * 能夠在少量的程式碼改動下,確保 `list_insert_before()` 的正確性。 ### merge sort ```c static list_item_t *merge_sorted_lists(list_item_t *a, list_item_t *b) { list_item_t **p = &a, **head = &a; while (*p && b) { if ((*p)->value > b->value) { list_item_t *temp = b; b = b->next; temp->next = *p; *p = temp; } p = &(*p)->next; } if (b) *p = b; return *head; } static list_item_t *merge_sort(list_item_t *head) { if (!head || !head->next) return head; list_item_t *slow = head, *fast = head->next; while (fast && fast->next) { slow = slow->next; fast = fast->next->next; } list_item_t *mid = slow->next; slow->next = NULL; list_item_t *left = merge_sort(head); list_item_t *right = merge_sort(mid); return merge_sorted_lists(left, right); } static void list_merge_sort(list_t *l) { l->head = merge_sort(l->head); } ``` * 以上實作之 `merge_sorted_lists` 使用 `list_item_t **p` 來追蹤合併點,避免額外 `prev` 變數 * 條件插入: * 若 `b->value < (*p)->value`,則將 `b` 自合併點插入(`*p = temp`),並更新 `b` 為 `b->next`。 * 反之,移動 `p` 到 `next`,直到找到適合的插入點。 * 確保 `b` 插入完畢:最後如果 `b` 還有節點,直接連接到合併點 `*p`。 #### 測試方式 ```c #define SHUFFLE true list_reset(SHUFFLE); my_assert(list_size(&l) == 0, "Initial list size is expected to be zero."); for (size_t i = 0; i < N; i++) list_insert_before(&l, NULL, &items[i]); my_assert(list_size(&l) == N, "list size should be N"); list_merge_sort(&l); size_t k = 0; list_item_t *cur = l.head; while (cur) { my_assert(cur->value == k, "List is not sorted correctly."); k++; cur = cur->next; } ``` 與原程式之測試方式相同,但稍微改動 `list_reset` 函式,使其能夠根據 `shuffle` 參數決定是否初始化隨機值。 ```c static list_t *list_reset(bool shuffle) { srand(time(NULL)); for (size_t i = 0; i < N; i++) { items[i].value = shuffle ? rand() : i; items[i].next = NULL; } l.head = NULL; return &l; } ``` ## quiz 1-2 此題主要展示以 binary serch tree 實作的記憶體管理器,用來管理空閒區塊 (free block),允許在釋放時維護記憶體區塊的組織結構,以利之後的分配 (malloc) 和合併 (coalesce) 操作。 ### 函式解釋 * `remove_free_tree()` * 負責從二元搜尋樹中移除一個空閒區塊 `target`。 * 若 `target` 有兩個子節點,則需要找到中序前驅節點 (predecessor) 來替代 `target`,確保 `BST` 結構維持完整。 * 若 `target` 只有一個子節點或沒有子節點,則直接將 `target` 從樹中移除。 * `find_free_tree()` * 負責在 `BST` 中找到指向 `target` 之指標 * 實作如下: ```c block_t **find_free_tree(block_t **root, block_t *target) { while (*root) { if (taget->size == (*root)->size) return root else if (target->size > (*root)->size) root = &(*root)->right; else root = &(*root)->left; } return root; } ``` * 找到指向含有符合要求空間大小之節點之指標並回傳 * `find_predecessor_free_tree()` * 尋找中序前驅節點,即左子樹中的最右節點 * 此節點之 size 必小於所有 `root` 右子樹節點之 size * 此節點之 size 必大於等於所有 `root` 左子樹節點之 size * 使用此節點替換將被移出 BST 之 `target` * 實作如下: ```c block_t *find_predecessor_free_tree(block_t **root, block_t *node) { block_t *pred = NULL; if (node->l) { pred = node->l; while (pred->r) pred = pred->r; } return pred; } ``` ### 測試程式 * 首先實作 `insert_free_tree()` 以將新節點加入 BST * 如果 `find_free_tree()` 回傳的 `target` 不是 `NULL`,表示已經存在相同大小的節點 * 因此直接以新的 `block` 取代 `target`,並將 `target` 的原始子樹變成 `block` 的左子樹,確保 `BST` 結構仍然維持。 ```c void insert_free_tree(block_t **root, block_t *block) { block_t **target = find_free_tree(root, block); if (!(*target)) { *target = block; return; } block->r = (*target)->r; (*target)->r = NULL; block->l = (*target); *target = block; } ``` #### 將 BST 利用 Graphviz 視覺化 * 遞迴呼叫 `export_dot` 將 BST 之結構寫入 Graphviz `.dot` 之檔案 ```c void export_dot(block_t *root, FILE *file) { if (!root) return; if (root->l) fprintf(file, " %zu -> %zu;\n", root->size, root->l->size); if (root->r) fprintf(file, " %zu -> %zu;\n", root->size, root->r->size); export_dot(root->l, file); export_dot(root->r, file); } ``` * 產生 `.dot` 檔案 ```c void generate_dot_file(block_t *root, int step) { char filename[50]; snprintf(filename, sizeof(filename), "tree_step_%d.dot", step); FILE *file = fopen(filename, "w"); if (!file) { perror("Failed to open file"); return; } fprintf(file, "digraph BST {\n"); fprintf(file, " node [shape=circle, style=filled, fillcolor=lightblue];\n"); export_dot(root, file); fprintf(file, "}\n"); fclose(file); } ``` * 下一步利用 Graphviz 視覺化樹狀結構,較易驗證實作是否正確 * 利用以下 python script 產生 .png ```python import os import glob import subprocess def generate_images(): dot_files = sorted(glob.glob("tree_step_*.dot")) for dot_file in dot_files: png_file = dot_file.replace(".dot", ".png") print(f"Generating {png_file}...") subprocess.run(["dot", "-Tpng", dot_file, "-o", png_file]) if __name__ == "__main__": generate_images() ``` * 測試程式主函式如下: ```c int main() { block_t *root = NULL; int step = 0; block_t b1 = {32, NULL, NULL}; block_t b2 = {64, NULL, NULL}; block_t b3 = {16, NULL, NULL}; block_t b4 = {48, NULL, NULL}; block_t b5 = {8, NULL, NULL}; block_t b6 = {128, NULL, NULL}; // 插入節點 32, 64, 16 insert_free_tree(&root, &b1); insert_free_tree(&root, &b2); insert_free_tree(&root, &b3); generate_dot_file(root, ++step); // 插入節點 48, 8, 128 insert_free_tree(&root, &b4); insert_free_tree(&root, &b5); insert_free_tree(&root, &b6); generate_dot_file(root, ++step); printf("Initial Tree:\n"); print_tree(root); // 移除節點 32 remove_free_tree(&root, &b1); generate_dot_file(root, ++step); printf("After removing 32:\n"); print_tree(root); // 移除節點 16 remove_free_tree(&root, &b3); generate_dot_file(root, ++step); printf("After removing 16:\n"); print_tree(root); // 移除節點 8 remove_free_tree(&root, &b5); generate_dot_file(root, ++step); printf("After removing 8:\n"); print_tree(root); return 0; } ``` * 插入節點 32, 16, 64 ![tree_step_0](https://hackmd.io/_uploads/HyRRHtBhkg.png) * 插入節點 48, 8, 128 ![tree_step_1](https://hackmd.io/_uploads/rJtJIKHhJl.png) * 移除節點 32 ![tree_step_2](https://hackmd.io/_uploads/HyFZUYH31e.png) * 移除節點 16 ![tree_step_3](https://hackmd.io/_uploads/ryZf8YS21x.png) * 移除節點 8 ![tree_step_4](https://hackmd.io/_uploads/Skh7LKH2ye.png) 透過上圖所示,`remove_free_tree` 符合預期,在移除目標節點後,仍維持住 BST 資料結構。