# 2025q1 Homework2 (quiz1+2) contributed by <`WenHsuanYu`> ## quiz 1 ### 測驗 1 首先看到題目給定的 `list_t` 與節點 `list_item_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; ``` ```graphviz digraph structs { node[shape=record]; rankdir=LR; list_item_t [label="<value> value|<list_item_ptr>next"] } ``` ```graphviz digraph structs { node[shape=record]; rankdir=LR; list_t[label="<list_item_ptr>head"] } ``` 其中 head 會指向頭節點 (如果頭節點存在的話)。 題目指出其關鍵操作 `list_insert_before` 函式的語意如下: ![image](https://hackmd.io/_uploads/rkRBhaPNge.png) 由此可得知,傳入參數 `l` 為指向串列的指標,而函式會走訪串列以定位插入位置傳入參數 `before` 會指向插入節點 `item` 的下一個位置。 當 `before` 指向頭節點,`item` 會插入到最前面;`before` 指向 `NULL`,`item` 會插入到最尾端。當 `before` 不屬於 `l` 所指向的串列的節點時,會導致未定義的行為。 此 `list_insert_before` 函式定義如下: ```c list_item_t **p; for (p = AAAA; *p != BBBB; p = CCCC) ; *p = item; DDDD = before; ``` 依照該函式功能的註解,可推論一開始間接指標 p 指向偽節點位置(結構成員只有指標,而不具有值),故 `AAAA` 為 `&l->head`,而迴圈會走訪每個節點(實際上 p 指向各個節點的 next 指標位置)直到註解所說定位到 `before` 之間的位置,由此可知 `BBBB` 為 `before`,而 `CCCC` 為 `&(*p)->next`,離開迴圈後,此時,間接指標 `p` 會指向 `before` 前節點的 `next` 指標,因此解參考(即 `*p`)很容易得到下一個節點,所以我們可以透過解參考的方式將該節點 next 指標指向插入節點 `item`,之後插入節點再跟原先的 before 連接起來,故 `DDDD` 為 `item->next`。 解釋 list_insert_before 函式,假設 ‵p‵ 已經走訪到 ‵before‵ 之前的節點,如下圖所示: ```graphviz digraph list { node[shape=record]; rankdir=LR head [label="<head>head|<ref> "] node1 [label="<name>node1| {<value>999|<next>next}"]; node2 [label="<name>node2| {<value>998|<next>next}"]; node3 [label="<name>node3| {<value>997|<next>next}"]; node4 [label="<name>node4| {<value>996|<next>next}"]; node5 [label="<name>node5| {<value>995|<next>next}"]; node6 [label="<name>node6| {<value> 994 |<next>next}"]; head:ref -> node1; node1:next->node2; node2:next->node3; node3:next->node4; node4:next->node5; node5:next->node6; before->node4 p->node3:next [label="next 指標的位置"] } ``` 接著,我們想要插入 `item` 節點,其值為 2, 其。 ```graphviz digraph list { node[shape=record]; rankdir=LR head [label="<head>head|<ref> "] node1 [label="<name>node1| {<value>999|<next>next}"]; node2 [label="<name>node2| {<value>998|<next>next}"]; node3 [label="<name>node3| {<value>997|<next>next}"]; node4 [label="<name>node4| {<value>996|<next>next}"]; node5 [label="<name>node5| {<value>995|<next>next}"]; node6 [label="<name>node6| {<value> 994 |<next>next}"]; item [label="<name>item| {<value> 2|<next>next}"]; head:ref->node1; node1:next->node2; node2:next->node3; node3:next->item [color=red]; item:next->node4 [color=blue]; node4:next->node5; node5:next->node6; before->node4:node4 p->node3:next } ``` 紅線表示插入 `item` 節點,與其相對應的程式碼如下: ```c *p = item; ``` 藍線表示插入節點指向 `before` 所指向的節點位置,與其相對應的程式碼如下: ```c item->next = before; ``` **延伸問題**: 1. 解釋運作原理 程式流程如下: - `main` 函數會呼叫 `test_suite` 函式,該函式會進行鏈結串列的插入測試,並針對函式回傳結果進行處理,印出相關測試資訊,最後將函式回傳結果進行兩次 `not` 運算,反映出測試正常(以 `0` 結束)或者測試異常(以 `1` 不正常結束)。 ```c int main(void) { printf("---=[ List tests\n"); char *result = test_suite(); if (result) printf("ERROR: %s\n", result); else printf("ALL TESTS PASSED\n"); printf("Tests run: %d\n", tests_run); return !!result; } ``` - test_suite 函式透過巨集 `my_run_test` 來呼叫 `test_list` 測試函式,此巨集會接收傳入測試函式,並執行測試函式,檢查測試結果,若有錯誤訊息則將其回傳,否則在 `test_suite` 函式那一層會回傳 NULL,表示測試正常。 ```c #define my_run_test(test) \ do { \ char *message = test(); \ tests_run++; \ if (message) \ return message; \ } while (0) int tests_run = 0; static char *test_suite(void) { my_run_test(test_list); return NULL; } ``` - `test_list` 測試函式主要用於驗證鏈結串列操作的正確性,其測試流程如下: - 初始化設定: 首先,透過呼叫 `list_reset` 函式來初始化全域節點陣列和鏈結串列。這會將每個節點的值設定為其對應的索引值,並清除所有現有的鏈結關係,確保測試環境的乾淨。 - 測試情境一:節點前端插入: 在這個測試中,我們模擬將所有節點(目前設定為 1000 個)依序從鏈結串列的最前端插入。插入完成後,我們會利用 `my_assert` 巨集檢查鏈結串列的總長度是否正確,並確認節點數值是否呈現反向排列,以驗證前端插入的邏輯無誤。 - 測試情境二:節點尾端插入: 接著,我們將所有節點(同樣是 1000 個)依序從鏈結串列的尾端插入。插入完成後,一樣會透過 `my_assert` 巨集來檢查鏈結串列的長度,並驗證節點數值是否呈現順向排列,以確保尾端插入的功能正常。 - 測試情境三:節點尾端插入 這個情境是任意順序方式插入,當前同測試情境二。 ```c #define my_assert(test, message) \ do { \ if (!(test)) \ return message; \ } while (0) static list_item_t items[N]; static list_t l; ``` ```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; } ``` ```c static char *test_list(void) { /* Test inserting at the beginning */ list_reset(); 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, l.head, &items[i]); my_assert(list_size(&l) == N, "Final list size should be N"); size_t k = N - 1; list_item_t *cur = l.head; while (cur) { my_assert(cur->value == k, "Unexpected list item value"); k--; cur = cur->next; } /* Test inserting at the end */ list_reset(); 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, "Final list size should be N"); k = 0; cur = l.head; while (cur) { my_assert(cur->value == k, "Unexpected list item value"); k++; cur = cur->next; } /* Reset the list and insert elements in order (i.e. at the end) */ list_reset(); 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"); return NULL; } ``` 2. 撰寫合併排序操作 ```c /** * Merges two singly-linked lists into a single sorted list. * * @param l Pointer to the head of the first sorted list. * @param r Pointer to the head of the second sorted list. * @return Pointer to the head of the merged sorted list. * * The function assumes that both input lists are sorted in ascending order. * It iteratively selects the smaller head node from the two lists and appends it to the result list. * The merged list reuses the nodes from the input lists; no new nodes are allocated. */ static list_item_t *merge_twolists(list_item_t *l, list_item_t *r) { list_item_t dummy; list_item_t **indirect = &dummy->next; dummy.next = NULL; while(l && r) { list_item_t **item = (l->value <= r->value) ? &l : &r; *indirect = *item; indirect = &(*indirect)->next; *item = (*item)->next; } *item = a ? a : b; return dummy.next; } /** * @brief Sorts a singly linked list using the merge sort algorithm. * * This function implements the merge sort algorithm for singly linked lists. * It recursively splits the list into two halves, sorts each half, and then * merges the sorted halves back together. * * @param head Pointer to the head of the singly linked list to be sorted. * @return Pointer to the head of the sorted linked list. */ static list_item_t *list_merge_sort(list_item_t *head) { if (!head || !head->next) return head; list_item_t *slow = head; list_item_t *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_list(head); list_item_t *right = merge_sort_list(mid); return merge_twolists(left, right); } ``` ### 測驗 2 ### 測驗 3 ## quiz 2 ### 測驗 1 ### 測驗 2 ### 測驗 3