# 2025q1 Homework1 (lab0) contributed by < `alexc-313` > ### Reviewed by `SimonLiu423` 1. 在 `q_free()` 的實作中(commit [aca44ec](https://github.com/sysprog21/lab0-c/commit/aca44eca6483d44880c5a602f5ad933d8ce06cf2)),每次在釋放節點前會呼叫 `list_del(cur)` 將節點從該 list 中移除,但這步驟是可以省略的,因為釋放目前節點不會影響 `list_for_each_safe` 的運作,所以沒必要確保刪除過程中 list 的每個節點 `prev`、`next` 都是合法的。 2. `queue.h` 中已有 `q_release_element()` 函式可以去釋放指定的 element,為了增加可讀性及可維護性,建議將 `q_free()`、`q_delete_dup()` 中釋放節點的部分改為利用這個函式達成。 3. 大部分 commit message 中都沒有提及你如何實作該函式,會讓讀者難以理解你為何做這次修改,以及你如何實作。 4. 在 `queue_insert_*` 中, ```c ... int len = strlen(s) + 1; new->value = malloc(sizeof(char) * len); if (!new->value) { free(new); return false; } ... ``` 可以確定每次新增一個節點,該節點的 `value` 必定不會是 `NULL`,如此一來,在 `q_delete_node()` 中是否沒有必要先檢查再釋放? ```c ... if (node->value) free(node->value); ... ``` 5. Commit [e1647b8](https://github.com/sysprog21/lab0-c/commit/e1647b8fcd9106470537849cfb3331f3b2b4f16b) 中有不相干的修改。 6. `q_swap()` 中沒有判斷傳入的 `head` 是否為 `NULL` 或 list 是否為空,可能會造成不合法的記憶體存取。 7. `q_merge()` 若改為將所有節點移到第一個 queue,隨後在進行一次 `q_sort()`,速度是否會比原本的做法快? 8. Shuffle result 的直方圖中,X 軸的值重疊,無法看出該軸代表意義為何,該段落也沒有加以說明,建議可以為每個排列對應一個索引值,如 `1234 -> 0, 1243 -> 1, ...`,並用該索引值取代原本 X 軸的值。 ### Reviewed by `NeedToDebugMyLife` 1. Commit message 應該包含程式的實作方式,而不是只有程式的功能。像是 Commit [eb73a28](https://github.com/sysprog21/lab0-c/commit/eb73a284f8979d2c40f1802b987ec1a8b1e3e6f6) 中,你的 commit message 只寫了 "This function reverses the list in groups of K elements at a time.",這段話說明了 `q_reverseK` 這個函式的 "功能",而不是你實作這個函式的 "方式"。<br> 一個專案可能是跟其他開發者和未來的自己共同開發維護的。當不同開發者要針對你寫的程式做改動時,就能透過瀏覽你寫的 commit message 的內容來快速了解程式的運作原理是什麼,以快速針對問題來做處理,但如果 commit message 只寫了函式的功能,就會造成其他人需要先"重頭讀一遍"並且"理解"你的程式,才能夠對問題作後續的處理。這樣會大幅降低專案維護以及開發的效率。 2. Commit [112896d](https://github.com/sysprog21/lab0-c/commit/112896d5945ba2c2d0344fd86d9d76d3ba851b60) 中實作了 `q_delete_node` 函式,這部分可以使用 `<list.h>` 的 `list_del()` 加上 `<queue.h>` 內的 `q_release_element()` 來實現。 {%hackmd NrmQUGbRQWemgwPfhzXj6g %} ## 開發環境 ```shell $ 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: 48 bits physical, 48 bits virtual Byte Order: Little Endian CPU(s): 12 On-line CPU(s) list: 0-11 Vendor ID: AuthenticAMD Model name: AMD Ryzen 5 7640U w/ Radeon 760M Graphics CPU family: 25 Model: 116 Thread(s) per core: 2 Core(s) per socket: 6 Socket(s): 1 Stepping: 1 Frequency boost: enabled CPU(s) scaling MHz: 32% CPU max MHz: 4971.0000 CPU min MHz: 400.0000 BogoMIPS: 6986.86 Virtualization features: Virtualization: AMD-V Caches (sum of all): L1d: 192 KiB (6 instances) L1i: 192 KiB (6 instances) L2: 6 MiB (6 instances) L3: 16 MiB (1 instance) NUMA: NUMA node(s): 1 NUMA node0 CPU(s): 0-11 ``` ## 針對佇列操作的程式碼實作 ### q_head > Commit [117a180](https://github.com/sysprog21/lab0-c/commit/117a180a42f5733cb899c0d1eda96fce92e4216b) 使用 `malloc` 分配所需的記憶體,並用 `list.h` 中的 `INIT_LIST_HEAD` 函式來初始化佇列的 head。 ### q_free > Commit [aca44ec](https://github.com/sysprog21/lab0-c/commit/aca44eca6483d44880c5a602f5ad933d8ce06cf2) 在判斷 `head` 不是空指標後,用 `list.h` 中的 `list_for_each_safe` 巨集走訪每個節點,因為此巨集使用 `*safe` 來預先存放下一個指標,我們便可以使用 `free` 先釋放 `value` 使用的記憶體,再用第二個 `free` 釋放 `node` 本身所使用的記憶體,在走訪完整個佇列後,再用一次 `free` 釋放 `head`。 ### q_insert head/tail > Commit [a38b8fc](https://github.com/sysprog21/lab0-c/commit/a38b8fc814c53f02ce66a0ee6985a628c40b2cdd) 因為這兩個函式在概念上非常類似,所以在實作上我將這兩個函式放在同一個commit中。方法都是先判斷`head` 不是空指標後,用 `malloc` 逐次分配並確認記憶體成功的被分配,依據 [CERN Common vulnerabilities guide for C programmers](https://security.web.cern.ch/recommendations/en/codetools/c.shtml) 的建議,改用 `strncpy` 函式複製要插入佇列的數值,再分別使用 `list.h` 中的 `list_add` 或 `list_add_tail` 函式將複製完成的變數插入佇列中。 ### q_remove head/tail > Commit [f62cfd5](https://github.com/sysprog21/lab0-c/commit/f62cfd5f629f2f714d9f29802ffe092561114872) 如同上個 commit,這兩個函式在概念上非常類似,所以在實作上我也將這兩個函式放在同一個commit中。方法都是先判斷 `head` 不是空指標且非空佇列後,用 `list.h` 中的 `list_first_entry` 或 `list_last_entry` 找出目標節點的 `value`,再用 `strncpy` 函式複製到指定的變數,並確保字串的最後是終止字元 `\0` ,最後用 `list_del_init` 來移除節點並確保移除後的節點的指標有被妥善的處理,以避免後續使用上發生未定義的行為。 ### q_size > Commit [961c87b](https://github.com/sysprog21/lab0-c/commit/961c87bfdd318f898e81b941c378ece41bde5c19) 在判斷 `head` 是否空指標與是否是空佇列後,使用 `list.h` 中的 `list_for_each` 走訪並記錄節點數量。 ### q_delete_mid > Commit [e1647b8](https://github.com/sysprog21/lab0-c/commit/e1647b8fcd9106470537849cfb3331f3b2b4f16b) 在判斷 `head` 是否空指標與是否是空佇列後,使用快慢指標找出佇列的中點,將節點從佇列中釋放,並釋放節點所使用的記憶體。 ### q_delete_dup > Commit [f610366](https://github.com/sysprog21/lab0-c/commit/f610366bce1cb474a73b2a4f44905c4aa455b94f) 在判斷 `head` 是否空指標與是否是空佇列後,使用 `list.h` 中的 `list_for_each_entry_safe` 走訪整個佇列,當目前的節點的 `value` 跟下一個節點的 `value` 相同時,用變數 `found_dup` 記錄,並刪除目前節點。當目前節點不等於下個節點時,若 `found_dup == true` ,代表目前節點與上個已刪除的節點相同,故刪除該節點。 ### q_delete_node > Commit [112896d](https://github.com/sysprog21/lab0-c/commit/112896d5945ba2c2d0344fd86d9d76d3ba851b60) 在觀察到許多函式皆有使用以下程式碼後 ```c element_t *node = list_entry(n_ptr, element_t, list); list_del(n_ptr); free(node->value); free(node); ``` 我增加了 `q_delete_node` 來取代這些重複的程式碼。這個函式多了 ```c if (node->value) free(node->value); ``` 避免盲目的釋放記憶體。 ### q_swap > Commit [6608629](https://github.com/sysprog21/lab0-c/commit/66086299d8fd99b7d1a00f6a715dabde75ea0e3f) 使用 `list.h` 中的 `list_move` 函式讓 `cur` 跟 `cur->next` 互換,此時 `cur` 在原本 `cur->next` 的位置,再將 `cur` 設為 `cur->next` 就可完成兩兩互換。 ### q_reverse > Commit [bc01842](https://github.com/sysprog21/lab0-c/commit/bc01842d4b65d92ec0baaad0a1161c0e1b2a82c9) 在判斷 `head` 是否空指標與是否是空佇列後,使用 `list.h` 中的 `list_for_each_safe` 走訪整個佇列把每一個節點逐一用 `list_move` 移動到佇列的 `head->next` 位置。 ### q_reverseK > Commit [eb73a28](https://github.com/sysprog21/lab0-c/commit/eb73a284f8979d2c40f1802b987ec1a8b1e3e6f6) 使用兩個 for 迴圈,用 `*start` 來記錄第一個將被倒置的節點,內部迴圈用來倒置 `*start` 後的 K 個節點,外部迴圈用更新 `*start`。 ### merge_two > Commit [2424ca7](https://github.com/sysprog21/lab0-c/commit/2424ca77bb9280e48e6b52d2701e40d9c27df3f2) 這是一個 `q_sort` 的輔助程式,此函式接收兩個 `*list_head` 參數,並使用迭代的方式將兩個佇列以遞增或遞減的方式合併,實作上我使用 `list_del` 將兩個節點從各自的佇列中移除,在比較其 `value` 後用 `list_add_tail` 把較小或較大的節點加到新的佇列 `merge` ,當其中一個佇列為空時,若其中一個佇列有尚未加入 `merge` 的節點,利用 `list_splice` 把剩餘的節點加在 `merge` 尾端,並在將第一個或第二個 `*list_head` 指向合併完成的佇列後回傳。 > Commit [8873e16](https://github.com/sysprog21/lab0-c/commit/8873e165768e59d1486d51ea0464f1cb61a40131) 為了讓 `merge_two` 能被 `q_merge` 使用,我們必須做一些修改,因為我在 `q_merge` 中的寫法需要確保 `merge_two` 回傳並將第一個 `*list_head` 指向合併完的佇列。 ```c if (!list_empty(head_1)) { list_splice_init(&head, head_1); } else { list_splice_init(&head, head_2); list_splice_init(head_2, head_1); } ``` 所以在第二個佇列剰於的情況下,會多使用一個 `list_splice_init` 把第二個佇列接到第一個佇列上。 ### q_sort > Commit [2424ca7](https://github.com/sysprog21/lab0-c/commit/2424ca77bb9280e48e6b52d2701e40d9c27df3f2) 這個函式用遞迴實做 merge sort 演算法。 在用快慢指標找到佇列的中點後使用 `list_cut_position` 函式將佇列剖半,再呼叫自己並傳入剖半完的佇列,最後呼叫 `merge_two` 函式將所有佇列組合起來。 ### q_ascend/descend > Commit [8827730](https://github.com/sysprog21/lab0-c/commit/88277307c98288aab401a621e58a056f5c94bde7) 參考 `list.h` 中的 `list_for_each_safe` 寫出 ```c for (node = (head)->prev, safe = node->prev; node != (head); node = safe, safe = node->prev) ``` 來安全的反著走訪所有的節點,用 biggest 變數存放最大的字串,再用 `q_delete_node` 刪除 `value` 比 `biggest` 大或小的節點。 ### q_merge > Commit [94075cc](https://github.com/sysprog21/lab0-c/commit/94075cc419ef8bfeb470f8edd24b016e76169b75) 用 for 迴圈走訪除了第一個以外每個 `queue_contex_t` ,呼叫 `merge_two` 函式將佇列一個個合併到第一個佇列裡。 ### 結果 ![Screenshot from 2025-03-11 09-13-38](https://hackmd.io/_uploads/ryGlPQpoJe.png) 由於我尚未完成 `lab0-c` 的 Welch's test 測試項目,所以除了 `trace-17` 的部份沒有通過以外,其他部份皆順利通過。 ## 亂數 ### shuffle > Commit [a87564f](https://github.com/sysprog21/lab0-c/commit/a87564ff3fada11f262667d273b025a9a6916fdc) 在 `q_test.c` 中加入 `do_shuffle` 函式,再用 `ADD_COMMAND` 就可以使用新的命令 `shuffle`。 其中 `do_shuffle` 的實做方法如下: 先用 `q_size` 取得佇列長度 `len` ,用 `rand` 抽出一個在區間 $[0, len-1]$ 中的隨機數,並將 `len-1` 跟隨機數做指標交換,再將 len - 1。 ### 統計方法驗證 shuffle 測試 [1, 2, 3, 4] 做 shuffle 1,000,000 次結果: ``` Expectation: 41666 Observation: {'1234': 41517, '1243': 41531, '1324': 41621, '1342': 41430, '1423': 42093, '1432': 41945, '2134': 41828, '2143': 41186, '2314': 41658, '2341': 41775, '2413': 41515, '2431': 41375, '3124': 41727, '3142': 41975, '3214': 41575, '3241': 41663, '3412': 41318, '3421': 41791, '4123': 41669, '4132': 41943, '4213': 41993, '4231': 41601, '4312': 41728, '4321': 41543} chi square sum: 28.45183122929967 ``` 直方圖: ![Figure_1](https://hackmd.io/_uploads/SkvyMEps1x.png) 有 24 個樣本,自由度為 23 ,直方圖呈現 uniform distribution , chi square sum 為 28.45 。查卡方分佈表可知 p-value 介於 0.1~0.9 ,因為 P value(0.1~0.9)> alpha(0.05),所以統計檢定的結果不拒絕虛無假說。統計檢定的結果不拒絕虛無假說 $(H_0)$,也就是樣本資料不拒絕 shuffle 的結果遵循 Uniform distribution,因為沒有足夠證據推翻。 ### 啟發和疑惑 由於 Fisher–Yates shuffle 是適用於陣列的演算法,在佇列上使用因為取用每個節點的時間並非 $O(1)$ 造成時間複雜度大幅增加,從 $O(n)$ 變成 $O(n^2)$,應該要使用適合佇列的演算法或是將佇列轉換成陣列。