# 2024q1 Homework1 (lab0)
contributed by < `164253` >
### Reviewed by `96121503`
* q_insert_head 中提到,沒有檢查 `node` 及 `val` 是否為空和有檢查的情況,可以補上兩種情況計算速度的比較結果。
> 後來發現不檢查會造成錯誤的記憶體釋放,因此不用測試這種錯誤作法
## 開發環境
:::danger
無論標題和內文中,中文和英文字元之間要有空白字元 (對排版和文字搜尋有利)
:::
空間滿了所以沒裝雙系統 所以我是 windows10 的 WSL2
```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): 8
On-line CPU(s) list: 0-7
Vendor ID: GenuineIntel
Model name: Intel(R) Core(TM) i5-10210U CPU @ 1.60GHz
CPU family: 6
Model: 142
Thread(s) per core: 2
Core(s) per socket: 4
Socket(s): 1
Stepping: 12
BogoMIPS: 4224.00
```
## 實作指定佇列操作
目前本機 100 分, `git push` 後僅有 95 分,無法通過第 17 筆的常數複雜度測試
### q_new
配置一個 `list_head` ,如果沒有空間要繼續嘗試直到成功
接著用 `INIT_LIST_HEAD` 初始化並回傳 `list_head`
### q_free
用 `list_for_each_entry_safe` 走訪所有節點,由於要刪除遇到的節點,所以用 `safe` 版
將途經節點的字串及節點本身釋放掉 最後釋放 `head`
### q_insert_head
配置一個 `element_t` 並用 `strdup` 處理字串配置
原本跳過 `node` 和 `val` 記憶體配置失敗的情況,但會造成錯誤的釋放記憶體
```diff
bool q_insert_head(struct list_head *head, char *s)
{
element_t *node = malloc(sizeof(element_t));
char *val = strdup(s);
- if (!node || !val || !head) {
- free(node);
- free(val);
- return false;
- }
- node->value = val;
- list_add(&node->list, head);
- return true;
+ bool flag = false;
+ if (head && node && val) {
+ node->value = val;
+ list_add(&node->list, head);
+ flag = true;
+ } else {
+ if (node)
+ free(node);
+ if (val)
+ free(val);
+ }
+ return flag;
}
```
若 `node, val, head` 中記憶體有問題,釋放掉已經配置的記憶體
> 此處沒有檢查 `node, val` 是否為 NULL ,雖然不影響函式結果,但之後需要實驗補上速度差異
> > 不檢查會造成錯誤的記憶體釋放,因此無須討論這種狀況
否則將 `val` 字串給 `node` ,並用 `list_add` 將 `node` 插入至 `head`
:::danger
改進你的漢語表達
:::
### q_insert_tail
原本我複製 `q_insert_head` 的內容並改掉 `list_add` ,但有許多重複程式碼
因此改為呼叫 `q_insert_head(head->prev, s)` 達成插入在最後一項的功能
```diff
bool q_insert_tail(struct list_head *head, char *s)
{
- element_t *node = malloc(sizeof(element_t));
- char *val = strdup(s);
- if (!node || !val || !head) {
- free(node);
- free(val);
- return false;
- }
- node->value = val;
- list_add_tail(&node->list, head);
- return true;
+ return q_insert_head(head->prev, s);
}
```
### q_remove_head
若 `list` 指向 NULL 或者是空的,終止並回傳 NULL
否則用 `list_first_entry` 找到第一個元素,並用 `list_del` 移除他
將他的 `value` 存至 `sp`
### q_remove_tail
和 `q_insert_tail` 一樣 可以簡單的用 `q_remove_head` 完成
但由於 `q_remove_head` 是刪除 `head` 的下一個元素 應該傳入 `head->prev->prev` 才對
```diff
element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize)
{
- return q_remove_head(head->prev, sp, bufsize);
+ return list_empty(head) ? NULL
+ : q_remove_head(head->prev->prev, sp, bufsize);
}
```
### q_size
特別處理 `head` 指向 NULL 的情況
其餘用 `list_for_each` 走訪過所有節點,並記錄共有幾個元素即可
### q_delete_mid
原本是用快慢指標的技巧找到中間那項並刪除
:::warning
針對環狀且雙向的佇列,是否有更快的方法?
:::
考慮雙向環狀的條件可以發現,用兩個指標從頭和尾向對方移動,
只需要存取 $n$ 個節點就好,不需要快慢指標的 $\frac32n$個
```diff
bool q_delete_mid(struct list_head *head)
{
if (!head || list_empty(head))
return false;
- struct list_head *slow = head, *fast = head;
- while (fast->next != head->prev && fast->next->next != head->prev) {
- slow = slow->next;
- fast = fast->next->next;
- }
- list_del(slow);
- q_release_element(list_entry(slow, element_t, list));
+ struct list_head *tail;
+ q_find_mid(head, head, tail);
+ list_del(head);
+ q_release_element(list_entry(head, element_t, list));
return true;
}
```
由於 `q_sort` 採用 merge sort ,也需要尋找中間那項,獨立成函式 `q_find_mid`
### q_delete_dup
走訪除了第一個以外的節點
檢查上一個節點和現在的是否相同,重複就刪掉上一個,並記錄這個節點也是重複的
若上一個和現在不同但被記錄,也要將上一個刪除
由於和 `q_ascend` 與 `q_descend` 有許多重疊,使用另一個子函式一併實現
### q_swap
先將最後一項的 `next` 設為 NULL ,每遇到兩個節點就把他們的連結互換
由於 `head` 被留下來最後接回去,至少有一個 `next` 指針可以拿來更改
每修改一個指針,就會多出一個指向重複節點的指針,因此自由度始終是一,可以執行到底
最初實作時沒有注意到 `head` 和他後面那項會被交換,要存下來的是 `head->next`
後來直接用 `q_reverse(head, 2)` 取代
```c
void q_swap(struct list_head *head)
{
q_reverse(head, 2);
}
```
### q_reverse
走訪所有節點並交換 `prev, next`
用 `do while` 一直執行到遇到 `head`
### q_reverseK
可以重用 `q_reverse` 來實作,需要注意必須傳入環狀佇列,要將第 K 個跟第一個連起來
:::warning
TODO: 改進程式碼的重用程度。
:::
### q_ascend, q_descend
要求回傳一個非嚴格遞增或遞減的佇列,一種想法是用 monostack,但均攤需要 $O(2n)$
可以反過來做,如果下一個節點會破壞性質,就把他刪掉
為了重用程式碼 `q_descend` 可以選擇反轉後用 `q_ascend` 再反轉,並改成刪除左邊的節點
但需要 $O(3n)$
由於和 `qdelete_dup` 有許多重疊,使用另一個子函式一併實現,改進原本重用率低的問題
### q_merge
考量到要在 `q_sort` 中重用,我將 `q_merge_two` 獨立出來,整個 list 每兩項合併一次
若還有兩項以上重複進行,且會一直合併至只剩一個 list
但因為需要保留 `queue_contex_t` 的結構,結束函式後程式會自己回收
所以實際上不能把相鄰項直接移除,採用清空被合併的 list ,每次要合併時尋找非空的 list
許多同學實作上把所有 list 合併到第一個,假設總共 $n$ 個節點 $q$ 個 list
第一個 list 中有 $n-q+1$ 個節點,總共會需要 $q-1$ 次合併
比較則要 $\frac{(q-1)((n-q+1)+(n-1))}2=\frac{-q^2+(2n+2)q-(n+1)}2$
走訪 $n$ 個節點
比較次數最多發生在 $q=n+1+\sqrt{n(n+1)}$ ,但 $q$ 最多只能到 $n-1$
所以實際上最多比較次數會是 $\frac{(n-2)(n+1)}2$
而我的作法要 $q-1$ 次合併,走訪 $n^2$ 個節點,比較 $n\log_2n$ ,證明見 [2024-3-19 課程簡記](https://hackmd.io/3e8LI8_0SLOo_UTDAnPKYQ?both#%E5%95%8F%E9%A1%8C%E4%B8%80-merge-sort-%E6%9C%80%E4%BD%B3%E6%9C%80%E5%B7%AE%E7%8B%80%E6%B3%81)
走訪節點的成本是固定的,但比較取決於資料型別,若是字串成本會很高
### q_sort
採用 merge sort ,將整個 list 切割開,並呼叫 `q_merge_two` 當作 n 個長度一的 list 合併
現在用 `q_delete_mid` 中尋找中點的做法,亦可用 `q_reverseK` , K 設為 `q_size()+1>>1`