# 2024q1 Homework1 (lab0)
contributed by < `v0103` >
### Reviewed by `Vincent550102`
### Reviewd by `vax-r`
* 只完成部分 queue.c 之操作函式,而且也沒有進行任何改進
* github commit message 依舊過於簡短並且沒有完整表達 why v.s. how
1. `q_sort`、`q_ascend`、`q_descend`、`q_merge` 尚未實作
2. `q_reverseK` 可善用 `list_cut_position`、`list_splice_init` 等內建巨集,例如:
```c
void q_reverseK(struct list_head *head, int k)
{
// https://leetcode.com/problems/reverse-nodes-in-k-group/
if (!head || list_empty(head))
return;
struct list_head *li, *safe, *tmp = head;
LIST_HEAD(tmp_head);
int i = 0;
list_for_each_safe (li, safe, head) {
if (++i == k) {
list_cut_position(&tmp_head, tmp, li);
q_reverse(&tmp_head);
list_splice_init(&tmp_head, tmp);
tmp = safe->prev;
i = 0;
}
}
}
```
### Reviewed by `w96086123`
1. `q_insert_head` 中有提出必須檢查的條件,這時的排版應該要以編號來表示而不是直接使用換行來表達,這樣會讓人不知道這裡是在表達什麼意思。
2. 不需要把程式碼完整呈現出來。
### Reviewed by `vestata`
1. 若是在修改的地方附上 commit 連結可以加快查閱速度。
2. git 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/) 中的規範。
3. 開發紀錄中應減少使用第一人稱「我」,以便更清晰地傳達資訊。
## 開發環境
```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): 6
On-line CPU(s) list: 0-5
Vendor ID: GenuineIntel
Model name: Intel(R) Core(TM) i5-9400F CPU @ 2.90GHz
CPU family: 6
Model: 158
Thread(s) per core: 1
Core(s) per socket: 6
Socket(s): 1
Stepping: 10
CPU max MHz: 4100.0000
CPU min MHz: 800.0000
BogoMIPS: 5799.77
```
## 針對佇列操作的程式實作
:::danger
無論標題和內文中,中文和英文字元之間要有空白字元 (對排版和文字搜尋有利)
:::
### `q_new`
這裡的 `if` 條件是為了確保前面的 `malloc` 函式能夠成功配置足夠大小的記憶體給 `new` 指標。若配置失敗, `malloc` 會返回 `NULL` ,因此在這種情況下,函式會直接回傳 `NULL` 。
初始化部分原本是以手動方式實作,後來發現在`list.h`中已經有適用的函式,因此決定直接使用該函式。這樣的做法有助於提高程式碼的重用性和可讀性,<s>避免重複造輪子</s>。
:::warning
閱讀 Wikipedia: [Reinventing the wheel](https://en.wikipedia.org/wiki/Reinventing_the_wheel),思考前後文是否合理。
:::
```c
struct list_head *q_new()
{
struct list_head *new = malloc(sizeof(struct list_head));
if (!new)
return NULL;
INIT_LIST_HEAD(new);
return new;
}
```
### `q_free`
:::warning
不知道就說不知道,不要說「不太知道」,工程人員說話要精準。
:::
這題我也想使用 `list.h` 裡的函式或巨集,但是<s>不太知道</s> 怎麼使用,因此參考 [alanjian85](https://hackmd.io/@alanjian85/lab0-2023)。
因此這裡使用 `list.h` 的巨集 `list_for_each_entry_safe` ,走訪整個佇列,用 `safe` 來指向 `entry->next` ,不使用 `list_for_each_entry` 是因為執行 `q_release_element` 會將entry刪掉以致於執行 `entry->next` 會出錯,整個佇列會遺失,至於 `list_for_each_entry` 的參數list則是要看 `queue.h` 裡的 `element_t` 結構的命名。另外如果佇列是 NULL,則會在一開始就結束該函式。
```c
void q_free(struct list_head *head) {
if(!head)
return ;
element_t *entry, *safe;
list_for_each_entry_safe(entry, safe, head, list)
q_release_element(entry);
free(head);
}
```
:::danger
1. 無論標題和內文中,中文和英文字元之間要有空白字元 (對排版和文字搜尋有利)
2. 留意[資訊科技詞彙翻譯](https://hackmd.io/@sysprog/it-vocabulary)及[詞彙對照表](https://hackmd.io/@l10n-tw/glossaries)
3. 改進你的漢語表達
:::
### `q_insert_head`
:::danger
避免非必要的項目縮排 (即 `* `),以清晰、明確,且流暢的漢語書寫。
:::
這裡有個要檢查的點
輸入的 list 是否為 `NULL`
`malloc` `new` 有沒有成功
`strdup` 函式本身會呼叫 `malloc` 因此也需要檢查是否成功
都沒問題才能將該 `new` 插入到 `list` 裡。
```c
bool q_insert_head(struct list_head *head, char *s)
{
if (!head)
return false;
element_t *new = malloc(sizeof(element_t));
if (!new)
return false;
new->value = strdup(s);
if (!new->value) {
free(new);
return false;
}
list_add(&new->list, head);
return true;
}
```
### `q_insert_tail`
和上述 `q_insert_head` 相似,僅需將 `list_add` 改成 `list_add_tail` 即可完成。
### `q_remove_head`
作業說明有提到,`remove` 和 `delete` 的差別,`remove` 不會將節點抹除,因此這裡只有使用 `list_del` 來 unlink,沒有使用 `free` 來釋放該節點的記憶體。兩個要注意的點是
* 檢查鏈結串列是否為 `empty`
* 為了確保在 `strncpy` 後 `sp` 有 null character 。
```c
element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize)
{
if (!head || list_empty(head))
return NULL;
element_t *tmp = list_first_entry(head, element_t, list);
list_del(head->next);
if (sp) {
strncpy(sp, tmp->value, bufsize);
sp[bufsize - 1] = '\0';
}
return tmp;
}
```
### `q_remove_tail`
和上述 `q_remove_head` 相似,僅需將 `list_first_entry` 改成 `list_last_entry` 即可完成。
### `q_delete_mid`
這裡我使用快慢指標,`fast` 每次走 2 步,`slow` 每次走 1 步,當 `fast` 走訪完整個 list,`slow` 則會在鏈結串列的中間。
我嘗試幾次後發現有兩個要注意的點
* `fast` 和 `slow` 一開始要從 `head->next` 出發才是正確的
* `fast` 指到 `head` 或 `head->prev` 都算是走訪完 list
找完中點後,由於 delete 是要將該節點記憶體釋放,所以除了用 `list_del` unlink 要再使用 `q_release_element` 釋放整個 element 的記憶體。
:::warning
改進漢語表達。
:::
```c
bool q_delete_mid(struct list_head *head)
{
if (!head || list_empty(head))
return false;
struct list_head *fast = head->next;
struct list_head *slow = head->next;
for (; fast != head && fast != head->prev;
fast = fast->next->next, slow = slow->next) {
}
list_del(slow);
q_release_element(list_entry(slow, element_t, list));
return true;
}
```
:::warning
針對環狀雙向佇列,提出更快的方法。
:::
`TO DO : 針對環狀雙向佇列可以使用兩個指標,一個往next,一個往prev`
### `q_delete_dup`
這段程式碼的目標是移除重複項目。程式使用 `list_for_each_entry_safe` 來走訪整個 `list` ,並使用 `strcmp` 函數比較項目的值。如果發現重複的項目,它將刪除所有重複的項目,但保留最後一個。為了將最後一項也刪掉,我使用 `dup_last` 變數,來確保最後一個重複的項目會被刪除。
```c
bool q_delete_dup(struct list_head *head)
{
if (!head || list_empty(head))
return false;
bool dup_last = false;
element_t *entry, *safe;
list_for_each_entry_safe (entry, safe, head, list)
if (&safe->list != head && !strcmp(entry->value, safe->value)) {
list_del(&entry->list);
q_release_element(entry);
dup_last = true;
} else if (dup_last) { // del dup last one
list_del(&entry->list);
q_release_element(entry);
dup_last = false;
}
return true;
}
```
### `q_swap`
由於 swap 只需要改變每個 element 的鏈結串列,~~不需要值~~ 不需要改變節點當中的數值,所以我這裡使用~~的是~~ `list_for_each_safe`,再使用 `list_del` 和 `list_add` 就可以將兩者 swap (下方解釋),最後 `safe = node->next` 可以確保都是兩個為一組 swap。
```c
void q_swap(struct list_head *head)
{
if (!head || list_empty(head))
return;
struct list_head *node, *safe;
list_for_each_safe (node, safe, head)
if (safe != head) {
list_del(node);
list_add(node, safe);
safe = node->next;
}
return;
```
後來看到 `list_move` 這個函式,剛好就是 `list_del` 和 `list_add` 的組合,在 `list.h` 裡可以看到他的敘述是 `Move a list node to the beginning of the list` 不過將輸入更改後便可以達到我要的功能 `將節點 node 移至節點 safe 後`。
```diff
list_for_each_safe (node, safe, head)
if (safe != head) {
- list_del(node);
- list_add(node, safe);
+ list_move(node, safe);
safe = node->next;
}
```
### `q_reverse`
reverse 跟 swap 一樣都只需要更改鏈結串列的指向,我一樣使用 `list_for_each_safe` 走訪每個節點,並將他們都`Move a list node to the beginning of the list`,這次是使用他真正的功能了,如此一來整個鏈結串列就被 reverse 了。
```c
void q_reverse(struct list_head *head)
{
if (!head || list_empty(head))
return;
struct list_head *node, *safe;
list_for_each_safe (node, safe, head)
list_move(node, head);
}
```
### `q_reverseK`
我這裡使用最土炮的方法,在 `q_reverse` 的基礎上加上兩個計數器,`count_turn` 用來確認後面是否還有完整的 k 組 element,`count_k` 則是用來數已被 reverse 的節點數,若達到 k 個則將重新計數,並改變後面節點要插入的位置。
```c
void q_reverseK(struct list_head *head, int k)
{
if (!head || list_empty(head))
return;
struct list_head *node, *safe, *start = head;
int count_k = 0, count_turn = 0;
int turn = q_size(head) / k;
list_for_each_safe (node, safe, head) {
list_move(node, start);
if (count_turn == turn) /*no complete k-group*/
return;
if (++count_k == k) { /*change start per kth node*/
start = safe->prev;
count_turn++;
count_k = 0;
}
}
}
```
:::warning
> [name=I-HSIN Cheng]
> 這裡是開發筆記不是教學頁面,不需要闡述每個函式的邏輯與做法,程式碼本身應該清楚到不需文字解釋即可理解,除非有任何特別之處,應該寫出可改進之處,另外說明文字的贅字太多且解說不清晰。
:::