# 2025q1 Homework1 (lab0)
contributed by <`Louis-Wup`>
{%hackmd NrmQUGbRQWemgwPfhzXj6g %}
## 開發環境
```
$ 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): 8
On-line CPU(s) list: 0-7
Vendor ID: GenuineIntel
Model name: 11th Gen Intel(R) Core(TM) i7-11370H @ 3.30GHz
CPU family: 6
Model: 140
Thread(s) per core: 2
Core(s) per socket: 4
Socket(s): 1
Stepping: 1
CPU(s) scaling MHz: 20%
CPU max MHz: 4800.0000
CPU min MHz: 400.0000
BogoMIPS: 6604.80
Virtualization features:
Virtualization: VT-x
Caches (sum of all):
L1d: 192 KiB (4 instances)
L1i: 128 KiB (4 instances)
L2: 5 MiB (4 instances)
L3: 12 MiB (1 instance)
NUMA:
NUMA node(s): 1
NUMA node0 CPU(s): 0-7
```
## Git 使用方法
我找到一個適合初學者的 git 教學網站
網站:https://www.runoob.com/git/git-tutorial.html
## 針對佇列操作的程式碼實作
:::info
在以下內容中, list_head 型態的變數與 element_t 型態的變數,都將稱為「節點」而非「元素」,因此即使使用如 list_for_each_entry_safe 等函式也會稱為「遍歷節點」,而非「遍歷元素」,而其 element_t 的 value 則稱為「節點內元素」或是「元素」。此外,注意所有佇列的實作皆為「雙向環狀鏈結串列」,因此若看到「遍歷佇列」等語句,其所指就是「遍歷鍊結串列」。不過,上述兩點配合前後文應能輕易理解其含意。
:::
### 了解佇列連接方式
在實做前建議先了解該題目佇列的連接方式,否則 `q_merge()` 有可能會不知如何下手
第一張圖是分別的樣子,以及指標的指向

第二張圖是佇列,也就是 element_t 串接的樣子

第三張圖是 queue_contex_t 串接的樣子 *特別注意 size 部分*

第四章就是完整的樣子

### 自行添加用以輔助的函式
- q_new_element()
- q_delete_element()
- q_strncmp()
- q_swap_two_node()
- q_bubble_sort()
- q_merge_sort()
- q_merge_two_lists()
### q_new()
> commit: [aad1da5
](https://github.com/sysprog21/lab0-c/commit/aad1da5845d9473e09aa716767a2caa54a427087)
#### 目標:建立空佇列
#### 想法與簡易流程:
1. 使用 `malloc` 分配記憶體空間並檢查是否成功
2. 使用 "list.h" 中的 `INIT_LIST_HEAD` 使其指標指向自身後回傳
### q_free()
>初始版本 commit: [aad1da5
](https://github.com/sysprog21/lab0-c/commit/aad1da5845d9473e09aa716767a2caa54a427087)
>修正版 commit: [9b6dcbc
](https://github.com/sysprog21/lab0-c/commit/9b6dcbc0739a604c1470b33ccbba43ec310c52a4)
#### 目標:釋放所有被佇列所佔用的記憶體空間
#### 想法與簡易流程:
1. 檢查佇列是否存在,若不存在則結束
2. 遍歷佇列中的所有節點,將其從佇列中移除後,使指標指向自身,並釋放記憶體
3. 釋放佇列頭部所佔的記憶體空間
#### 紀錄:
使用 "list.h" 的 `list_for_each_entry_safe` 時,因未正確理解變數 `entry` 與 `safe` 的功能,導致誤修改了錯誤的節點。實際上, `entry` 雖作為遍歷時的變數,仍可被修改,這是因為 `safe` 在 `entry` 變更前,已先記錄下一個要走訪的節點。如此,即使 `entry` 被刪除或修改,遍歷仍能透過 `safe` 賦值給 `entry` ,確保正確性。
```diff
- element_t *entry = NULL, *tmp = NULL;
- list_for_each_entry_safe (entry, tmp, head, list) {
- list_del_init(&tmp->list);
- q_release_element(tmp);
+ element_t *entry = NULL, *safe = NULL;
+ list_for_each_entry_safe (entry, safe, head, list) {
+ list_del_init(&entry->list);
+ q_release_element(entry);
```
### q_new_element()
>初始版本 commit: [773ef55
](https://github.com/sysprog21/lab0-c/commit/773ef5555caf180e15eeaed64e2278cf4ac9e90c)
>修正版 commit: [f0241e6
](https://github.com/sysprog21/lab0-c/commit/f0241e6c591c9ac4cb831d8dd8944ee291bb97c6)
#### 目標:生成新的佇列節點
#### 想法與簡易流程:
1. 使用 `malloc` 分配節點的記憶體空間並檢查是否成功
2. 使用 `malloc` 分配字串的記憶體空間並檢查是否成功,若失敗則 **釋放步驟 1 分配的記憶體**
3. 使用 `strncpy` 複製字串
4. 回傳指向該節點的指標
#### 紀錄:
一開始實作時忘記處理若 `malloc` 申請字串失敗時要釋放步驟 1. 分配的記憶體,導致在執行 `make test` 時,終端機出現錯誤。
```
TESTING trace trace-11-malloc:
# Test of malloc failure on insert_head
ERROR: Freed queue, but 3 blocks are still allocated
```
之後查看檔案 "trace-11-malloc.cmd",發現當 `malloc` 失敗後嘗試清空佇列時,會出現仍有節點未釋放的訊息。使用 Valgrind 檢查後,也確認問題發生在 `q_new_element` 函式。最終找出原因是當字串記憶體請求失敗時,忘記釋放節點步驟 1. 所佔用的記憶體空間,導致了這個記憶體洩漏的問題。
```
Freeing queue
ERROR: Freed queue, but 1 blocks are still allocated
==11558== 42 bytes in 1 blocks are definitely lost in loss record 1 of 2
==11558== at 0x4846828: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==11558== by 0x10F90B: alloc (harness.c:146)
==11558== by 0x10FA5E: test_malloc (harness.c:176)
==11558== by 0x10FE61: q_new_element (queue.c:44)
==11558== by 0x10FEEE: q_insert_tail (queue.c:75)
==11558== by 0x10CF78: queue_insert (qtest.c:233)
==11558== by 0x10D090: do_it (qtest.c:288)
==11558== by 0x10E74D: interpret_cmda (console.c:181)
==11558== by 0x10ED12: interpret_cmd (console.c:201)
==11558== by 0x10F7FC: run_console (console.c:659)
==11558== by 0x10DB3C: main (qtest.c:1444)
==11558==
==11558== 64 bytes in 1 blocks are still reachable in loss record 2 of 2
==11558== at 0x4846828: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==11558== by 0x10F90B: alloc (harness.c:146)
==11558== by 0x10FA5E: test_malloc (harness.c:176)
==11558== by 0x10FE48: q_new_element (queue.c:40)
==11558== by 0x10FEEE: q_insert_tail (queue.c:75)
==11558== by 0x10CF78: queue_insert (qtest.c:233)
==11558== by 0x10D090: do_it (qtest.c:288)
==11558== by 0x10E74D: interpret_cmda (console.c:181)
==11558== by 0x10ED12: interpret_cmd (console.c:201)
==11558== by 0x10F7FC: run_console (console.c:659)
==11558== by 0x10DB3C: main (qtest.c:1444)
==11558==
```
```diff
new_node->value = (char *) malloc(strlen(str) + 1);
- if (!new_node->value)
+ if (!new_node->value) {
+ free(new_node);
return NULL;
+ }
```
### q_insert_head() & q_insert_tail()
> commit: [067d4f6
](https://github.com/sysprog21/lab0-c/commit/067d4f6a9c626898e19d09473482386c7e549d98)
#### 目標:建立節點並插入佇列開頭 / 結尾
#### 想法與簡易流程:
1. 確認佇列是否存在
2. 使用 `q_new_element` 建立新節點並檢查是否成功
3. 使用 `list_add` / `list_add_tail` 將節點插入佇列頭部 / 尾部
### q_remove_head() & q_remove_tail()
> commit: [1774c2c
](https://github.com/sysprog21/lab0-c/commit/1774c2c6f5009143e164004cdce37df64cc1b2ab)
#### 目標:移除佇列開頭 / 結尾節點,並將內容複製到指定變數
#### 想法與簡易流程:
1. 確認佇列是否存在或為空
2. 使用 `list_first_entry` / `list_last_entry` 取得指向佇列中的第一個 / 最後一個節點的指標
3. 將該節點從佇列中移除,並讓其指標指向自身
4. 若該節點內元素非 `NULL` ,則使用 `strncpy` 將其複製到指定變數 `sp` 後回傳
### q_delete_element()
> commit: [b3a4024
](https://github.com/sysprog21/lab0-c/commit/b3a4024ac47bf3723817f38b6294f9d881898f61)
#### 目標:刪除特定節點
#### 想法與簡易流程:
1. 使用 `list_del_init` 將節點從佇列中移除,並讓其指標指向自身
2. 呼叫 `q_release_element` 釋放該節點佔用的記憶體空間
### q_delete_mid()
> commit: [aefa58c
](https://github.com/sysprog21/lab0-c/commit/aefa58c266d26150e668cea66bd4ad3b903f841b)
#### 目標:刪除佇列中央的節點
#### 想法與簡易流程:
1. 檢查佇列是否存在或為空,若是則回傳 `false`
2. 透過快慢指標的技巧,取得指向中央節點的指標
3. 透過 `q_delete_element` 刪除該節點
#### 紀錄:
一開始沒通過靜態程式分析
```
Running static analysis...
queue.c:146:28: style: Variable 'fast' can be declared as pointer to const [constVariablePointer]
for (struct list_head *fast = head->next;
```
之後將 `const` 補上去以增加安全行後通過了。
```diff
struct list_head *slow_mid = head->next;
- for (struct list_head *fast = head->next;
+ for (const struct list_head *fast = head->next;
fast != head && fast->next != head; fast = fast->next->next) {
slow_mid = slow_mid->next;
}
```
### q_strncmp()
> commit: [7991e1e
](https://github.com/sysprog21/lab0-c/commit/7991e1eee77bf099f7722499bd19d5216e27c5c9)
#### 目標:比較節點內元素的大小
#### 想法與簡易流程:
1. 使用 `strlen` 取得兩個比較對象的字串長度
2. 將較長字串的長度作為參數傳遞給 `strncmp` 使用並回傳結果
#### 紀錄:
這個函式主要是為了提高程式的可讀性,降低自身出錯的機率,因為原本的 `q_delete_dup` 就因為可讀性實在太差,導致我疏忽,未更新 `strncmp` 中使用的字串長度限制,這種錯誤可能會有潛在的安全問題,且在特定類型的輸入下會直接導致結果出錯(詳細於q_delete_dup說明),因此我決定做出該函式。
此外,當我將程式上傳時,未通過靜態程式碼檢查,需要補上 const 來提升安全性。
```diff
int q_strncmp(const struct list_head *a, const struct list_head *b)
{
- element_t *e_a = list_entry(a, element_t, list);
- element_t *e_b = list_entry(b, element_t, list);
+ const element_t *e_a = list_entry(a, element_t, list);
+ const element_t *e_b = list_entry(b, element_t, list);
...
return strncmp(e_a->value, e_b->value, n);
}
```
### q_delete_dup()
> 初始版本 commit: [367ba63
](https://github.com/sysprog21/lab0-c/commit/367ba635050b17e42da72216ac167b4dca0af4a3)
> 修正版 commit: [04e95f0
](https://github.com/sysprog21/lab0-c/commit/04e95f04767a0d0b23bdf04b2de769fd550c8d4c)
#### 目標:若佇列內存在內容重複的節點,則將含有重複內容之節點全部刪除
#### 想法與簡易流程:
1. 檢查佇列是否存在或為空,或是只有一個節點
2. 使用兩個指標 `from` 和 `to` ,分別標記內容重複的起始與結束節點。遍歷佇列時,若遇到內容重複的節點,則將變數 `delete` 設為 `true` ,並繼續遍歷,直到找到內容不同的節點或佇列結束
3. 若 `delete` 為 `true` ,則將其重設為 `false` ,並刪除 `from` 到 `to` (含)的所有節點
4. 若遍歷結束則完成,否則回到步驟 2
#### 紀錄:
一開始我想使用 "list.h" 的 `list_for_each_entry_safe` 來遍歷佇列,不過這樣寫需要在遍歷的開頭檢查比較字串是否為空,因為一開始並沒有比較對象,像是
```c
list_for_each_entry_safe(entry, safe, head) {
if (!from)
from = entry;
else if(q_strncmp(from, entry))
...
}
```
而用 macro 又不能使第一個取得的節點變成 `head->next->next` 。因此,若每次迴圈開頭都必須執行一個除了第一次一定為 `false` 的 `if` 判斷,會對不具備分支預測,或者僅具備靜態分支預測(`if` 總是預測不會跳轉)的電腦造成大量時間浪費。於是我決定改為手動寫 `while` 迴圈來遍歷。
之後在上傳時沒通過靜態檢測,有兩部份要改,首先是要補上 `const` 增加安全性
```
queue.c:173:16: style: Variable 'e_from' can be declared as pointer to const [constVariablePointer]
element_t *e_from = NULL, *e_to = NULL;
^
queue.c:173:32: style: Variable 'e_to' can be declared as pointer to const [constVariablePointer]
element_t *e_from = NULL, *e_to = NULL;
```
```diff
- element_t *e_from = NULL, *e_to = NULL;
+ element_t const *e_from = NULL, *e_to = NULL;
```
接著是另一個問題,要將變數 `n` 放進迴圈內宣告
```
queue.c:174:9: style: The scope of the variable 'n' can be reduced. [variableScope]
int n = 0;
queue.c:174:11: style: Variable 'n' is assigned a value that is never used. [unreadVariable]
int n = 0;
```
```diff
- int n = 0;
while (from != head && to != head) {
e_from = list_entry(from, element_t, list);
e_to = list_entry(to, element_t, list);
- n = strlen(e_from->value) > strlen(e_to->value)
+ int n = strlen(e_from->value) > strlen(e_to->value)
? strlen(e_from->value) + 1
: strlen(e_to->value) + 1;
```
這部分我有些疑慮,我是故意將變數 `n` 放在迴圈外宣告,這樣可以避免變數 `n` 一直被重複宣告,避免不必要的開銷,我認為這樣做可能會對效能有所幫助。因此,接下來我打算查找相關資料或進行實驗,來驗證這樣的處理是否真的會對效能產生影響。
`TODO` : 變數於迴圈內重複宣告是否有效能影響
後來我自己測試時發現原始版本還存在一個問題是在移動 `to` 指標時,未更新傳遞給 `strncmp` 的字串長度限制。雖然測試資料沒有出現問題,但存在著潛在的安全風險,以及可能導致錯誤的結果。因此,我決定一方面修掉該問題,一方面提高程式碼可讀性,將 `list_entry` 以及大量使用 `strlen` 的部分移到另一個函式 `q_strncmp` 中,這樣程式碼相比原本變得更加簡潔。
```diff
while (from != head && to != head) {
- e_from = list_entry(from, element_t, list);
- e_to = list_entry(to, element_t, list);
- int n = strlen(e_from->value) > strlen(e_to->value)
- ? strlen(e_from->value) + 1
- : strlen(e_to->value) + 1;
- while (to != head && !strncmp(e_from->value, e_to->value, n)) {
+ while (to != head && !q_strncmp(from, to)) {
delete = true;
to = to->next;
- e_to = list_entry(to, element_t, list);
}
```
### q_swap_two_node()
> 原始版本 commit: [b4f7c6c
](https://github.com/sysprog21/lab0-c/commit/b4f7c6cf697523285d5072f74982929db3fe1626)
#### 目標:將傳入的兩節點互換位置
#### 想法與簡易流程:
1. 判斷是否為相鄰節點,若是則步驟 2 後結束,否則步驟 3 後結束
2. 先宣告變數紀錄 `left->prev` 和 `right->next` 後,處理 `left` 和 `right` 的指標,再處理 `left->prev` 和 `right->next`
3. 先宣告變數紀錄 `left->prev`, `left->next`, `right->prev` 以及 `right->next` 後,處理 `left` 和 `right` 的指標,再處理 `left->prev`, `left->next`, `right->prev` 以及 `right->next`
#### 紀錄:
該方法需要判斷傳入的兩節點是否相鄰,因此需要一個 `if-else` 的判斷式,但修改內容又有大部分雷同,因此嘗試使用教授上課教的間接指標把 `if-else` 去掉,不過沒有成功,因此後來又上網查找是否有方法把 `if-else` 判斷移除後,成功找到了,該方法也確實是透過間接指標來處理特例,以下是討論串連結。
Reference(最後一個回答) : https://stackoverflow.com/questions/20095529/swapping-nodes-in-double-linked-list
### q_reverse()
> 原始版本 commit: [b82a4f3
](https://github.com/sysprog21/lab0-c/commit/b82a4f3a247321d930ab3e425a7b409f6803ffd8)
> 修正版 commit: [e89369e
](https://github.com/sysprog21/lab0-c/commit/e89369e11d3be873bd91555c3f9a989806274279)
#### 目標:將佇列反轉
#### 想法與簡易流程:
1. 檢查佇列是否存在、為空或僅有一個節點
2. 使用 `list_for_each_safe` 遍歷佇列,將每個節點的 `next` 指向原本的 `prev` ,並將 `prev` 指向原本的 `next`
3. 最後,同樣調整 `head` 的 `next` 和 `prev` ,完成整個佇列的反轉
#### 紀錄:
我的初始實作方法是透過兩個指標,一個正向、一個反向逐一走訪,並在每一步使用 `q_swap_two_node` 交換節點。為了確保正確性,我額外使用了兩個指標來記錄下一步要訪問的節點。然而,這種方法雖然直觀,但包含過多的 `if-else` 判斷與額外的指標變更,影響效率。
後來,我在網路上找到了一種更好的方式,不僅消除了 `if-else` 判斷與不必要的指標變動,還省去了 `q_swap_two_node` 的使用,更重要的是,這個方法同樣適用於單向鏈結串列的反轉。
以下是簡化的程式碼比對,詳細內容請參閱 [commit 9664572](<https://github.com/sysprog21/lab0-c/commit/96645725e821b91b3416402ad8c7227754276f09>)。
演算法 Reference: https://takeuforward.org/data-structure/reverse-a-doubly-linked-list/
```diff
github 內程式碼中並未移除 q_swap_two_node
此處的「移除」只是表示新的實作方法已經不再依賴該函式
- q_swap_two_node()
- {
- if (l->next == r) {
- ...
- } else {
- ...
- }
- }
q_reverse()
{
- while (l != r) {
- ...
- q_swap_two_node(l, r);
- if (l->prev == r)
- break;
- ...
- }
+ list_for_each_safe (entry, safe, head) {
+ entry->next = entry->prev;
+ entry->prev = safe;
+ }
+ ...
}
```
### q_reverseK()
> commit: [fb0a58e
](https://github.com/sysprog21/lab0-c/commit/fb0a58eedfd3957c867817669d93ea9e8be91b33)
#### 目標:以 k 個節點為一組反轉
#### 想法與簡易流程:
這個函式的主要實做想法是一次將佇列前 k 個節點取出並反轉後,放入其他佇列尾部,不斷重複到整個佇列都完成反轉
1. 檢查佇列是否存在、為空、僅有一個節點或 k = 1
2. 建立兩個雙向環狀鍊結串列 `tmp_list` 和 `reverse_list` ,前者用於暫存從 `head` 移走的 k 個節點以便後續反轉,後者用於存放所有反轉後的節點
3. 遍歷佇列,每當走訪第 k 個節點時,使用 `list_cut_position` 將其移動至 `tmp_list` ,再使用 `q_reverse` 反轉,最後透過 `list_splice_tail_init` 將排序好的節點加入 `reverse_list`
4. 最後,使用 `list_splice` 將 `reverse_list` 併回 `head` ,完成以每 k 個節點為一組的整個佇列的反轉
#### 紀錄:
當時最一開始我的想法其實是使用 `list_cut_position` 以每 k 個節點為單位儲存到一個新型態的鍊結串列,這個鍊結串列每個節點都為一個雙向環狀鍊結串列與 `next` 指標,之後只要透過 `list_cut_position` 以每 k 個節點為一組組成鏈接串列後,每組反轉在串接即可,但準備實作後發現兩個問題,第一個是若 k 很小,則需要大量的而外空間,最差要 O(n) 的額外空間,第二個是該函式沒有回傳值,換言之該函式不能有失敗的風險,否則若失敗了自己也不會知道,因此不能使用含有 `malloc` 這類有可能失敗的函式。
### q_swap()
> commit: [fb0a58e
](https://github.com/sysprog21/lab0-c/commit/fb0a58eedfd3957c867817669d93ea9e8be91b33)
#### 目標:節點兩兩一組反轉
#### 簡易流程:
1. 將 k = 2 作為參數傳遞給 `q_reverseK`
### q_ascend() / q_descend()
> 原始版本 commit: [0941ff7
](https://github.com/sysprog21/lab0-c/commit/0941ff7e20d405f4bc656b5d9e469ca210bef21f)
> 修正版 commit: [a2de161
](https://github.com/sysprog21/lab0-c/commit/a2de161179698faec8573251aa2e864d8543a886)
#### 目標:將佇列從小到大 / 從大到小排列,若遇到順序錯誤的節點直接刪除
#### 想法與簡易流程:
1. 檢查佇列是否存在或為空,或僅有一個節點
2.
- q_ascend(): 使用兩個指標 `small` 和 `big` ,並分別初始化為 `head->next` 以及 `head->next-next` 後,一起從佇列**頭部向尾部**遍歷,途中若遇到順序錯誤的節點直接**刪除**,順序正確則計數器值加一
- q_descend(): 使用兩個指標 `big` 和 `small` ,並分別初始化為 `head->prev->prev` 以及 `head->prev` 後,一起從佇列**尾部向頭部**遍歷,途中若遇到順序錯誤的節點直接*刪除*,順序正確則計數器值加一
3. 回傳計數計值
#### 紀錄:
一開始我計算回傳值時犯了錯,原以為 `small` 和 `big` 指標都有指向節點,所以初始值應該為 2,但其實應該是 0,因為 while loop 還會比較一次。結果執行時出現了 `Segmentation fault` ,於是我使用 Valgrind 檢測程式後,得到了以下錯誤訊息:
```
==18817== Invalid read of size 1
==18817== at 0x4850367: strcmp (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==18817== by 0x10BD5F: do_ascend (qtest.c:755)
==18817== by 0x10E74D: interpret_cmda (console.c:181)
==18817== by 0x10ED12: interpret_cmd (console.c:201)
==18817== by 0x10F7FC: run_console (console.c:659)
==18817== by 0x10DB3C: main (qtest.c:1444)
==18817== Address 0xdeadbeef is not stack'd, malloc'd or (recently) free'd
==18817==
Segmentation fault occurred. You dereferenced a NULL or invalid pointer==18817=
```
發現是 `qtest` 裡面 `strcmp` 出問題,因此立刻想到是比較到不應該比較的部份,並發現是回傳大小出錯導致 `qtest` 裡面迴圈跑過頭。修正後,錯誤訊息轉變為有些節點未釋放,但 "queue.c" 以及 "queue.h" 該函式上方英文說明都為 remove ,因此我去仔細查看 "queue.h" 該函式的詳細說明後,發現其中有一行指出
```c
// Memory allocated to removed nodes must be freed.
```
因此,我將節點的移除操作改為刪除,變得可以成功執行了。最後,我還發現原本 lab0-c 的 repository 中已經有人提交了相關的 pull request ,只是尚未通過。
此外在實作 `q_descend` 函式時突然想到, LeetCode 中的題目是單向鍊結串列,所以無法直接反向搜尋,而一時之間又想不出好解法,我就去 LeetCode 找解法,找到一個跟上述 ascend 類似的作法(Reference裡面的第三種解法),它的時間複雜度是 O(n),空間複雜度是 O(1),不過最重要的是這個作者對他的解法提供了圖解,使人能夠輕鬆理解其邏輯。
Reference : https://leetcode.com/problems/remove-nodes-from-linked-list/solutions/5073124/remove-nodes-from-linked-list/
### q_bubble_sort()
> 原始版本 commit: [344417a
](https://github.com/sysprog21/lab0-c/commit/344417a20e957f415d1fabb238f98f0ec7dda92e)
> 修正版 commit: [a883983
](https://github.com/sysprog21/lab0-c/commit/a883983ada37253b591bd0fac264e1fc7689a38b)
#### 目標:將佇列從大到小 / 從小到大排序
#### 想法與簡易流程:
1. 宣告一個指標 `ptr_end` ,並將其設為 `head`
2. 透過指標 `ptr_a` 和 `ptr_b` 從佇列頭部開始,逐一向 `ptr_end` 走訪,直到遇到 `ptr_end` 。在這個過程中,若發現需要調整順序的節點,則使用 `q_swap_two_node` 函式進行交換
3. 在每一輪遍歷後,更新 `ptr_end` 為 `ptr_end->prev` ,並重複步驟 2 ,直到 `ptr_end` 指向 `head->next` 為止
#### 紀錄:
該函式一開始是實做在 `q_sort` ,想說只是 Bubble Sort 且之前已經實作過 `q_swap_two_node` ,所以可以簡單快速地實現這個排序。然而,結果還是出現了錯誤。第一個錯誤是在交換節點後,指標順序會對調,我忘記將指標復原成原來的順序。第二個錯誤是在 Bubble Sort 的內層迴圈中,我用來作為終止條件的指標指向的節點是可以交換的,因此當該節點被交換時,會導致終止條件出現錯誤。後來成功修復後因為效能考量(測試資料效能測試沒過),就將該函式移出 `q_sort` 獨自存在,並改用 Merge Sort 來實做 `q_sort` 。
後來測試時發現,我的作法雖然從小到大是穩定排序,但從大到小不是,因為我的作法是單純使用 `xor` 運算,等號的有無會造成其中一個方向的排序變成不穩定,還需要想有沒有除了 `if-else` 以外的解決方法。
```c
// 下方是 Bubble sort 使否交換的判斷
if ((q_strncmp(ptr_a, ptr_b) > 0) ^ descend) {
q_swap_two_node(ptr_a, ptr_b);
...
}
```
`TODO:` 解決從大到小的不穩定排序。
### q_merge_sort()
> commit: [487a0c5
](https://github.com/sysprog21/lab0-c/commit/487a0c58de54d7cd06531f4aa30d6403f1aa1059)
#### 目標:將佇列從大到小 / 從小到大排序
#### 想法與簡易流程:
1. 檢查佇列是否為空或只有一個節點,若是,則直接回傳該佇列。
2. 使用快慢指標方法,找到 Merge Sort 的中間斷開點。
3. 宣告並建立新的佇列(注意,這裡我沒有使用 `malloc` ,而是直接宣告)。
4. 使用 `list_cut_position` 來將佇列一分為二。
5. 分別對這兩個子佇列遞迴呼叫 `q_merge_sort`。
6. 合併兩個已排序的佇列後回傳合併結果。
#### 紀錄:
這個函式不實作在 `q_sort` 內的原因是,原本使用 Bubble Sort 實作 `q_sort` ,但由於效率問題,測試資料無法通過,因此我改為使用 Merge Sort 實作。然而,我希望保留 Bubble Sort 的實作,因為未來可能還有使用的機會,所以我將 Bubble Sort 以及 Merge Sort 獨立出來,並讓 `q_sort` 決定應該呼叫哪個函式。至於步驟 3,我故意不使用 `malloc` 來產生新佇列,因為 `q_sort` 函式沒有回傳值,應盡量避免失敗的可能性,所以不使用可能會失敗的 `malloc` ,且這樣的宣告方式可以在函式結束時自動釋放記憶體空間,無需手動 `free` ,如此還避開了大量的 `malloc` 及 `free` 的使用。另外,雖然回傳值其實是可以省略的,我也注意過並測試過了,但不回傳似乎有些不太對勁,因此我還是保留了讓函式有回傳值。
### q_sort()
> 相關 commit 請移步到 q_bubble_sort() 以及 q_merge_sort()
#### 目標:將佇列從大到小 / 從小到大排序
#### 想法與簡易流程:
1. 呼叫 `q_merge_sort`
### q_merge_two_lists()
> 初始版本 commit: [58c552e
](https://github.com/sysprog21/lab0-c/commit/58c552e75a186fc38b8972e9e188468d1debdc03)
> 修正版 commit: [8f8acdd
](https://github.com/sysprog21/lab0-c/commit/8f8acdde5a8a8f4faf43dea2868af4ea4b59e965)
> 再修正版 commit: [05da83b
](https://github.com/sysprog21/lab0-c/commit/05da83bbce0a7f86222afc221eca9012ddf50bea)
#### 目標:將兩個排序過得佇列合併,並依然保持由大到小 / 由小到大排列
#### 想法與簡易流程:
實作方式是以 `head_a` 為開頭,依序串接節點,當其中一個佇列串接完畢後,直接將剩餘佇列銜接至尾部並回傳。
1. 宣告 `ptr` 指向 `head_a` ,用來指向需更新的位置
2. 宣告 `a` 和 `b` 分別指向 `head_a->next` 和 `head_b->next`
3. 使用 `a` 和 `b` 逐一走訪各自的佇列,並透過 `q_strncmp` 取得下一個應該加入的節點的指標給 `node` (指向 指向目標節點的指標)後更新指標,直到其中一個佇列遍歷完畢
4. 將剩餘的佇列直接串接到 `head_a` 的尾部,最後回傳 `head_a`
#### 紀錄:
一開始我的想法是額外宣告一個佇列,然後將要合併的佇列逐一加入該佇列裡,當有一個完成時,另一個直接串起來,最後在將整條合併後的佇列指派給 `head_a` 後回傳。不過後來發覺裡面有沒必要的操作,像是額外開一個佇列,以及把佇列還回去,我明明可以直接用 `head_a` 當開頭來串接其他節點,還有我想到在上課的 [linked list 和非連續記憶體操作](https://hackmd.io/@sysprog/c-linked-list) 中有使用間接指標去減少 `if-else statement` 大括號裡面的程式碼的技巧,雖然我還需要查資料或做實驗了解為什麼要這麼做,明明一樣都是一個 `if-else` 的判斷加特定行數須執行,這些程式在 `if-else` 的大括號裡面跟外面執行究竟有沒有效能上的差異,還是只是因為這樣程式碼比較"乾淨"。
`TODO:` 實驗程式碼在 `if-else` 大括號裡面和外面對效能有無影響。
下方為部份差異,可以看到透過間接指標,將 `if-else` 大括號裡的程式碼提出來,使程式碼變比較乾淨,至於詳細改動請看 修正版 commit: [8f8acdd
](https://github.com/sysprog21/lab0-c/commit/8f8acdde5a8a8f4faf43dea2868af4ea4b59e965)
```diff
- INIT_LIST_HEAD(ptr);
- while (a != head_a && b != head_b) {
- if ((q_strncmp(a, b) ^ descend) < 0) {
- ptr->next = a;
- a->prev = ptr;
- a = a->next;
- } else {
- ptr->next = b;
- b->prev = ptr;
- b = b->next;
- }
- ptr = ptr->next;
- }
+ for (struct list_head **node = NULL; a != head_a && b != head_b;
+ *node = (*node)->next) {
+ node = (q_strncmp(a, b) ^ descend) < 0 ? &a : &b;
+ ptr->next = *node;
+ (*node)->prev = ptr;
+ ptr = (*node);
+ }
```
後來測試又發現上述程式碼有兩個問題,第一個是把運算順序打反了
```diff
- node = (q_strncmp(a, b) ^ descend) < 0 ? &a : &b;
+ node = ((q_strncmp(a, b) < 0) ^ descend) ? &a : &b;
```
按照之前的版本,這個 `descend` 根本無法對 `q_strncmp` 的結果造成什麼影響,除了 LSB 可能會差個一;改正後是再比較大小後,根據 `descend` 判斷是否選擇另一個節點。而另外一個問題則是跟 `q_bubble_sort` 一樣,因為對同大小處理的不同,在從大到小排序時會是不穩定排序,而有沒有除了 `if-else` 以外的解決方法目前仍在尋找中。
`TODO:` 解決從大到小的不穩定排序。
### q_merge()
> commit: [58c552e
](https://github.com/sysprog21/lab0-c/commit/58c552e75a186fc38b8972e9e188468d1debdc03)
#### 目標:將多個排序過得佇列合併,並依然保持由大到小 / 由小到大排列
#### 想法與簡易流程:
直接透過 `q_merge_two_list` 不斷頭尾合併,直到最後剩一個佇列即可
1. 取得一共有多少佇列,並判斷是否須往下執行
2. 使用雙層迴圈,外層負責判斷還須多少輪內層迴圈,內層迴圈負責頭尾佇列的合併。
3. 回傳 `q_size` 的結果
#### 紀錄:
一開始對於 `queue_contex_t` 的結構不是很了解,花了一堆時間使用 `printf` 來推測具體結構,終於推測出來,結果放在該區塊最一開始了,不過了解區塊架構後,剩下就簡單了,基本沒碰到什麼問題,就是對問題敘述有一個疑惑
`However, q_merge() is responsible for making the queues to be NULL-queue, except the first one.`
那到底要不要將 `q` 指向 `NULL` 呢,因為沒有那就不符合題目敘述,將 queues 變成 NULL-queue, 而是 Empty-queue, 但如果指向 `NULL` ,那那些 queue 的 head 不就沒有 `free` ,變成 memory-leak 嘛?目前(沒有 sync)我測試是有沒有指向 `NULL` 都會過就是了。
---
## 在 qtest 提供新的命令 shuffle
### 實做 q_shuffle()
> 原始版本 commit: [1702b1e
](https://github.com/sysprog21/lab0-c/commit/1702b1ec0a562c6ad605f017f3cc6c9f15b13bb8)
> 修正版 commit: [5980d4c
](https://github.com/sysprog21/lab0-c/commit/5980d4c4eb5b4348aad7a093b9f1ccf87659e454)
依照 [Fisher–Yates shuffle](https://ithelp.ithome.com.tw/articles/10337601) 演算法實做 `q_shuffle()` 很簡單,比較需要額外注意的是我們正常隨機取 0 ~ n-1 的整數時會直接對 `rand()` 後的結果模 n ,當佇列很小時,每個元素被選擇的機率基本還是維持均勻分佈,但當佇列大到一定程度時,分佈就不均勻了,每次被選擇的基本都將是 0 ~ `RAND_MAX` 的節點,因此我將取得變數的部份改成如下:
```diff
- struct list_head *ptr = head;
- for (int r = rand() % turn + 1; r; r -= 1) {
+ struct list_head *ptr = head->next;
+ int r = rand();
+ for (r = (int) ((float) r / ((unsigned) RAND_MAX + 1) * turn); r;
+ r -= 1) {
ptr = ptr->next;
}
```
不過即使是目前這種方法,當佇列遠遠大於 `RAND_MAX` 時,節點被選擇的機率仍舊不會是均勻分佈,每次選擇時都會有幾個節點是無法被選到的,這是因為我們的亂數相較於佇列長度有限,這裡舉個例子:
- 假設佇列長度為 100000 , 而 `RAND_MAX` 為 32767 (C99 Standard 只保證 `RAND_MAX` 大於等於 32767),則透過上面方法隨機出 r 為 32766 和 32767 時,對佇列的選擇會是節點 99993 和 99996 ,因為 $$ \lfloor 32766 / 32768 * 100000 \rfloor = 99993 $$ $$ \lfloor 32767 / 32768 * 100000 \rfloor = 99996 $$
可以發現不但跳過了 99994 和 99995 ,且 99997 ~ 99999 的節點根本不能被選到(假定32767已經是最大亂數),這是因為亂數小於佇列長度必然的限制,就像我們不能用 1 bit 表達 0, 1, 2 一樣,我們沒有足夠大的亂數可以去表達所有我們希望出現的數字,若非要表達只能產生第二個亂數。因此我們可以發現當佇列長度遠大於 `RAND_MAX` 時仍舊不會是均勻分佈(這裡注意隨著每次進行,乘上的數字會越來越小,所以真正從頭到尾無法被選到的數字只有最後幾個節點,至於其他節點則是輪流無法被選擇),但至少可以取得後面的節點,相較於直接取模仍舊好多了。
### 新增命令到 qtest
> commit: [1702b1e
](https://github.com/sysprog21/lab0-c/commit/1702b1ec0a562c6ad605f017f3cc6c9f15b13bb8)
基本上,參考其他命令(如 descend)就可以輕鬆加入 shuffle 命令。只需要宣告 `do_shuffle()` ,然後在 `console_init()` 中使用 `ADD_COMMAND()` 加入命令它即可。比較麻煩的是,因為作業規定不能修改 "queue.h" ,所以 `q_shuffle()` 直接放在 "queue.c" 裡時, "qtest.c" 會無法使用它。這是因為 "qtest.c" 只有 include "queue.h" ,而 "queue.h" 又沒有宣告 `q_shuffle()` 。
如果讓 "qtest.c" 直接 include "queue.c" 也是不應該的。最後,我找到了一個解決方案,使用 `extern` ,只需要在 `do_shuffle()` 上方加上:
```c
extern void q_shuffle(struct list_head *head);
```
這樣 "qtest.c" 就能正確調用 "queue.c" 中的 `q_shuffle()` 了。
### 檢驗 q_shuffle() 的亂度品質