# 2025q1 Homework1 (lab0) contributed by <`wx200010`> {%hackmd NrmQUGbRQWemgwPfhzXj6g %} ## 開發環境 ```shell $ hostnamectl Operating System: Ubuntu 24.04.2 LTS Kernel: Linux 6.11.0-17-generic $ gcc --version gcc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0 $ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Address sizes: 39 bits physical, 48 bits virtual Byte Order: Little Endian CPU(s): 20 On-line CPU(s) list: 0-19 Vendor ID: GenuineIntel Model name: 12th Gen Intel(R) Core(TM) i7-12700H CPU family: 6 Model: 154 Thread(s) per core: 2 Core(s) per socket: 14 Socket(s): 1 Stepping: 3 CPU(s) scaling MHz: 39% CPU max MHz: 4700.0000 CPU min MHz: 400.0000 BogoMIPS: 5376.00 Virtualization features: Virtualization: VT-x Caches (sum of all): L1d: 544 KiB (14 instances) L1i: 704 KiB (14 instances) L2: 11.5 MiB (8 instances) L3: 24 MiB (1 instance) NUMA: NUMA node(s): 1 NUMA node0 CPU(s): 0-19 ``` ## 開發指定的佇列操作 ### `q_new` > [Commit 95096ed](https://github.com/wx200010/lab0-c/commit/95096edd077febeb7070b5b820f227f9b8e6d633) `q_new` 需要對新配置的 head 進行初始化,正好 linux kernel API 提供了 `INIT_LIST_HEAD` 來初始化 head 的 `next` 和 `prev` 指標,因此我用了該巨集來簡化程式碼: ```c struct list_head *q_new() { return NULL; struct list_head *head = malloc(sizeof(struct list_head)); if (!head) return NULL; INIT_LIST_HEAD(head); return head; } ``` ### `q_free` > [Commit 95096ed](https://github.com/wx200010/lab0-c/commit/95096edd077febeb7070b5b820f227f9b8e6d633) `q_free`的目的是釋放整條 queue 所佔用的記憶體,因此我使用了`list_for_each_entry_safe` 來走訪整條 queue 並釋放記憶體。 不過在一開始的版本中,我額外使用了 `list_del` 巨集來確保每個節點從串列中被移除,如下: ```c void q_free(struct list_head *head) { if (!head) return; element_t *curr = NULL, *next = NULL; list_for_each_entry_safe (curr, next, head, list) { list_del(&curr->list); free(curr->value); free(curr); } free(head); } ``` 然而這步是多餘的,因為整條串列在經過`q_free`處理後就被釋放記憶體了,不能再被訪問,因此最後的版本便拔掉了多餘的程式碼: ```diff void q_free(struct list_head *head) { if (!head) return; element_t *curr = NULL, *next = NULL; list_for_each_entry_safe (curr, next, head, list) { - list_del(&curr->list); free(curr->value); free(curr); } free(head); } ``` ### `q_size` > [Commit 7ec6f4e](https://github.com/wx200010/lab0-c/commit/7ec6f4ee3ed5698a801e703cf1df4b83a407bea1) `q_size` 的目的是回傳整條串列的節點總數,這步驟可以依靠`list_for_each`巨集來走訪整條串列: ```c int q_size(struct list_head *head) { if (!head) return 0; int len = 0; struct list_head *it; list_for_each (it, head) len++; return len; } ``` ### `q_insert_head` 與 `q_insert_tail` > [Commit 9dccf54 (最新)](https://github.com/wx200010/lab0-c/commit/9dccf548e760c1145043896c12bf7cecba39dea4) > [Commit 143dc94](https://github.com/wx200010/lab0-c/commit/143dc94cded5bb64d245a05d6a0e32bd46dbaab4) > [Commit 06b615d](https://github.com/wx200010/lab0-c/commit/06b615defd202e5a1b7b265b8cf6c558310dc973) > [Commit c29bb5a](https://github.com/wx200010/lab0-c/commit/c29bb5ae13c8b239fb05ff5c41a058e0b68376a2) > [Commit 3d17f18 (最舊)](https://github.com/wx200010/lab0-c/commit/3d17f1822f6313ac6af96a4069ca39be80c232d3) 這兩者都需要配置一個新節點並插入到queue中,只差在新節點會被插在 head 或是 tail,因此我額外撰寫了一段inline函數 `q_insert` 來簡化程式碼,其中參數 `insert_func` 會決定插入時是使用 `list_add` 或 `list_add_tail`,程式碼如下: ```c /* * Insert an element at the head or tail of the queue, depending on the caller. * This function simplifies the implementation of q_insert_head and * q_insert_tail. */ static inline bool q_insert(struct list_head *head, char *s, void (*insert_func)(struct list_head *, struct list_head *)) { if (!head) return false; element_t *new = malloc(sizeof(element_t)); if (!new) return false; new->value = strdup(s); if (!new->value) { free(new); return false; } insert_func(&new->list, head); return true; } bool q_insert_head(struct list_head *head, char *s) { return q_insert(head, s, list_add); } bool q_insert_tail(struct list_head *head, char *s) { return q_insert(head, s, list_add_tail); } ``` 注意到`q_insert`在複製參數`s`字串值到新配置的節點時,可以使用`strdup`函數來完成。 之前我使用`strlen(s) + 1`來計算出完整字串長度,再使用`strncpy`來複製字串。然而僅使用`strdup`便能達到相同效果,又能節省程式碼。 > `strdup`的部份參考了[allen741的code](https://github.com/allen741/lab0-c/blob/master/queue.c#L73)。 > 設計 `q_insert` 來簡化程式碼的部份,參考了[25077667的code](https://github.com/25077667/lab0-c/blob/dev/queue.c#L261) ### `q_remove_head` 與 `q_remove_tail` > [Commit 8b56ec1(最新)](https://github.com/wx200010/lab0-c/commit/8b56ec1661dcc6fe8d1f17cf00575fb265d0ac2a) > [Commit 06b615d](https://github.com/wx200010/lab0-c/commit/06b615defd202e5a1b7b265b8cf6c558310dc973) > [Commit 5804494(最舊)](https://github.com/wx200010/lab0-c/commit/5804494c768eabb0b36270576c8f2f30670e8176) 這兩者函式的差別與上面的 insert 一樣,只差在刪除的節點是在開頭或結尾,因此我也是額外設計了一份 inline 函式 `q_remove` 來簡化程式碼,參數`node`即是需要被刪除的節點,程式碼如下: ```c /* * Remove an element from the head or tail of the queue, depending on the * caller. This function simplifies the implementation of q_remove_head and * q_remove_tail. */ static inline element_t *q_remove(struct list_head *head, struct list_head *node, char *sp, size_t bufsize) { if (!head || list_empty(head)) return NULL; element_t *entry = list_entry(node, element_t, list); list_del(&entry->list); if (sp) { strncpy(sp, entry->value, bufsize - 1); sp[bufsize - 1] = '\0'; } return entry; } element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize) { return q_remove(head, head->next, sp, bufsize); } element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize) { return q_remove(head, head->prev, sp, bufsize); } ``` 注意到在刪除節點的部份,可以使用 `list_del` 巨集來實現。 > 設計 `q_remove` 來簡化程式碼的部份,參考了[25077667的code](https://github.com/25077667/lab0-c/blob/dev/queue.c#L271) ### `q_delete_mid` > [Commit 0c17e9d](https://github.com/wx200010/lab0-c/commit/0c17e9d6a63a1cc8d69dcfc355a10eab50861744) 該函式的難點在於如何定位 middle node,這部份我參考了[你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list#%E6%A1%88%E4%BE%8B%E6%8E%A2%E8%A8%8E-Leetcode-2095-Delete-the-Middle-Node-of-a-Linked-List)與[快慢指標](https://hackmd.io/@sysprog/ry8NwAMvT)。在得到 middle node 後便可使用 linux kernel API 提供的 `list_entry` 和 `list_del` 巨集來處理移除節點並釋放記憶體的部份。 相關程式碼如下: ```c bool q_delete_mid(struct list_head *head) { // https://leetcode.com/problems/delete-the-middle-node-of-a-linked-list/ if (!head || list_empty(head)) return false; /* Find the middle node in queue */ struct list_head *del_node = head->next; for (const struct list_head *ptr = head->next; ptr != head && ptr->next != head; ptr = ptr->next->next) { del_node = del_node->next; } /* Delete the middle node */ element_t *del = list_entry(del_node, element_t, list); list_del(del_node); free(del->value); free(del); return true; } ``` ### `q_swap` > [Commit a52438d](https://github.com/wx200010/lab0-c/commit/a52438d0d9b6dae2047f3763a68dd8ccb5ae1cef) q_swap需要將兩兩相鄰的節點進行互換,因此我使用了兩個指標`L`與`R`來處理兩兩互換的部份,程式碼如下: ```c void q_swap(struct list_head *head) { // https://leetcode.com/problems/swap-nodes-in-pairs/ if (!head || list_empty(head)) return; struct list_head *L = head->next, *R = L->next; while (L != head && R != head) { /* swap L and R node */ L->prev->next = R; R->next->prev = L; R->prev = L->prev; L->next = R->next; L->prev = R; R->next = L; L = L->next; R = L->next; } return; } ``` > 實際上可以改用`list_move`完成,程式碼相當精簡,請見[LIAO-JIAN-PENG的code](https://github.com/LIAO-JIAN-PENG/lab0-c/blob/master/queue.c#L215) ### `q_reverse` > [Commit d816356](https://github.com/wx200010/lab0-c/commit/d816356261fdd4f2ab42205a3ec8d7a92f07bb39) 該函式需要將整條串列進行反轉,我的想法是走訪整條串列並逐一把每個節點的`prev`與`next`指標值進行交換,走訪的部份則靠`list_for_each_safe`來實現: ```c void q_reverse(struct list_head *head) { if (!head || list_empty(head)) return; struct list_head *node, *safe = NULL; list_for_each_safe (node, safe, head) { node->next = node->prev; node->prev = safe; } safe = head->next; head->next = head->prev; head->prev = safe; } ``` ### `q_merge` > [Commit a9d3750](https://github.com/wx200010/lab0-c/commit/a9d3750306fa26324c89a0f0049b9f062c01bc87) 在[你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list#%E6%A1%88%E4%BE%8B%E6%8E%A2%E8%A8%8E-LeetCode-21-Merge-Two-Sorted-Lists)中,有提到如何合併兩條已排序的串列,因此我設計了`merge_two_lists`函式來完成兩兩合併的功能。 ```c /* Merge two list from the first element*/ struct list_head *merge_two_lists(struct list_head *L1, struct list_head *L2, bool descend) { /* Indirect pointer method */ struct list_head *head = NULL, **ptr = &head, **node; for (node = &L1; L1 && L2; *node = (*node)->next) { node = ((strcmp(list_entry(L1, element_t, list)->value, list_entry(L2, element_t, list)->value) <= 0) ^ descend) ? &L1 : &L2; *ptr = *node; ptr = &(*ptr)->next; } *ptr = (struct list_head *) ((uintptr_t) L1 | (uintptr_t) L2); return head; } ``` > 注意到`merge_two_lists`是基於單向非環狀鏈結串列上設計使用的,因此在本次作業的雙向環狀鏈結串列上,會需要做額外處理(詳情請見下方q_merge的程式碼) > 該想法參考了[millaker的code](https://github.com/millaker/lab0-c/blob/master/queue.c#L375) 而`q_merge`需要將多條串列進行合併,同樣在[你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list#%E6%A1%88%E4%BE%8B%E6%8E%A2%E8%A8%8E-LeetCode-21-Merge-Two-Sorted-Lists)中也有提到關於合併效率的問題,若是每次都拿第一條串列來合併剩下的串列,會導致合併速度越來越慢,因此我選擇了開頭跟結尾兩兩合併的實現方式,程式碼如下: ```c /* Merge all the queues into one sorted queue, which is in ascending/descending * order */ int q_merge(struct list_head *head, bool descend) { // https://leetcode.com/problems/merge-k-sorted-lists/ if (!head || list_empty(head)) return 0; int chain_size = q_size(head); // Convert each queue to a non-circular linked list. for (struct list_head *p = head->next; p != head; p = p->next) { list_entry(p, queue_contex_t, chain)->q->prev->next = NULL; } // Merge all the queues into one sorted queue struct list_head *L, *R = head->prev; while (chain_size > 1) { L = head->next; while (L != R) { queue_contex_t *contex_L = list_entry(L, queue_contex_t, chain); queue_contex_t *contex_R = list_entry(R, queue_contex_t, chain); contex_L->q->next = merge_two_lists(contex_L->q->next, contex_R->q->next, descend); contex_L->size += contex_R->size; contex_R->q->prev = contex_R->q->next = contex_R->q; R = R->prev; if (L == R) break; L = L->next; } chain_size = (chain_size + 1) >> 1; } /* Restore the first queue to circular doubly Linked List */ struct list_head *q_head = list_entry(head->next, queue_contex_t, chain)->q; L = q_head; while (L->next != NULL) { L->next->prev = L; L = L->next; } L->next = q_head; q_head->prev = L; return list_first_entry(head, queue_contex_t, chain)->size; } ``` 該程式碼會先將每條queue各自轉換成非環狀鏈結串列,就能使用`merge_two_lists`函式來實現合併,最後只剩一條queue時再重新維護`prev`指標與環狀結構就完成了。 > 同樣,該想法參考了[millaker的code](https://github.com/millaker/lab0-c/blob/master/queue.c#L375),只差在我將其改變成頭尾兩兩合併的實現方式。 ### `q_sort` > [Commit af24429](https://github.com/wx200010/lab0-c/commit/af24429d3049f73f4f751cd65a50a14a882980e9) q_sort的目的是要將 queue 裡的 elements 進行排序。由於queue是雙向環狀鏈結串列,因此我的作法是先將queue轉換成非環狀鏈結串列(把`head->prev->next`設為NULL)後,再使用先前學到的`mergesort_list`函數來實現非環狀串列的合併排序。 > 這部分參考了[你所不知道的 C 語言: linked list 和非連續記憶體 - Merge Sort 的實作](https://hackmd.io/@sysprog/c-linked-list#Merge-Sort-%E7%9A%84%E5%AF%A6%E4%BD%9C) 可以注意到的是,`mergesort_list`函數也需要尋找到 middle node,因此也會使用到快慢指標的方法,以下是相關程式碼: ```c /* Perform merge sort on the list. */ struct list_head *mergesort_list(struct list_head *head, bool descend) { if (!head || !head->next) return head; struct list_head *slow = head, *mid, *left, *right; for (const struct list_head *fast = head->next; fast && fast->next; fast = fast->next->next) slow = slow->next; mid = slow->next; slow->next = NULL; left = mergesort_list(head, descend); right = mergesort_list(mid, descend); return merge_two_lists(left, right, descend); } /* Sort elements of queue in ascending/descending order */ void q_sort(struct list_head *head, bool descend) { if (!head || list_empty(head)) return; head->prev->next = NULL; head->next = mergesort_list(head->next, descend); /* Fix the prev pointer */ struct list_head *p = head; while (p->next != NULL) { p->next->prev = p; p = p->next; } p->next = head; head->prev = p; } ``` ### `q_delete_dup` > [Commit b3d23af](https://github.com/wx200010/lab0-c/commit/b3d23af0e575aa7b4b2a295c1b53df44b75fb2ba) ### `q_reverseK` > [Commit c355a99](https://github.com/wx200010/lab0-c/commit/c355a99ccce111a0b7dff0b7d9992403136a1bce) ### `q_ascend` > [Commit 103efdd](https://github.com/wx200010/lab0-c/commit/103efdd3bc286d0aeb8bfb4ecda7418111b57424) ### `q_descend` > [Commit 103efdd](https://github.com/wx200010/lab0-c/commit/103efdd3bc286d0aeb8bfb4ecda7418111b57424)