# 2025q1 Homework1 (lab0)
contributed by <`As7r1d`>
{%hackmd NrmQUGbRQWemgwPfhzXj6g %}
## 開發環境
```
$ gcc --version
gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
Copyright (C) 2021 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): 8
On-line CPU(s) list: 0-7
Vendor ID: GenuineIntel
Model name: Intel(R) Core(TM) i5-8250U CPU @ 1.60GHz
CPU family: 6
Model: 142
Thread(s) per core: 2
Core(s) per socket: 4
Socket(s): 1
Stepping: 10
CPU max MHz: 3400.0000
CPU min MHz: 400.0000
BogoMIPS: 3600.00
```
## 開發過程
### q_new
- **功能:** 根據作業要求建立新的空佇列。
- **透過程式碼可以觀察到:**
- NULL queue 表示佇列未成功初始化,Empty queue 表示佇列已初始化但無元素。而 q_new 函式是要求建立一個 Empty queue 。
- 空佇列 (Empty queue) 結構:
- 1 個 head 指標去指向空佇列
- head 所指向的 next (head->next) 指向 head 自己
- head 所指向的 prev (head->prev) 指向 head 自己
- **實作內容:**
- 從 [list.h](https://github.com/As7r1d/lab0-c/blob/master/list.h) 標頭檔中發現有許多已定義 API 可用,所以我使用`INIT_LIST_HEAD` 達成該函式要求,並且使用 `malloc` 配置記憶體區塊給 head。
- **程式碼:**[Commit 20e2f3d](https://github.com/sysprog21/lab0-c/commit/20e2f3d97f778da75ce37ac50fabd7da4d0e47ad)
- **qtest 執行:**
```
./qtest
cmd> new
l = []
cmd> ih a
l = [a]
cmd> ih b
l = [b a]
cmd> ih c
l = [c b a]
cmd> free
l = NULL
```
### q_free
- **功能:** 釋放佇列所佔用的記憶體。
- **想法:**
- 追蹤 [queue.h](https://github.com/As7r1d/lab0-c/blob/master/queue.h) 標頭檔中,發現定義 `element_t` ,`element_t` 表示鏈結串列中之元素。
- q_free 函式釋放 `element_t` 所佔據的空間。
- element_t
- 結構:
- 指向字元陣列
- 雙向鏈結串列之節點
- **實作內容:**
- 先檢查 head 是否為 NULL。
- 使用 [list.h](https://github.com/As7r1d/lab0-c/blob/master/list.h) 中 `list_for_each_safe` 來遍歷佇列中 `element_t` 並刪除節點。
- 使用 [list.h](https://github.com/As7r1d/lab0-c/blob/master/list.h) 中 [`list_del`](https://github.com/As7r1d/lab0-c/blob/0c90a5c1abf1460ab750f915efaa2c48429c4e48/list.h#L144) 從鏈結串列中移除該節點。
- free(entry->value):如果 value 指標指向的記憶體不為 NULL,則釋放。
- free(entry):釋放 `element_t` 節點本身。
- 釋放 head
- **程式碼:**[Commit 20e2f3d](https://github.com/sysprog21/lab0-c/commit/20e2f3d97f778da75ce37ac50fabd7da4d0e47ad)
- **qtest 執行:**
```
./qtest
cmd> new
l = []
cmd> ih a
l = [a]
cmd> ih b
l = [b a]
cmd> free
l = NULL
```
### q_insert_head 和 q_insert_tail
- **功能:**
- q_insert_head:在佇列開頭 (head) 插入 (insert) 給定的新節點 (以 LIFO 準則)。
- q_insert_tail :在佇列尾端 (tail) 插入 (insert) 給定的新節點 (以 FIFO 準則)。
- 想法:
- 這兩個函式的核心概念相同,主要差異在於插入的位置不同
- 會使用到 `strdup()` 為一個字串分配新的記憶體,並把原始字串的內容複製過去。這樣就不會跟原始字串共用同一塊記憶體。
- 實作內容:
- 檢查 head 是否為 NULL ,如果 head 為 NULL,直接回傳 false。
- `malloc(sizeof(element_t))` 動態配置記憶體空間給新節點。
如果 malloc 失敗,回傳 false。
- `strdup(s)` 為 value 配置記憶體並存入字串內容,複製字串 s 並存入 value。
```c
new_element->value = strdup(s);
if (!new_element->value) {
free(new_element);
return false;
}
list_add(&new_element->list, head);
```
- 將新節點加入佇列
- q_insert_head:用 `list_add(&new_element->list, head)` 把新節點加到佇列開頭。
- q_insert_tail:用 `list_add_tail(&new_element->list, head)` 把新節點加到佇列尾端。
- 成功插入後回傳 true
- **程式碼:**[Commit d0eaa19](https://github.com/sysprog21/lab0-c/commit/d0eaa19b91504f6331f4032fee95ce37bd722e3c)
- **qtest:**
```
cmd> new
l = []
cmd> ih a
l = [a]
cmd> ih b
l = [b a]
cmd> it c
l = [b a c]
cmd> it d
l = [b a c d]
```
### q_remove_head 和 q_remove_tail
- **功能:**
- q_remove_head:在佇列開頭 (head) 移去 (remove) 給定的節點。
- q_remove_tail:在佇列尾端 (tail) 移去 (remove) 給定的節點。
- 這兩個函式會回傳被移除的 element_t 節點,但不會幫你 free()。
- **實作內容:**
- q_remove_head:取 head->next 第一個節點。
- q_remove_tail:取 head->prev 即最後一個節點。
- 使用 `list_entry(node, type, member)` 從 `struct list_head *` 回推到 `type *(element_t *)`。
```c
if (!head || list_empty(head)) {
return NULL;
}
struct list_head *first = head->next;
element_t *element = list_entry(first, element_t, list);
```
- 如果 `sp` 不是 NULL,就複製字串內容
- 為了確保不會超過緩衝區的大小,把 element->value 複製到 `sp`,但最多只會複製 `bufsize - 1` 個字元
- 加上 `\0` ,確保字串結束。
```c
if (sp != NULL && bufsize > 0) {
strncpy(sp, element->value, bufsize - 1);
sp[bufsize - 1] = '\0';
}
```
- 程式碼: [Commit 5eb769f](https://github.com/sysprog21/lab0-c/commit/5eb769f1e62c79ef610e4e921df0151de3db4aa3)
- **qtest:**
```
l = [c b a]
cmd> rh
Removed c from queue
l = [b a]
cmd> rt
Removed a from queue
l = [b]
```
### q_size
- **功能:**計算目前佇列的節點總量
- **實作內容:**就是透過 while 迴圈遍歷整個鏈結串列,從 head->next 開始,沿著 next 一直走,直到回到 head 為止,每走一個節點就讓 count 加 1,最後回傳總數。
- **程式碼:**[Commit 189849e](https://github.com/sysprog21/lab0-c/commit/189849eb8991aa6a8094ea15684ee2904f0e66c2)
- **qtest:**
```
cmd> ih a
l = [a]
cmd> ih b
l = [b a]
cmd> ih c
l = [c b a]
cmd> size
Queue size = 3
l = [c b a]
```
### q_delete_mid
- **功能:** 移走佇列中間的節點,詳見 LeetCode 2095. Delete the Middle Node of a Linked List
- **實作內容:** 使用快慢指針的方式找到中間點,slow 指標一次前進 1 步,fast 指標一次前進 2 步。當 fast 走到底時,slow 剛好走到中間節點。
```c
while (fast->next != head && fast->next->next != head) {
slow = slow->next;
fast = fast->next->next;
}
element_t *element = list_entry(slow, element_t, list);
list_del(slow);
```
- **程式碼:**[Commit 9795cc0](https://github.com/sysprog21/lab0-c/commit/9795cc08915f6c861a48a24c6889d204545e8a6a)
- **qtest:**
```
cmd> size
Queue size = 3
l = [c b a]
cmd> dm
l = [c a]
```
### q_delete_dup
- **功能:** 在已經排序的狀況,移走佇列中具備重複內容的節點,詳見 LeetCode 82. Remove Duplicates from Sorted List II
- **實作內容:**
`current` 代表目前正在檢查的節點。從佇列的第一個有效節點開始,不包含 head。一一比對 current 和後面的所有節點,找出重複的字串。
```c
struct list_head *current = head->next;
```
`strcmp()` 比對兩個字串,如果相等,表示找到重複的節點
```c
if (strcmp(current_element->value, compare_element->value) == 0)
```
如果 found_duplicate 為 true,代表 current 這個節點也有重複,直接刪掉。如果沒有重複,current 就繼續往下一個節點移動。
```c
if (found_duplicate) {
list_del(current);
}
```
- **程式碼:** [Commit 9795cc0](https://github.com/sysprog21/lab0-c/commit/9795cc08915f6c861a48a24c6889d204545e8a6a)
- **qtest:**
```
l = [c b c c a]
cmd> dedup
ERROR: Duplicate strings are in queue or distinct strings are not in queue
l = [b a]
```
由於會出現上述error,目前還在修正。
### q_swap
- **功能:** 交換佇列中鄰近的節點。
- **實作內容:**
- 如果佇列長度<= 1,不需要交換,直接返回。
- 取得 current(第一個節點)和 current->next(第二個節點),刪除 second 節點,然後將它插入到 first 之前。更新current節點繼續進行交換下一對節點
```c
struct list_head *first = current;
struct list_head *second = current->next;
list_del(second);
list_add(second, first->prev);
current = first->next;
```
- **程式碼:** [Commit d9a5993](https://github.com/sysprog21/lab0-c/commit/d9a59930cfa7a3cd623090b507f60cbcd2305836)
- **qtest:**
```
l = [h g f e d c b a]
cmd> swap
l = [g h e f c d a b]
cmd>
```
### q_reverse
- **功能:** 以反向順序重新排列鏈結串列,該函式不該配置或釋放任何鏈結串列節點。
- **實作內容:**
- 遍歷整個佇列並交換 next 和 prev
```c
while (current != head) {
temp = current->next;
current->next = current->prev;
current->prev = temp;
current = temp;
}
```
`temp = current->next` :暫存 current->next,確保反轉後能找到下一個節點。
`current->next = current->prev` :讓 next 指向原本的 prev。
`current->prev = temp` :讓 prev 指向原本的 next
- **程式碼:**[Commit a745816](https://github.com/sysprog21/lab0-c/commit/a74581604150ec22fcc01b31c0efbb4df6729d1e)
- **qtest:**
```
l = [g h e f c d a b]
cmd> reverse
l = [b a d c f e h g]
```
### q_reverseK
- **功能:** 類似 q_reverse,但指定每 k 個節點為一組,進行反向順序排列,詳見 LeetCode 25. Reverse Nodes in k-Group
- **實作內容:**
`list_cut_position()` 這個函式,它能夠直接將前 k 個節點切割出來形成一個新的鏈結。當 k 個節點被切割出來後,接下來反轉。這裡直接利用了 q_reverse(),因為它已經能夠有效地反轉整個雙向鏈結串列,所以不需要再手寫反轉邏輯,這樣可以讓程式碼更模組化,提升可讀性。
反轉後的 k 個節點需要拼接回結果,因此使用 `list_splice_tail()` 來將反轉後的區塊加到一個新的 result 鏈結。
最後,處理剩餘不足 k 的節點時,直接將它們拼接到 result,不做任何反轉。
- **程式碼:** [Commit a745816](https://github.com/sysprog21/lab0-c/commit/a74581604150ec22fcc01b31c0efbb4df6729d1e)
- **qtest:**
```
l = [j i b a d c f e h g]
cmd> reverseK 2
l = [i j a b c d e f g h]
```
### q_sort
- **功能:** 以遞增順序來排序鏈結串列的節點。
- **實作內容:**
- 選擇 Merge Sort 來實現
- 將鏈結串列拆成兩半,這樣可以遞迴地對較小的部分進行排序,最終再合併回去,然後再使用快慢指標來找出鏈結串列的中間點( slow 一次走 1 步,fast 一次走 2 步,fast 到達尾端時,slow 剛好在中間點)
- 透過 `list_cut_position(&left, head, slow)` 將 head 後半段的節點分割成 left,這樣 head 和 left 各自代表原來的前半段和後半段。
```c
struct list_head left;
struct list_head *fast, *slow;
INIT_LIST_HEAD(&left);
slow = head->next;
fast = head->next->next;
while (fast != head && fast->next != head) {
slow = slow->next;
fast = fast->next->next;
}
list_cut_position(&left, head, slow);
q_sort(&left, descend);
q_sort(head, descend);
```
- 要合併排序後的兩個部,去比較 left 和 head 的第一個節點,根據 strcmp() 的結果來決定排序方式,使用 `list_del(chosen)` 從原來的鏈結中移除這個節點,`list_add_tail(chosen, &result)` 將選出的節點加入 result。重複這個過程,直到 left 或 head 其中一個先變空。
- 程式碼:[Commit 4d3fa7e](https://github.com/sysprog21/lab0-c/commit/4d3fa7e85249af19f20dc527ac810e69b6bd2b6d)
- 執行 make test 時發現排序是unstable:
```
# Test of insert_head and remove_head
# Test of insert_head, insert_tail, remove_head, remove_tail, and delete_mid
# Test of insert_head, insert_tail, remove_head, reverse and merge
# Test of insert_head, insert_tail, size, swap, and sort
ERROR: Not stable sort. The duplicate strings "bear" are not in the same order.
# Test of insert_head, insert_tail, remove_head, reverse, size, swap, and sort
ERROR: Not stable sort. The duplicate strings "bear" are not in the same order.
# Test of insert_head, insert_tail, delete duplicate, sort, descend and reverseK
ERROR: Not stable sort. The duplicate strings "gerbil" are not in the same order.
# Test of truncated strings
# Test operations on empty queue
# Test remove_head with NULL argument
# Test operations on NULL queue
# Test of malloc failure on insert_head
# Test of malloc failure on insert_tail
# Test of malloc failure on new
# Test performance of insert_tail, reverse, and sort
Warning: Skip checking the stability of the sort because the number of elements 2000000 is too large, exceeds the limit 100000.
# Test performance of sort with random and descending orders
# 10000: all correct sorting algorithms are expected pass
# 50000: sorting algorithms with O(n^2) time complexity are expected failed
# 100000: sorting algorithms with O(nlogn) time complexity are expected pass
ERROR: Not stable sort. The duplicate strings "vujqm" are not in the same order.
ERROR: Not stable sort. The duplicate strings "vujqm" are not in the same order.
ERROR: Not stable sort. The duplicate strings "iwkui" are not in the same order.
ERROR: Not stable sort. The duplicate strings "iwkui" are not in the same order.
ERROR: Not stable sort. The duplicate strings "cqson" are not in the same order.
Error limit exceeded. Stopping command execution
# Test performance of insert_tail
# Test if time complexity of q_insert_tail, q_insert_head, q_remove_tail, and q_remove_head is constant
Testing insert_tail...(0/10)
```
因此修改如下,使排序成為stable:
```diff
- if ((descend && cmp > 0) || (!descend && cmp < 0))
+ if ((descend && cmp >= 0) || (!descend && cmp <= 0))
```
- **qtest:**
```
l = [i j a b c d e f g h]
cmd> sort
l = [a b c d e f g h i j]
```
### q_descend 和 q_ascend
- **功能:**
- q_ascend:移除所有右側有更小值的節點,最後只保留從左到右遞增的節點。
- q_descend:移除所有右側有更大值的節點,最後只保留從左到右遞減的節點。
- **實作內容:**
這兩個函式的目標類似,一開始的思路是左到右遍歷並檢查右側的節點,但當節點過多時對於較大的鏈結串列會很慢。因此參考 [LeetCode 2487. Remove Nodes From Linked List](https://leetcode.com/problems/remove-nodes-from-linked-list/description/) 。直接從左到右遍歷並檢查右側的節點,如果我們先反轉鏈結串列,從右側開始往左掃描,那麼右側的節點已經是處理過的,這樣我們只需要單向遍歷一次,時間複雜度變成 O(n)。
反轉鏈結串列:
```c
q_reverse(head);
```
這樣我們從左到右遍歷時,其實是從原本的最右側往左掃描。
遍歷 cur,確保 cur 是目前掃描到的最大(最小)值。next_node 負責檢查 cur 之後的節點。針對 q_ascend,如果 next_node 有比 cur 小 的值,就刪除 next_node;針對 q_descend,如果 next_node 有比 cur 大的值,就刪除 next_node。最後透過 list_del() 安全移除 next_node,並釋放記憶體。
```c
struct list_head *cur = head->next;
while (cur != head) {
struct list_head *next_node = cur->next;
while (next_node != head) {
element_t *cur_element = list_entry(cur, element_t, list);
element_t *next_element = list_entry(next_node, element_t, list);
struct list_head *next_next = next_node->next;
if (strcmp(cur_element->value, next_element->value) < 0) {
list_del(&next_element->list);
free(next_element->value);
free(next_element);
}
next_node = next_next;
}
cur = cur->next;
}
```
- **程式碼:** [Commit 4d3fa7e](https://github.com/sysprog21/lab0-c/commit/4d3fa7e85249af19f20dc527ac810e69b6bd2b6d)
- **qtest:**
```
l = [b a d c f e h g j i]
cmd> ascend
l = [a c e g i]
```
```
l = [h d b a c e g i]
cmd> descend
l = [i]
```
### q_merge
- **功能:**合併所有已排序的佇列,並合而為一個新的已排序佇列
- **實作內容:**
當在構思 q_merge 這個函式時,要將多個已排序的鏈結串列合併為一個,並且根據 descend 參數來選擇升序或降序排序。不需要頻繁地比較和插入,而是透過 list_splice_tail() 一次性合併所有鏈結串列,再進行一次排序。
取出 head 內的第一個 queue_contex_t:
```c
queue_contex_t *first_ctx = list_entry(head->next, queue_contex_t, chain);
struct list_head *result_queue = first_ctx->q;
```
queue_contex_t 是一個封裝鏈結串列的結構,其中 q 存放具體的鏈結串列。first_ctx->q 會作為合併後的最終結果。
遍歷 head 內的所有 queue_contex_t,將所有鏈結串列合併:
```c
struct list_head *chain_node = head->next->next;
while (chain_node != head) {
queue_contex_t *current_ctx = list_entry(chain_node, queue_contex_t, chain);
struct list_head *current_queue = current_ctx->q;
if (!list_empty(current_queue)) {
list_splice_tail(current_queue, result_queue);
first_ctx->size += current_ctx->size;
INIT_LIST_HEAD(current_queue);
current_ctx->size = 0;
}
chain_node = chain_node->next;
}
```
遍歷 head 內的所有 queue_contex_t,取得 current_queue(當前佇列)。如果 current_queue 不是空的,將 current_queue 透過 list_splice_tail() 併入 result_queue,然後更新 first_ctx->size,因為 result_queue 現在包含了更多節點。清空 current_queue,避免重複計算。繼續遍歷 head 內的下一個 queue_contex_t。
最後對 result_queue 進行排序
```c
q_sort(result_queue, descend);
```
最終合併後的大小
```c
return first_ctx->size;
```
- **程式碼:**[Commit 4d3fa7e](https://github.com/sysprog21/lab0-c/commit/4d3fa7e85249af19f20dc527ac810e69b6bd2b6d)
- **qtest:**
```
l = [f d c b a]
cmd> new
l = []
cmd> ih g
l = [g]
cmd> ih h
l = [h g]
cmd> ih a
l = [a h g]
cmd> ih b
l = [b a h g]
cmd> merge
l = [a a b b c d f g h]
```
## 論文〈Dude, is my code constant time?〉
## 以 Valgrind 分析記憶體問題