# 2024q1 Homework1 (lab0)
contributed by < `RRbell1027` >
### Reviewed by `SimonLee0316`
* 在git commit的時候 不同功能應該分開
* commit message 並沒有寫,可以參考[如何寫一個 Git Commit Message](https://blog.louie.lu/2017/03/21/%E5%A6%82%E4%BD%95%E5%AF%AB%E4%B8%80%E5%80%8B-git-commit-message/)
> 明確指出 git commit 並提供參考做法,實際做一次給學員看,不要只提「心法」。
> :notes: jserv
### Reviewed by `SHChang-Anderson`
* 筆記中可以加入 commit 紀錄連結,以更好追蹤討論相關程式碼。
* 嘗試使用 List API 如 : list_move_tail 或 list_move 透過不同順序移動節點達成 q_swap 實作。
* shuffle 實作中,你對四個節點的鏈結串列進行實驗,但由於只執行了 1000 次測試,無法驗證是否符合 Uniform distribution ,可以嘗試先針對三個節點的鏈結串列進行實驗,提高測試的次數,觀察實驗結果。
### Reviewed by `Shawn531`
* Shuffle 實作中提到 Fisher–Yates shuffle 演算法在鏈結串列上會比陣列多處理上 n 倍的時間。是否可以說明以及提出解決方法。
* Shuffle 實作中,考慮 `old` 剛好為串列中最後一個元素之狀況或許能幫助改善結果。
* 可以附上 commit 連結。
### Reviewed by `ShawnXuanc`
* commit 中有看到標題為 `1-13 and 16 complete` 可能會不太容易了解意思,提交內容一次涵蓋太多功能
* 可以嘗試將程式碼重複利用像是 `q_remove` 以及 `q_insert` 的部份
### Reviewed by `Shiang1212`
在[如何寫一個 Git Commit Message](https://blog.louie.lu/2017/03/21/%E5%A6%82%E4%BD%95%E5%AF%AB%E4%B8%80%E5%80%8B-git-commit-message/) 有提到:限制標題最多只有 50 字元。
> 50 字元的限制不是一個硬規則,而是一個經驗法則。讓標題保持在 50 字以下能夠確保標題的可讀性,並且強迫作者思考如何用更簡潔的方式表達發生什麼事情。
如 [d9f91cb](https://github.com/sysprog21/lab0-c/commit/d9f91cbbf85221313bc19cbd3002c4be6d6d6b12) 就沒有滿足要求。儘可能的概括這個 commit 的更動內容,能有效提高 commit message 的可讀性。如果你發現沒辦法在 50 字內描述這個 commit,那你該想想是否在該 commit 內做了太多的更動。
## 開發環境
```shell
$ gcc --version
gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.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): 12
On-line CPU(s) list: 0-11
Vendor ID: GenuineIntel
Model name: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
CPU family: 6
Model: 158
Thread(s) per core: 2
Core(s) per socket: 6
Socket(s): 1
Stepping: 10
CPU max MHz: 4100.0000
CPU min MHz: 800.0000
BogoMIPS: 4399.99
Virtualization: VT-x
L1d: 192 KiB (6 instances)
L1i: 192 KiB (6 instances)
L2: 1.5 MiB (6 instances)
L3: 9 MiB (1 instance)
NUMA node0 CPU(s): 0-11
```
## 針對佇列操作的程式碼實作
:::danger
避免過多的中英文混用,已有明確翻譯詞彙者,例如「鏈結串列」(linked list) 和「佇列」(queue),就使用該中文詞彙,英文則留給變數名稱、人名,或者缺乏通用翻譯詞彙的場景。
:::
:::info
好的,已改寫。
:::
依照我原先對鏈結串列的了解,在看完 lab0 敘述後,我原以為的 `list_head` 結構體會是:
```c
struct list_head {
struct list_head *prev;
struct list_head *next;
char *value;
};
```
在想要存取的資料 `value` 之上加上指標 `prev` `next` 來實作資料間的串連。因此,當翻閱 `list.h` 以及 `queue.h` 對 `struct lsit_head` 的運用感到非常訝異。c 語言不具備類別 ,但卻可以運用物件在結構中擺放的順序實作繼承(is a),更可以添加功能豐富且成熟的結構實作組合(has a) 概念,利用 `container_of` 存取包含 `struct list_head` 的 `element_t` 。藉此,便不需要為每個具有鏈結串列特性的資料集都實作一整個函式庫。
`struct list_head` 設計了不含有效資料的指標頭 `header`,操作時使用指向 `header` 的指標 `*head`,實作[「linked list 和非連續記憶體」](https://hackmd.io/@sysprog/c-linked-list)中提到的間接指標技巧,在寫完作業回過頭複習時 <s>格外印象深刻</s>。
:::danger
不用說「格外印象深刻」,資訊科技領域變化極快,你每天都會發現令你「印象深刻」的議題。
:::
本次作業要求滿足 `qtest.c` 中的 17 項題目要求,目前進度為功能完善(trace-01-ops 到 trace-13-mlloc 完成),卻缺乏效率(除了 trace-16-perf 以外都沒過關)。在後續的檢討中我將近一步改善程式效能,並研讀課程筆記裡的論文。
### `q_new`
初始化 `queue->q` ,依照需求建立「」。
`queue.h` 的註釋,空佇列是指 `queue->q` 需包含 `head` 且 `header->next` 和 `header->prev` 皆指向 `head` 。
```graphviz
digraph {
rankdir = LR
node [shape=plaintext];
head [label=<<table>
<tr><td port="next">next</td>
<td>value</td>
<td port="prev">prev</td></tr>
</table>>];
q->head[constraint=false];
head:next:w->head:w;
head:prev:e->head:e;
}
```
:::danger
allocate 的翻譯是「配置」,以區隔 dispatch (分配/分發) 的翻譯。
:::
可以在 `malloc` <s>分配</s> 空間後,利用 `list.h` 中的巨集`INIT_LIST_HEAD` 來實作。
要注意確認 `malloc` 是否成功指派空間, `qtest.c`會模擬指派空間失敗。
```c
struct list_head *head = malloc(sizeof(struct list_head));
if (!head)
return NULL;
INIT_LIST_HEAD(head);
```
:::danger
避免非必要的項目縮排 (即 `* ` 和 `- `),以清晰、明確,且流暢的漢語書寫。
:::
時間複雜度和空間複雜度都是 $O(n)$
### `q_free`
`list.h` 提供了相當於 `container_of` 的 `list_entry` 巨集,並結合 `list_each` 實作迭代 `element_t` 的迴圈。
```diff
- for (struct list_head *li = head->next; li != head; li = li->next) {
- element_t *ele = (element *)((char *)li - offset(element_t, lsit));
+ element_t *ele;
+ list_for_each_entry(ele, head, list) {
```
`list.h `提供了 `list_for_each_safe` 以防對當前節點的操作會導致丟失下個節點。
值得注意的是,`element_t->value` 是 `char *`,在接下來的 `q_insert_head` 和 `q_insert_tail` 中都是指派 `strdup` 的回傳值,可以通過 `free()` 來清空記憶體空間。
:::danger
避免非必要的項目縮排 (即 `* ` 和 `- `),以清晰、明確,且流暢的漢語書寫。
:::
空間複雜度 $O(1)$ ,時間複雜度 $O(n)$
### `q_insert_head`
利用 `list.h` 提供的 `list_add` 可以將節點放入 linked list 頭部。
```c
element_t *ele = malloc(sizeof(element_t));
/* ... */
list_add(&ele->list, head);
```
:::danger
避免非必要的項目縮排 (即 `* ` 和 `- `),以清晰、明確,且流暢的漢語書寫。
:::
要注意 `strdup` 內部也使用了 malloc 來分配空間,所以要做檢查。
```c
ele->value = strdup(s);
if (!ele->value) {
free(ele);
return false;
}
```
空間複雜度 $O(1)$ ,時間複雜度 $O(1)$
### `q_insert_tail`
與 `q_insert_head` 同理。
空間複雜度 $O(1)$ ,時間複雜度 $O(1)$
### `q_remove_head`
先用指標指向 target element 避免找不到空間, `list_del` 不會更動目標節點資訊,有可能發生非預期結果。
:::danger
避免非必要的項目縮排 (即 `* ` 和 `- `),以清晰、明確,且流暢的漢語書寫。
:::
如果不加上 `\0` , 當 `bufsize` 小於 `strlen(node->value)` 會導致字串讀取不會終止。
```c
strncpy(sp, node->value, bufsize - 1);
sp[bufsize - 1] = '\0';
```
空間複雜度 $O(1)$ ,時間複雜度 $O(1)$
### `q_remove_tail`
與 `q_remove_head` 同理。
空間複雜度 $O(1)$ ,時間複雜度 $O(1)$
### `q_size`
原先想利用 `q_contex_t` 尋找 `size` ,後續發現無法從 `*head` 找到 queue 的 `q_contex_t` ,於是用回作業牛刀小試的程式碼。
:::danger
避免非必要的項目縮排 (即 `* ` 和 `- `),以清晰、明確,且流暢的漢語書寫。
:::
空間複雜度 $O(1)$ ,時間複雜度 $O(n)$
### `q_delete_mid`
利用快慢指標找尋中間節點。
<s>[time=Wed, Mar 6, 2024 11:42 PM]list_sort.h 程式碼對快慢指標找中間節點有更好的寫法,記得改。</s>
:::danger
HackMD 會記錄日期,你不用在開發紀錄中標示。
:::
```c
struct list_head *slow = head->next, *fast = head->next;
while (fast->next != head && fast->next->next != head) {
slow = slow->next;
fast = fast->next->next;
}
list_del(slow);
```
空間複雜度 $O(1)$ ,時間複雜度 $O(n)$
### `q_delete_dup`
利用兩個迴圈來比對兩節點字串是否重複,因為外迴圈不會訪問到內迴圈中已經被刪除的節點,故時間複雜度不是 $O(n^2)$ 而是 $O(n)$ 。
:::danger
避免非必要的項目縮排 (即 `* ` 和 `- `),以清晰、明確,且流暢的漢語書寫。
:::
因為會更改到當前外迴圈<s>訪問</s> 節點的下個節點,所以 `for` 中的 `safe` ,要在內迴圈跳出後才能指派。
:::danger
access 的翻譯是「存取」,而非「訪問」(visit)
:::
```c
for (li = head->next; li != head; li = safe) {
e1 = list_entry(li, element_t, list);
bool flag = false;
while (li->next != head) {
e2 = list_entry(li->next, element_t, list);
if (strcmp(e1->value, e2->value) == 0) {
flag = true;
/* free e2 */
} else {
break;
}
}
safe = li->next;
if (flag) {
/* free e1 */
}
}
```
空間複雜度 $O(1)$ ,時間複雜度 $O(n)$
### `q_swap`
* 利用兩個指標兩個兩個訪問節點,故迭代的 `for` 迴圈要另外寫。
* `l1` 和 `l2` 交換後順序發生了變動,expression-3 要注意寫法。
```c
for (l1 = head->next, l2 = head->next->next; l1 != head && l2 != head;
l1 = l1->next, l2 = l2->next->next->next)
```
空間複雜度 $O(1)$ ,時間複雜度 $O(n)$
:::danger
HackMD 不是讓你張貼完整程式碼的地方,GitHub 才是!因此你在開發紀錄只該列出關鍵程式碼 (善用 diff 標示),可附上對應 GitHub commit 的超連結,列出程式碼是為了「檢討」和「便於他人參與討論」,不是用來「假裝自己有付出」
:::
### `q_reverse`
想在一個迴圈內連同 `head` 一起處理,故不使用 `list.h` 的巨集,而實作了 do-while 迴圈。
```c
do {
/* swap next and prev */
} while (li != head);
```
空間複雜度 $O(1)$ ,時間複雜度 $O(n)$
### `q_reverseK`
新增兩個 linked list head `rev` 和 `batch` ,利用 `list_cut_position` 將 K 個節點裁切到 `batch` 翻轉後暫存到 `rev` ,來確保 `batch` 之間的順序不變。
```c
list_for_each_safe (li, safe, head) {
cnt++;
if (cnt == k) {
/* remove k node
* reverse
* store into rev
*/
}
}
```
空間複雜度 $O(1)$ ,時間複雜度 $O(n \times k)$
### `q_sort`
<s>[筆記未完成]</s>
:::danger
不要輕易說「完成」,工程領域是持續的檢討和改進。
> 好的,後續會再優化或更新
:::
:::danger
某些濫用「優化」的語境,根本與「改良」和「改善」無異,那為何不直接講「改良」和「改善」呢?
$\to$ [資訊科技詞彙翻譯](https://hackmd.io/@sysprog/it-vocabulary)
:::
* 使用了快速排序法,因為無法分配陣列所以用遞迴。
* 模仿了測驗一的演算法,將節點分成三部分:大於 piv、 piv 和小於等於 piv
```c
/* Seperate list into 3 part.*/
list_for_each_entry_safe (ele, safe, head, list) {
list_del(&ele->list);
if ((strcmp(pivot->value, ele->value) > 0) ^ descend)
list_add(&ele->list, &left);
else
list_add(&ele->list, &right);
}
/* recursive */
q_sort(&left, descend);
q_sort(&right, descend);
/* store back to list*/
list_splice(&left, head);
list_add_tail(&pivot->list, head);
list_splice_tail(&right, head);
```
空間複雜度 $O(log(n))$,時間複雜度 $O(nlog(n))$
:::danger
明確標注你參閱的 GitHub 帳號名稱。
:::
參考了<s>同學</s> 程式碼後,嘗試使用合併排序法,效率高於快速排序法。
:::danger
避免非必要的項目縮排 (即 `* ` 和 `- `),以清晰、明確,且流暢的漢語書寫。
:::
使用遞迴將佇列二分,並利用 `q_merge` 做合併。
```c
q_sort(&left, descend);
q_sort(head, descend);
/* Create a queue chain for q_merge */
queue_contex_t left_queue = {.q = &left};
queue_contex_t right_queue = {.q = head};
LIST_HEAD(q_head);
list_add_tail(&right_queue.chain, &q_head);
list_add_tail(&left_queue.chain, &q_head);
q_merge(&q_head, descend);
```
:::danger
你如何確保目前的測試已涵蓋排序演算法的最差狀況?
:::
### `q_ascend`
* 反過來迭代,當前節點如果大於下一個節點就刪除。
```c
/* iterate from the last to first */
for (struct list_head *li = head->prev->prev, *safe = li->prev; li != head;
li = safe, safe = li->prev) {
element_t *cur = list_entry(li, element_t, list),
*pre = list_entry(li->next, element_t, list);
/* if cur > pre */
if (strcmp(cur->value, pre->value) > 0) {
/* free cur */
}
}
```
空間複雜度 $O(1)$ ,時間複雜度 $O(n)$
### `q_descend`
與 `q_ascend` 同理。
空間複雜度 $O(1)$ ,時間複雜度 $O(n)$
### `q_merge`
與 `q_swap` 類似的兩兩合併,發現效能比依序和第一的佇列合併來得好。
:::danger
避免非必要的項目縮排 (即 `* ` 和 `- `),以清晰、明確,且流暢的漢語書寫。
:::
利用快慢指標找到中心點,將鏈結串列拆分成兩半依序合併,利用遞迴重複動作。
```c
/* Seperate queue chain */
struct list_head *qchain1 = head;
LIST_HEAD(qchain2);
list_cut_position(&qchain2, slow, head->prev);
/* Merge two list */
int size = q_merge(qchain1, descend) + q_merge(&qchain2, descend);
/* Merge qchain1 and qchain2 */
```
:::danger
HackMD 不是讓你張貼完整程式碼的地方,GitHub 才是!因此你在開發紀錄只該列出關鍵程式碼 (善用 diff 標示),可附上對應 GitHub commit 的超連結,列出程式碼是為了「檢討」和「便於他人參與討論」,不是用來「假裝自己有付出」
:::
:::danger
明確標注你參照的 GitHub 帳號名稱。
:::
參考了<s>同學</s> 程式碼,改為同時合併所有佇列,刪除了遞迴邏輯,效能比兩兩合併好。
```c
/* Compare all the queues in the same time.
* Append the maximum(or minimum) node to sorted list.
* I just post the most important part of the code.
*/
list_for_each (qi, head) {
if (!target)
target = ctx->q->next;
else {
int cmp =
strcmp(list_entry(ctx->q->next, element_t, list)->value,
list_entry(target, element_t, list)->value);
if (descend ^ (cmp < 0))
target = ctx->q->next;
}
}
list_del(target);
list_add_tail(target, &sorted_list);
```
---
## 在 qtest 提供新的命令 shuffle
鏈結串列在索引上效能不彰, Fisher–Yates shuffle 演算法在鏈結串列上會比陣列多處理上 n 倍的時間。
:::danger
很好,你的改進方案呢?
:::
```c
/* queue.c */
/* Shuffle queue 8*/
void q_shuffle(struct list_head *head)
{
if (!head || list_empty(head) || list_is_singular(head))
return;
int size = q_size(head);
struct list_head *old, *tmp;
for (int i = size; i > 1; i--) {
old = head;
int r = rand() / (RAND_MAX / (i - 1) + 1) + 1;
while (r > 0) {
old = old->next;
r--;
}
tmp = old->prev;
list_del(old);
list_move(head->prev, tmp);
list_add_tail(old, head);
}
}
```
:::danger
command 的翻譯是「命令」,而非「指令」(instruction)
:::
在 `qtest.h` 上新增 `do_shuffle` 函式,並在 `Makefile` 上新增<s>指令</s> `make` 來執行 lab0 的 Python 程式碼。
```shell!
# Makefile
shuffle: qtest scripts/shuffle.py
scripts/shuffle.py -c
```
:::warning
授課教師現在還用 2012 年出貨的 MacBook Air,後者性能更好,足以開發課程的所有程式碼,那你呢?
> 抱歉,因為第一次測試時當機,故降低了測試次數。但確實不該是電腦性能不佳的問題,我會再找找看原因。
:::
* <s>由於電腦性能不佳</s> ,僅測試了1000次。
