# 2025q1 Homework2 (quiz1+2) contributed by < `lumynou5` > ## 1-1 `list_insert_before` 函式巧妙地利用指標的指標來避免特例: ```c list_item_t **p; for (p = &l->head; *p != before; p = &(*p)->next) ; *p = item; (*p)->next = before; ``` 由於 `p` 指向的是「節點指向下一個節點的指標」,因此可以用來判斷是否到達 `before` 和修改前一個節點的值,而不需要兩個 `list_item_t *`,且避免了 `before` 為 `l->head` 時的特例,因為 `list_t` 是不同的結構體,如果使用 `list_item_t *`,要將節點插入為第一個節點,就得額外處理。 一個簡單的合併排序實作是: ```c list_item_t const DUMMY_LIST_ITEM = { .value = INT_MAX }; static void list_merge_sort_impl(list_item_t **pbegin, list_item_t *end) { if ((*pbegin)->next == end) // We have a single node in this sub. return; list_item_t *slow = *pbegin; for (list_item_t *fast = slow->next; fast != end && fast->next != end;) { slow = slow->next; fast = fast->next->next; } list_merge_sort_impl(pbegin, slow->next); list_merge_sort_impl(&slow->next, end); list_item_t *l = *pbegin, *r = slow->next, **p = pbegin; while (l != &DUMMY_LIST_ITEM || r != &DUMMY_LIST_ITEM) { if (l->value < r->value) { *p = l; l = l->next != slow->next ? l->next : &DUMMY_LIST_ITEM; } else { *p = r; r = r->next != end ? r->next : &DUMMY_LIST_ITEM; } p = &(*p)->next; } *p = end; } void list_merge_sort(list_t *l) { if (!l) return; list_merge_sort_impl(&l->head, NULL); } ``` 首先以快慢指標找到第一段的結尾、第二段的開頭(`slow->next`),將兩段分別排序,然後再合併起來,很普通的遞迴式合併排序。 ## 1-2 函式 `remove_free_tree` 的核心部分為: ```c= block_t **node_ptr = find_free_tree(root, target); if ((*node_ptr)->l && (*node_ptr)->r) { block_t **pred_ptr = &(*node_ptr)->l; while ((*pred_ptr)->r) pred_ptr = &(*pred_ptr)->r; if (*pred_ptr == (*node_ptr)->l) { block_t *old_right = (*node_ptr)->r; *node_ptr = *pred_ptr; (*node_ptr)->r = old_right; } 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); *node_ptr = pred_node; (*node_ptr)->l = old_left; (*node_ptr)->r = old_right; } } else if ((*node_ptr)->l || (*node_ptr)->r) { block_t *child = ((*node_ptr)->l) ? (*node_ptr)->l : (*node_ptr)->r; *node_ptr = child; } else { *node_ptr = NULL; } target->l = NULL; target->r = NULL; ``` - 第 4 行:同樣利用間接指標來找到前導節點(左子樹中最右的節點),消除了可能是其親代節點的左或右子節點的差異。 - 第 8 行:若前導節點就是 `target` 的左子節點,表示前導節點沒有右子節點,直接以其替換 `target`,然後將原本的右子樹移過去即可。 - 第 12 行:若前導節點在更深處,則將其移到 `target` 的位置替換之。因為遞迴呼叫了 `remove_free_tree`,前導節點自身的子節點(若有)會替換到合適位置,然後直接將原本的左右子樹移過去即可。 - 第 21 行:若 `target` 只有一個子節點,以該子節點替換之即可。 - 第 24 行:若 `target` 沒有子節點,直接將指向它的指標設為 `NULL`,就將它從樹中移除了。 第 21 行與第 24 行的分支可以合併,因為如果 `target` 沒有子節點,`(*node_ptr)->r` 就是 `NULL`,刪除這個分支能夠省去 4 道指令(x86-64 gcc 14.2 `-O3`,[Compiler Explorer 演示](https://godbolt.org/z/97vfexvb5))。 第 15 行的 `pred_node` 似乎完全不必要,只在一處使用,完全可以用 `*pred_ptr` 替代。乍看之下像是儲存原本的值,避免函式呼叫中修改了,但 `pred_ptr` 指向哪實際上不可能被修改。而在一開始,`*node_ptr` 和 `target` 指向的是同一個物件,差別在於前者實際上是「在 `target` 原位的節點」,透過將程式碼中一些 `*node_ptr` 改為 `target` 能讓程式碼更簡短且意圖更清楚,`old_left`、`old_right` 也可以用 `target->l`、`target->r` 替換: ```c block_t **node_ptr = find_free_tree(root, target); if (target->l && target->r) { block_t **pred_ptr = &target->l; while ((*pred_ptr)->r) pred_ptr = &(*pred_ptr)->r; if (*pred_ptr != target->l) { remove_free_tree(&target->l, *pred_ptr); (*node_ptr)->l = target->l; } (*node_ptr)->r = target->r; *node_ptr = *pred_ptr; } else { block_t *child = target->l ? target->l : target->r; *node_ptr = child; } target->l = NULL; target->r = NULL; ``` 但我發現這樣修改後,`-O3` 下產生的組合語言反而更長。在閱讀過組合語言後,我發現是一些分支被展開了,事實上在 `-O0` 下以上程式碼產生的組合語言確實短非常多,`-O1` 下更短,但 `-O2`、`-O3` 卻都比各自的上一等級產生更長的組合語言。我認為這是因為編譯器判斷分支足夠短,直接重複部分指令兩次的收益高過膨脹的檔案大小。 :::success TODO: 測試實際效能。 ::: ## 1-3 函式 `quick_sort` 的主體: ```c int n = list_length(list); int value; int i = 0; int max_level = 2 * n; struct list_head *begin[max_level]; struct list_head *result = NULL, *left = NULL, *right = NULL; begin[0] = list->next; list->prev->next = NULL; while (i >= 0) { struct list_head *L = begin[i], *R = list_tail(begin[i]); if (L != R) { struct list_head *pivot = L; value = list_entry(pivot, node_t, list)->value; struct list_head *p = pivot->next; pivot->next = NULL; while (p) { struct list_head *n = p; p = p->next; int n_value = list_entry(n, node_t, list)->value; if (n_value > value) { n->next = right; right = n; } else { n->next = left; left = n; } } begin[i] = left; begin[i + 1] = pivot; begin[i + 2] = right; left = right = NULL; i += 2; } else { if (L) { L->next = result; result = L; } i--; } } list->next = result; rebuild_list_link(list); ``` 流程(以最外層迴圈第一次迭代的視角描述): - 首先將串列的第一個節點作為軸節點,依次將剩餘的節點和其比較,分別放入 `left` 和 `right` 中。 - 將 `left`、`pivot` 和 `right` 分別推入堆疊。 - 重複以上流程,但對象的串列是剛剛推入堆疊的 `right`。這會對每個推入堆疊的串列重複,直到(不是直到所有串列都,而是「深度優先」地,因為堆疊先進後出)串列中只有零或一個節點。 - 若串列中只有零或一個節點,將那唯一的節點(若有)插到 `result` 的最前面,然後從堆疊彈出這個串列。 由於堆疊先進後出的特性,以及最後得到結果時是插到最前面,值相等的節點順序會反過來,因此排序並不是穩定的。 ## 2-1 函式 `list_quicksort` 的主體: ```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 = list_entry(head, struct listitem, list); list_del(&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 list_move_tail(&item->list, &list_greater); } list_quicksort(&list_less); list_quicksort(&list_greater); list_add(&pivot->list, head); list_splice(&list_less, head); list_splice_tail(&list_greater, head); ``` 迴圈是從前往後迭代,而 `list_move_tail` 將節點從串列移到子串列的最後面,因此值相等的節點順序維持不變。如果改為 `list_move` 移到最前面,那順序就會倒過來,並且因為遞迴,最終的順序其實不一定。