# 2023q1 Homework1 (lab0)
contributed by < `ericlai1021` >
## 開發環境
```shell
$ gcc --version
gcc (Ubuntu 11.2.0-19ubuntu1) 11.2.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) i7-8550U CPU @ 1.80GHz
CPU family: 6
Model: 142
Thread(s) per core: 2
Core(s) per socket: 4
Socket(s): 1
Stepping: 10
BogoMIPS: 3983.99
```
## 開發過程
### q_new
:::danger
注意書寫規範:中英文間用一個半形空白字元區隔。
:notes: jserv
> 了解,已修正
:::
使用 `malloc` 動態配置一個自定義結構 `list_head` 大小的記憶體空間,若成功配置記憶體空間,則使用 `INIT_LIST_HEAD` 來初始化,否則,直接回傳 `NULL`
```c
struct list_head *q_new()
{
struct list_head *head = malloc(sizeof(*head));
if (head)
INIT_LIST_HEAD(head);
return head;
}
```
### q_free
想法是走訪鏈結串列的所有節點,並逐一刪除該節點,所以選擇使用 `list_for_each_entry_safe` 巨集來實作之,作法如下:
1. 若此鏈結串列為空或NULL,則直接釋放該空間
2. 否則,透過 `list_for_each_entry_safe` 來逐一走訪每個節點
3. 先將當前節點移出鏈結串列 `l` ,再將該節點釋放空間
4. 最後,釋放鏈結串列 `l` 開頭的節點
```c
void q_free(struct list_head *l)
{
if (!l || list_empty(l)) {
free(l);
return;
}
element_t *entry, *safe;
list_for_each_entry_safe (entry, safe, l, list) {
list_del(&entry->list);
q_release_element(entry);
}
free(l);
return;
}
```
### q_insert_head
思考過程如下:
1. 因為要插入一個節點,因此需要先配置一個 `element_t` 大小的空間
2. 若配置失敗,直接回傳 `false`
3. 計算新節點的 `value` 所需要的字串長度,注意這邊要將字串結尾符號也納入計算
4. 為新節點的 `value` 配置空間,若配置失敗,則將該節點的空間釋放,並直接回傳 `false`
5. 透過 `strncpy` 函式來將參數字串 `s` 複製到新節點的 `value`
6. 最後將新節點加入到鏈節串列 `head` 當中,並回傳 `true`
```c
bool q_insert_head(struct list_head *head, char *s)
{
if (!head)
return false;
element_t *node = malloc(sizeof(*node));
if (!node)
return false;
size_t len = strlen(s) + 1;
node->value = malloc(sizeof(char) * len);
if (!node->value) {
free(node);
return false;
}
strncpy(node->value, s, len);
list_add(&node->list, head);
return true;
}
```
### q_insert_tail
作法同 `q_insert_head` 唯一的差別在把 `list_add` 改成 `list_add_tail`
### q_remove_head
這部分參考同學 <[`YSRossi`](https://hackmd.io/@YSRossi/lab0-2023#q_remove_head)> 的共筆,並與同學指教解題過程。
思考過程如下:
1. 首先處理邊界條件
2. 取得鏈結串列 `head` 的第一個節點
3. 將該節點移除鏈結串列 `head`
4. 將移走的節點的 `value` 複製到 `sp` 中
```c
element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize)
{
if (!head || list_empty(head))
return NULL;
element_t *node = list_first_entry(head, element_t, list);
list_del_init(&node->list);
if (sp) {
strncpy(sp, node->value, bufsize - 1);
sp[bufsize - 1] = '\0';
}
return node;
}
```
### q_remove_tail
作法同 `q_remove_head` 唯一的差別在把 `list_first_entry` 改成 `list_last_entry`
### q_size
根據課程作業說明 [`lab0`](https://hackmd.io/@sysprog/linux2023-lab0/%2F%40sysprog%2Flinux2023-lab0-a) 的範例程式,透過 `list_for_each` 巨集來走訪鏈結串列,並用一個 `int` 變數來計算節點個數
```c
int q_size(struct list_head *head)
{
if (!head || list_empty(head))
return 0;
int size = 0;
struct list_head *pos;
list_for_each (pos, head)
size++;
return size;
}
```
### q_delete_mid
思考過程如下:
1. 首先處理邊界條件
2. 宣告兩個 `list_head` 型態指標變數 `forward` 與 `backward` 分別表示往順向與逆向走
3. 當 `forward == backward` 或 `forward` 與 `backward` 相鄰時,表示已經走到中間節點
4. 將中間節點刪除,並回傳 `true`
```c
bool q_delete_mid(struct list_head *head)
{
// https://leetcode.com/problems/delete-the-middle-node-of-a-linked-list/
if (!head || list_empty(head))
return false;
struct list_head *forward = head->next, *backward = head->prev;
while (forward != backward && forward->next != backward) {
forward = forward->next;
backward = backward->prev;
}
list_del_init(forward);
element_t *entry = list_entry(forward, element_t, list);
q_release_element(entry);
return true;
}
```
### q_delete_dup
因為輸入是已排序好的鏈結串列,所以直觀的想法就是從頭走訪整條鏈結串列的所有節點,只要鄰近兩點的 `value` 一樣,表示要將所有持有該 `value` 的節點都刪除,具體作法如下:
1. 首先處理邊界條件,這邊要注意的是鏈結串列的節點必須至少兩個,否則沒有實作的意義
2. 利用 `list_for_each_entry_safe` 巨集在走訪整條鏈結串列時就算刪除當前節點,即 `entry` ,也能夠過 `safe` 繼續走訪
3. 加入一變數 `del` 用來標記需要被移除的節點
4. 比較當前節點 `entry` 與下一個節點 `safe` 的 `value` 是否相等
5. 若相等,則將 `del` 標記為下一個節點 `safe` ,並移除當前節點 `entry`
6. 若不相等且 `del` 不為空,則將 `del` 移除,並將 `del` 設為空,避免重複釋放記憶體
```c
bool q_delete_dup(struct list_head *head)
{
// https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/
if (!head || list_empty(head) || list_is_singular(head))
return false;
element_t *entry, *safe;
element_t *del = NULL;
list_for_each_entry_safe (entry, safe, head, list) {
if ((&safe->list != head) && !strcmp(entry->value, safe->value)) {
del = safe;
list_del(&entry->list);
q_release_element(entry);
} else if (del) {
list_del(&del->list);
q_release_element(del);
del = NULL;
}
}
return true;
}
```
### q_swap
這部分由於後面會實作 `q_reverseK` 函式,而 `q_swap` 函式就是 `q_reverseK` 的特例,即 K 設為 2 ,這麼做不但可以使程式更精簡,也使程式更易於維護
```c
void q_swap(struct list_head *head)
{
// https://leetcode.com/problems/swap-nodes-in-pairs/
q_reverseK(head, 2);
}
```
### q_reverse
最直觀的作法就是透過 `list_for_each_safe` 巨集走訪過整條鏈結串列的節點,將當前節點的 `next` 指標指向當前節點的 'prev' 指標,再將當前節點的 `prev` 指標指向下一個節點 `safe` ,最後別忘了將 `head->next` 指向 `head->prev` ,並將 `head->prev` 指向鏈結串列的第一個節點,即 `safe` 指標所指的地方
```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) {
node->next = node->prev;
node->prev = safe;
}
head->next = head->prev;
head->prev = safe;
}
```
### q_reverseK
這題最直觀的想法就是利用 `list_cut_position` 函式將鏈結串列每K個切出來,接著利用先前實作過的 `q_reverse` 函式將切分出來的鏈結串列做反轉,最後再透過 `list_splice` 函式將切分出來的鏈結串列接回原本的鏈結串列當中。
實際研讀過 <[`list.h`](https://github.com/ericlai1021/lab0-c/blob/master/list.h)> 當中 `list_cut_position` 與 `list_splice` 兩個函式的程式碼後得出以下流程:
1. 首先處理邊界條件,這裡只要鏈結串列不為空或 `NULL` 演算法即可成立
2. 創造一個新的鏈結串列 `new_head`,用來串接被切分出來的鏈結串列
3. 用 `list_for_each_safe` 巨集走訪整條鏈結串列,搭配一個 `cnt` 判斷是否走過 K 個節點
4. 當 `cnt == k` 用 `list_cut_position` 函式切分出一條具有 K 個節點的鏈節串列,並用 `new_head` 去串接
6. 使用先前實作過的 `q_reverse` 函式將 `new_head` 做反轉
7. 接著要將反轉過後的鏈結串列 `new_head` 接回去原本的鏈結串列 `head` 當中,這邊為了搭配 `list_cut_position` 函式當中要求第一個引數的鏈結串列必須為空,所以使用 `list_splice_init` 函式將兩條鏈結串列串接起來之後,將 `new_head` 設為空
8. 更新要被切分出來的起始指標 `cut`
要注意這邊 `cut` 是更新為 `safe->prev` 而不是當前節點 `node` ,因為使用 `list_cut_position` 函式之後 `node` 跟 `safe` 就不相連了
```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;
LIST_HEAD(new_head);
struct list_head *node, *safe, *cut = head;
int cnt = 0;
list_for_each_safe (node, safe, head) {
if (++cnt == k) {
list_cut_position(&new_head, cut, node);
q_reverse(&new_head);
list_splice_init(&new_head, cut);
cut = safe->prev;
cnt = 0;
}
}
}
```
### q_sort
初步嘗試參考 [Linked List Sort](https://npes87184.github.io/2015-09-12-linkedListSort/) 實作,選擇當中為 stable sort 的 Merge Sort 來實作之。
首先, [Linked List Sort](https://npes87184.github.io/2015-09-12-linkedListSort/) 實作當中使用的是單向鏈結串列,所以我們也需要對原先的雙向環狀鏈結串列做調整,這裡只是簡易的破壞單向的環狀結構即可達到演算法的目的,作法就是將串列的最後一個節點的 `next` 指向 NULL
```c
head->prev->next = NULL;
```
接著傳入串列的第一個節點的指標給 `mergeSortList` 函式做排序,排序完再接回串列的 head
```c
head->next = mergeSortList(head->next);
```
實作 Merge Sort 當中會包含兩個函式 `mergeSortList` 跟 `merge` , `mergeSortList` 會傳入未排序過的鏈結串列的開頭指標,並回傳已排序好的鏈結串列的開頭指標。由於 Merge Sort 為 [`divide-and-conquer`](https://en.wikipedia.org/wiki/Divide-and-conquer_algorithm) 演算法,在 divide 階段會不斷將串列切分成兩條大小一樣的子串列,直到不能再切分為止 (即串列當中只剩下一個元素),這邊搭配快慢指標來找到串列的中間元素,接著將串列切分成兩條子串列
```c
struct list_head *fast = head->next;
struct list_head *slow = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
fast = slow->next;
slow->next = NULL;
```
### q_descend
此題要求刪除一個節點左邊所有小於該節點的 `value` 的節點,換個角度去思考,也就是說,只要從鏈結串列的開頭往前 (即 `prev` 指標) 去走訪,只要遇到一個節點的 `value` 小於上一個走訪節點的 `value` ,就將該節點刪除。
實際操作如下:
1. 首先處理邊界條件,這裡只要 `head` 不為空或 `NULL` 演算法即可成立
2. 用一個變數 `pos` 初始化指向 `head->prev` ,即鏈結串列最後一個節點
3. 宣告一個字串變數 `maxStr` 初始化為空字串
4. 往 `prev` 指標的方向持續走訪鏈結串列
5. 若當前節點的 `value` 小於 `maxStr` ,就刪除該節點
6. 否則,將 `maxStr` 指向當前節點的 `value`
7. 走訪完所有節點就回傳鏈結串列的 size
```c
int q_descend(struct list_head *head)
{
// https://leetcode.com/problems/remove-nodes-from-linked-list/
if (!head || list_empty(head))
return 0;
struct list_head *pos = head->prev;
char *maxStr = "";
while (pos != head) {
element_t *entry = list_entry(pos, element_t, list);
pos = pos->prev;
if (strcmp(entry->value, maxStr) < 0) {
list_del(&entry->list);
q_release_element(entry);
} else {
maxStr = entry->value;
}
}
return q_size(head);
}
```
### q_merge