---
# System prepended metadata

title: 2025q1 Homework1 (lab0)

---

# 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)$，應該要使用適合佇列的演算法或是將佇列轉換成陣列。