# 2025q1 Homework1 (lab0) contributed by < [WavJaby](https://github.com/WavJaby/lab0-c) > {%hackmd NrmQUGbRQWemgwPfhzXj6g %} <details><summary>開發環境</summary> ```shell $ gcc --version gcc (Ubuntu 14.2.0-4ubuntu2) 14.2.0 Copyright (C) 2024 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. $ 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): 12 On-line CPU(s) list: 0-11 Vendor ID: GenuineIntel Model name: 12th Gen Intel(R) Core(TM) i7-12700KF CPU family: 6 Model: 151 Thread(s) per core: 1 Core(s) per socket: 12 Socket(s): 1 Stepping: 2 BogoMIPS: 7219.20 Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx rdtscp lm constant_tsc rep_good nopl xtopology nonstop_tsc cpuid tsc_kn own_freq pni pclmulqdq ssse3 cx16 sse4_1 sse4_2 movbe popcnt aes rdrand hypervisor lahf_lm abm 3dnowprefetch ibrs_enhanced fsgsbase bmi1 bmi2 invpcid rdseed adx clflushopt sha_ni arat md_clear flush_l1d arch_capabilities ``` </details> --- <pre style="color:#67A37C; font-size:105%; font-weight:bold"> /** * 以前只有寫過小專案,或是找好玩的小專案貢獻一下, * 所以沒有寫過這麼正式的開發日誌或是commit, * 希望我可以趁這個機會好好練習一下 * <font color="#F47067">@param</font> <font color="#DDD">homework</font> lab0 * <font color="#F47067">@param</font> <font color="#DDD">date</font> 2025/03/XX * <font color="#F47067">@return</font> queue.c */ </pre> ## 實作 `queue.c` 首先學習[如何撰寫一個好的 commit message](https://blog.louie.lu/2017/03/21/如何寫一個-git-commit-message/),我認為比較重要的是限制[標題最多只有 50 字元](https://blog.louie.lu/2017/03/21/如何寫一個-git-commit-message/#rules02),之前都不知道有這個規則,只知道[以祈使句撰寫標題](https://blog.louie.lu/2017/03/21/如何寫一個-git-commit-message/#rules05)。 接著進行[牛刀小試](/@sysprog/linux2025-lab0-a/#牛刀小試)環節,並產生第一個 commit [85e9348](https://github.com/WavJaby/lab0-c/commit/85e93488996c495d52f13cc50a145775d5636708)。因為沒有使用過 Git hook 所以不知道 `Change-Id` 什麼時候會生成出來,怎麼生成出來。 俗話說得好 >Software and cathedrals are much the same – first we build them, then we pray. -Sam Redwine 秉持著這個心情,按下 Enter 之後,`Change-Id` 成功的出現在我的 commit message 裡面。 終於可以開始寫程式了╰(\*°▽°\*)╯ <font style="font-size:70%;color:gray">這個不算emoji</font>。 </br> ### Implement `q_new`, `q_free` functions [6b7a573](https://github.com/WavJaby/lab0-c/commit/ef49dec9b29295bdbf963cc0c70556c903441836) 我在寫 <img style="height:1.6em;margin:-0.3em 0 0 0" src="https://upload.wikimedia.org/wikipedia/commons/1/18/C_Programming_Language.svg"> 的時候習慣把 new 和 free 一起寫,因為需要 new 的地方 就會需要 free ,不然到時候寫一寫忘記這東西到底怎麼 new 的,就又要回去看code,很累。 像是之前我寫了一個自己的 string 函式庫 [wjcl_string.h](https://github.com/WavJaby/WJCL/blob/main/string/wjcl_string.h) ,原因是因為平常取得字串長度都用 `strlen` ,每次都要遍歷字串的所有字元,感覺就慢得要死。 所以我想到一個在確保字串可以正常使用的前提下(例如可以直接丟到 `printf` 裡面),儲存長度的方法,就是在創建字串時,多創建 8 byte 來存字串長度,然後回傳 `ptr + 8` 把那 8 byte 藏在字串的前面,需要 `free` 的時候再 `ptr - 8` 就好了。 缺點就是不能用內建的 `strcat` 之類的功能,長度就會跑掉,~~但這個就比較好用,我才不用內建的~~。 `q_new` 的部分,在 `malloc` 完後,先確認有沒有成功,然後使用 `INIT_LIST_HEAD` 初始化 list head。 `q_free` 的部分,使用 `list_for_each_entry_safe` 來遍歷整個鏈結串列,並使用 `q_release_element` 來釋放鏈結串列中的所有元素。 :::info 這邊不能使用 `list_for_each_entry` 是因為如果當前的元素已經釋放掉後,就無法取得下一個元素了,使用 `list_for_each_entry_safe` 會預先取得下一個元素,在下一輪迴圈時直接變更為當前元素,然後在預先取得下一個元素,這樣就避免了這個問題。 ::: ### Implement `q_insert_head`, `q_insert_tail` functions [cfb2fae](https://github.com/WavJaby/lab0-c/commit/cfb2fae58cd9d44bae186d100874d33813567b57) 我同時實作了這兩個函式,因為他們都有一個相似處,就是要創建一個新的 `element_t` 並加入到鏈結串列中。 所以我另外實作了一個函式 `create_element` 來創建新的元素。 ```c static inline element_t *create_element(char *s) { element_t *element = malloc(sizeof(element_t)); if (!element) return NULL; element->value = strdup(s); // If string copy failed, free the element if (!element->value) { free(element); return NULL; } return element; } ``` 首先先確認 `element_t` 有沒有創建成功,接著複製字串。 這邊使用 `harness.h` 標頭檔中的 `strdup` 來複製字串,方便之後[追蹤記憶體配置和釋放的狀況](/@sysprog/linux2025-lab0-b/#追蹤記憶體配置和釋放的狀況) 如果字串複製失敗,要釋放剛剛創建好的 `element_t` 並回傳 `NULL` 接著只要呼叫 `create_element` 並且判斷有沒有創建成功後, - `q_insert_head` 使用 `list_add(&entry->list)` 將節點插入頭 - `q_insert_tail` 使用 `list_add_tail(&entry->list)` 將節點插入尾 ### Implement q_remove_head, q_remove_tail [c24fe3d](https://github.com/WavJaby/lab0-c/commit/c24fe3d853535001dcfbc5f47797a54bed20c888) 這兩個函式也有共通處,但我有點懶惰就沒有把他們拆出去了,主要差別在於一個使用 `list_first_entry` 另一個是 `list_last_entry`。 首先兩個函式都要先判斷鏈結串列中有沒有東西,如果是空的就直接跳出 ```c if (!head || list_empty(head)) return NULL; ``` 接下來取得 `element_t` , - `q_remove_head` 使用 `list_first_entry` 取得鏈結串列中第一個元素 - `q_remove_tail` 使用 `list_last_entry` 取得鏈結串列中最後一個元素 `queue.h` 標頭檔中有註明 `If sp is non-NULL and an element is removed, copy the removed string to *sp (up to a maximum of bufsize-1 characters, plus a null terminator.)` 意思是如果有成功移除元素而且 `sp != NULL` ,要複製元素中的字串到 `sp` ,並且最大長度是 `bufsize - 1`,所以我使用 `strncpy` 複製字串,然後在大長度加上 null terminator。 ```c if (sp && bufsize) { strncpy(sp, entry->value, bufsize - 1); sp[bufsize - 1] = '\0'; } ``` 最後把這個元素從鏈結串列中移除,而且是"移除",不是"刪除",所以我使用 `list_del_init` ,僅將元素從鏈結串列移除,並且初始化元素中的 `list_head` ,最後回傳被移除的元素。 ### Implement q_delete_mid [60e0295](https://github.com/WavJaby/lab0-c/commit/60e0295bf8a7b6c938abdf04e123cbb1187bfe5e) 因為之後會實作 merge sort 也會需要找中間值,所以我建立了一個新的函式 `_q_find_mid` ,我是用左右一起往中間夾的方式找中間的節點, ```c static inline struct list_head *_q_find_mid(struct list_head *left, struct list_head *right) { while (left != right) { left = left->next; // When there are an even number of nodes, select next first if (right == left) break; right = right->prev; } return left; } ``` 如果是鏈結串列長度是偶數,優先去得靠左邊的節點 ,所以如果 `left->next == right` 的時候,就可以直接跳出迴圈。 有了這個函式後,在 `q_delete_mid` 中就可以呼叫這個函式拿到中間的節點, ```c struct list_head *mid = _q_find_mid(head->next, head->prev); list_del(mid); q_release_element(list_entry(mid, element_t, list)); ``` 然後用 `list_del` 先將節點從鏈結串列中移除,然後用 `q_release_element` 釋放元素。 ### Implement q_reverse [259dfbc](https://github.com/WavJaby/lab0-c/commit/259dfbc8193b3be0da67555518a3ba0a3f7219e6) 如果想要把整個鏈結串列反過來的話,只要遍歷每個節點,然後一個一個插入到鏈結串列的最前面就可以了,所以我用 `list_for_each_safe (node, safe, head)` 來取得每一個節點,然後用 `list_move(node, head)` 把每個節點都移到最前面。 ### Implement q_swap [ed44ff8](https://github.com/WavJaby/lab0-c/commit/ed44ff8edbc2cb7ad7f0dfcca8cc28491970030f) 這個函式要把每兩個節點一組然後交換,一樣使用 `list_for_each_safe (node, next, head)` 取得兩個節點,然後用 `list_move(node, next)` 把 `node` 挪到 `next` 的下一個。 然後在挪動之前要先確保 `next != head` ,因為如果鏈結串列長度是奇數的話,最後一組的 `next` 會等於 `head`。 最後因為我們把 `node` 移到 `next` 後面了,所以我把 `next = node->next` 讓下個迴圈的 `node` 變成下一組。 ### Implement q_ascend and q_descend [cabf09a](https://github.com/WavJaby/lab0-c/commit/cabf09a753034d56054322a45044a84902532e6f) 這兩個函式也是有共通點,所以我建立了一個函式 `_q_remove_not_in_order` 來找不符合順序的節點並且刪除它們。 因為要判斷兩個元素的順序(升序或降序),所以我建立了一個函式 `_q_value_cmpare` ,來判斷兩個字串是否符合規則 ```c static inline int _q_value_cmpare(char *a, char *b, bool descend) { return descend ? strcmp(b, a) : strcmp(a, b); } ``` 接著實作`_q_remove_not_in_order` 這個函式會遍歷整個鏈結串列,並從每個元素開始,向前檢查前面的元素順序是否正確,如果發現有元素不符合指定的順序,就將其刪除。 ```c list_for_each_entry (curr, head, list) { // 從last往前判斷順序 while (&last->list != head && // 判斷當前以及前面的元素順序 _q_value_cmpare(curr->value, last->value, descend) < 0) { tmp = element_prev(last, list); // 刪除不符合順序的元素 list_del(&last->list); q_release_element(last); // 往前一個元素 last = tmp; } // 紀錄最後一個元素,以供下次判斷 last = curr; } ``` 接著在在 `q_ascend` 以及 `q_descend` 中,判斷如果 鏈結串列大小 <= 1 就直接回傳,否則就要使用 `_q_remove_not_in_order` 來移除不符合順序的元素 - `q_ascend` 使用 `_q_remove_not_in_order(head, false)` - `q_descend` 使用 `_q_remove_not_in_order(head, true)` 最後再使用 `q_size(head)` 回傳鏈結串列長度 ### Implement q_delete_dup [fffd619](https://github.com/WavJaby/lab0-c/commit/fffd619aa0561ad49c8919b6484374b25499e23e) 因為牽涉到移除元素,所以需要使用 `list_for_each_entry_safe` ```c if (&next->list != head && strcmp(curr->value, next->value) == 0) { list_del(&curr->list); q_release_element(curr); dup = true; } ``` 先判斷 `curr` 是不是跟 `next` 一樣,如果一樣的話就刪除當前的元素,然後因為要移除所有有重複的元素,所以我用一個布林值 `dup` 紀錄有找到重複的值 ```c else if (dup) { list_del(&curr->list); q_release_element(curr); dup = false; } ``` 最後重複直到找到不一樣的值,並且 `dup` 是 `true` 的話,也要刪除當前元素,因為他會是最後一個重複的元素。 ### Implement q_reverseK [de50aac](https://github.com/WavJaby/lab0-c/commit/de50aac07eeff6a2dcb9635ebd396d0d467c8d4a) 使用 `list_for_each_safe` 遍歷鏈結串列 ```c list_for_each_safe (curr, next, head) { if (++count != k) continue; ``` 如果 `count != k` 的話跳過這個迴圈,達到 `k` 時,使用 `q_reverse` 的方法,把鏈結串列反轉 ```c while (count--) { next_element = element->next; list_move(element, group_start); element = next_element; } ``` 反轉後,更新 group_start 為組最後一節點。 ```c group_start = next->prev; } ``` ### Implement q_sort [743c731](https://github.com/WavJaby/lab0-c/commit/743c73194dc4b102291b68fcc8a7efa7d03d460b) 實作 merge sort 為了美觀,我另外新增了兩個函式,一個做切分遞迴 `_q_merge_sort` ,一個做合併 `_q_merge` ,其實切分遞迴可以用原本的函式就好了。 ---- 先介紹 `_q_merge` ```c void _q_merge(struct list_head *l_head, struct list_head *r_head, bool descend) ``` 目的是將兩個已經排序好的鏈結串列 `l_head` 和 `r_head` 合併成一個新的鏈結串列,並根據 descend 參數決定是升序還是降序。 我的做法是將右邊串列的第一個元素一個一個加到左邊串列,直到左邊串列頭碰到右邊串列頭,或是右邊串列的元素都被清空。 首先先取得左邊串列的第一個元素 ```c { element_t *l = list_first_entry(l_head, element_t, list); ``` 然後如果右邊串列不是空的的話,就重複迴圈,每次迴圈都先取得右邊串列第一個元素用於比較 ```c while (!list_empty(r_head)) { element_t *r = list_first_entry(r_head, element_t, list); ``` 接著這個內層循環會用前面定義的 `_q_value_cmpare` 來比較 `l` 以及 `r` 元素的值,如果輸出小於等於 0(根據 descend 決定順序),則表示 l 已經在正確位置,繼續移動 `l` 到下一個元素,直到找到需要插入 `r` 的位置或 `l` 回到了 `l_head` 。 ```c while (&l->list != l_head && _q_value_cmpare(l->value, r->value, descend) <= 0) l = list_entry(l->list.next, element_t, list); ``` 如果 `l` 回到了 `l_head` ,表示左右串列的所有元素都已經在正確位置。此時可以直接將右邊串列剩餘的部分追加到左邊串列的末尾,然後清空 `r_head` ,結束合併過程。這是一個優化步驟,能提升排序效率。 ```c if (&l->list == l_head) { // Append right list to left list l_head->prev->next = r_head->next; r_head->next->prev = l_head->prev; r_head->prev->next = l_head; l_head->prev = r_head->prev; INIT_LIST_HEAD(r_head); break; } ``` 如果左編串列還有元素需要調整,將右鏈表的元素 `r` 插入到 `l` 的前面,完成一次合併操作。 ```c list_move_tail(r_head->next, &l->list); } } ``` ---- 接著介紹 `_q_merge_sort` ```c void _q_merge_sort(struct list_head *head, bool descend) ``` 這個函式通過遞迴從中細分串列直到當前陣列為單一元素,接著合併左右串列來完成整個串列的排序。 首先先判斷結束條件,用 `list_is_singular` 判斷當前的串列長度是否等於1,接著用之前定義過的函式 `_q_find_mid` 來取得串列的中間點;要切分的地方 ```c { if (list_is_singular(head)) return; struct list_head *l = head, *r = &(struct list_head) {}, *mid = _q_find_mid(head->next, head->prev), *l_end = mid->prev; ``` 從中間節點 `mid` 分割成兩個獨立的串列:左半部分以 `l` 為頭,右半部分以 `r` 為頭。 ```c r->next = mid; mid->prev = r; r->prev = l->prev; l->prev->next = r; l->prev = l_end; l_end->next = l; ``` 對左半部分和右半部分分別進行遞迴排序,直到每個子串列都成為單一元素。 ```c _q_merge_sort(l, descend); _q_merge_sort(r, descend); ``` 最後使用 `_q_merge` 將排序好的左半部分和右半部分合併成一個完整的排序串列。 ```c _q_merge(l, r, descend); } ``` ---- 最後的最後 `q_sort` 只需要呼叫 `_q_merge_sort(head, descend)` ,就可以拿到排序好的串列。 ### Implement q_merge [4e501c7](https://github.com/WavJaby/lab0-c/commit/4e501c756bd888e9daf5d9f2bfdcdc772fb0134f) 這個函式輸入了一個 `queue_contex_t` 的鏈結串列,我先拿了第一個元素當作要合併到的目標 `out` ```c queue_contex_t *out = list_first_entry(head, queue_contex_t, chain), *node; ``` 接著自訂一個迴圈從 `out` 的下一個元素開始,直到串列頭。 前面已經實作了 `_q_merge` ,就可以利用他將 `node->q` 鏈結串列追加到 `out->q` ```c for (node = element_next(out, chain); &node->chain != head; node = element_next(node, chain)) { _q_merge(out->q, node->q, descend); } ``` </br> </br> </br> </br> </br> </br> </br> </br> </br> </br> </br> </br> </br> </br> </br> </br> </br> </br> </br> </br> </br> </br> </br> </br> </br> </br> </br> </br> </br> </br> </br> </br>