# 2023q1 Homework1 (lab0)
contributed by < `yutingshih` >
## 開發環境
> 目前尚未在實體機器執行 GNU/Linux,因此暫時在 Apple M1 Mac 上使用 Lima VM,參考 [Lima VM with a foreign architecture (slow mode)](https://github.com/lima-vm/lima/blob/master/docs/multi-arch.md)
```shell
$ uname -a
Linux lima-linux2023 5.15.0-67-generic #74-Ubuntu SMP Wed Feb 22 14:14:39 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux
$ gcc --version
gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.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: 40 bits physical, 48 bits virtual
Byte Order: Little Endian
CPU(s): 4
On-line CPU(s) list: 0-3
Vendor ID: AuthenticAMD
Model name: QEMU Virtual CPU version 2.5+
CPU family: 15
Model: 107
Thread(s) per core: 1
Core(s) per socket: 4
Socket(s): 1
Stepping: 1
BogoMIPS: 1999.99
Flags: fpu de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx lm rep_
good nopl cpuid extd_apicid pni cx16 hypervisor lahf_lm cmp_legacy svm 3dnowprefetch vmmcall
Virtualization features:
Virtualization: AMD-V
Caches (sum of all):
L1d: 256 KiB (4 instances)
L1i: 256 KiB (4 instances)
L2: 2 MiB (4 instances)
L3: 64 MiB (4 instances)
NUMA:
NUMA node(s): 1
NUMA node0 CPU(s): 0-3
```
## 開發過程
### q_new
使用 `malloc` 函式配置記憶體給新建立的 `struct list_head`,並用 Linux kernel API 提供的 `INIT_LIST_HEAD` 將 `head` 初始化 (把 `prev` 和 `next` 設為自身) 後回傳。
```c
struct list_head *q_new()
{
struct list_head *head = malloc(sizeof(struct list_head));
if (!head)
return NULL;
INIT_LIST_HEAD(head);
return head;
}
```
### q_free
在實作 `q_free` 函式時原本想用上課教過的 `list_for_each` 巨集來走訪 linked list 的每個節點並釋放,但仔細看 `list_for_each` 的實作和註解描述,卻發現沒辦法使用 Linux kernel 風格的間接指標的方式來處理節點的刪除
```c
/**
* list_for_each - Iterate over list nodes
* @node: list_head pointer used as iterator
* @head: pointer to the head of the list
*
* The nodes and the head of the list must be kept unmodified while
* iterating through it. Any modifications to the the list will cause undefined
* behavior.
*/
#define list_for_each(node, head) \
for (node = (head)->next; node != (head); node = node->next)
```
所幸在同一份標頭檔的下方不遠處有提供了另一個巨集 `list_for_each_safe`,可以用兩個 `list_head` 指標來進行走訪,一個用來指向目前要刪除的節點,另一個指向目前還「安全」的節點,正好符合我們的需求
```c
/**
* list_for_each_safe - Iterate over list nodes and allow deletions
* @node: list_head pointer used as iterator
* @safe: list_head pointer used to store info for next entry in list
* @head: pointer to the head of the list
*
* The current node (iterator) is allowed to be removed from the list. Any
* other modifications to the the list will cause undefined behavior.
*/
#define list_for_each_safe(node, safe, head) \
for (node = (head)->next, safe = node->next; node != (head); \
node = safe, safe = node->next)
```
因此我的 `q_free` 實作如下:利用 `list_for_each_safe` 巨集從 `head` 開始循序走訪每個 list 節點,再用 `list_entry` 巨集取得包含 `list_head` 的資料結構的起始位址,再依序釋放 `element_t` 結構體中手動配置的記憶體 (先釋放 `value` 字串再釋放 `elem` 物件本身),為了避免釋放後的記憶體被存取所帶來的安全性議題,當記憶體空間被釋放後就馬上將指向該空間的指標歸零。
由於`list_for_each_safe` 是從標頭節點 `head` 的下個節點開始走訪,因此走訪結束後,還要記得釋放 `head` 指向的記憶體,並將 `head` 指標歸零。
```c
void q_free(struct list_head *head) {
if (!head)
return;
struct list_head *node, *safe;
list_for_each_safe(node, safe, head) {
element_t *elem = list_entry(node, element_t, list);
free(elem->value), elem->value = NULL;
free(elem), elem = NULL;
}
free(head), head = NULL;
}
```
### __e_new
`element_t` 的建構函式,用來讓 `q_insert_head` 和 `q_insert_tail` 呼叫以產生新的節點
```c
static element_t *__e_new(char *s)
{
element_t *elem = malloc(sizeof(element_t));
if (!elem)
return NULL;
size_t len = strlen(s);
elem->value = malloc((len + 1) * sizeof(char));
if (!elem->value) {
free(elem), elem = NULL;
return NULL;
}
strncpy(elem->value, s, len + 1);
return elem;
}
```
### q_insert_head
呼叫建構函式 `__e_new` 來創建新的 `element_t` 節點,並使用 Linux API 中的 `list_add`,來將新節點加入 linked list 的最前面 (`head` 節點的後面),並且在過程中檢查 `head` 是否為空或是新增節點時是否配置記憶體失敗。
```c
bool q_insert_head(struct list_head *head, char *s)
{
if (!head)
return false;
element_t *elem = __e_new(s);
if (!elem)
return false;
list_add(&elem->list, head);
return true;
}
```
### q_insert_tail
呼叫建構函式 `__e_new` 來創建新的 `element_t` 節點,並使用 Linux API 中的 `list_add_tail`,來將新節點加入 linked list 的最後面 (`head` 節點的前面),並且在過程中檢查 `head` 是否為空或是新增節點時是否配置記憶體失敗。
```c
bool q_insert_tail(struct list_head *head, char *s)
{
if (!head)
return false;
element_t *elem = __e_new(s);
if (!elem)
return false;
list_add_tail(&elem->list, head);
return true;
}
```
### q_remove_head
remove 操作必須要在串列至少有一個元素節點時才能進行,因此先確認標頭節點 `head` 存在且至少有一個元素節點,根據 `queue.h` 的描述,除了將節點自 linked list 移出之外,還需要將移除的節點回傳並將其儲存的字串複製到指定區域,需要注意的是,C 語言的字串是以零結尾的 (null-terminated),因此要記得在字串 `sp` 的最後補零,另外,由於移除的節點需要被回傳,為了避免非預期的記憶體存取,這裡使用 `list_del_init` 而非 `list_del` 來移除節點。
```c
element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize)
{
if (!head || list_empty(head))
return NULL;
element_t *elem = list_entry(head->next, element_t, list);
list_del_init(head->next);
if (sp && bufsize) {
strncpy(sp, elem->value, bufsize - 1);
sp[bufsize - 1] = '\0';
}
return elem;
}
```
### q_remove_tail
與 `q_remove_head` 類似,只是差別在於 `q_remove_tail` 移除的節點位於 linked list 的尾端,也就是 `head` 的前一個節點。
```c
element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize)
{
if (!head || list_empty(head))
return NULL;
element_t *elem = list_entry(head->prev, element_t, list);
list_del_init(head->prev);
if (sp && bufsize) {
strncpy(sp, elem->value, bufsize - 1);
sp[bufsize - 1] = '\0';
}
return elem;
}
```
### q_size
`q_size` 計算 linked list 中有幾個元素 (`struct list_head`),若 `head` 為 `NULL` 代表 linked list 為空,則回傳 `0`,否則走訪整個 linked list,走訪過程中利用計數器 `len` 計算經過幾個節點,最後回傳 `len`。
```c
int q_size(struct list_head *head)
{
if (!head)
return 0;
int len = 0;
struct list_head *li;
list_for_each (li, head)
len++;
return len;
}
```
### q_delete_mid
利用快慢指標的技巧來找到位於 linked list 中間的元素,一開始兩個指標在同樣的起點,在每一輪的疊代中 `fast` 指標往前走兩步,`slow` 指標往前一步,代表 `slow` 一定在 `fast` 和 `head` 的中點,當 `fast` 走訪完整個 linked list,則 `slow` 剛好走到一半,剛好就是我們想要移除的節點,接著使用 `list_del_init` 移除 `slow` 節點,並將指向的記憶體空間釋放。
```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 *fast, *slow;
for (fast = slow = head->next; fast != head && fast->next != head;
fast = fast->next->next, slow = slow->next)
;
list_del_init(slow);
element_t *elem = list_entry(slow, element_t, list);
q_release_element(elem), elem = NULL;
return true;
}
```
### q_delete_dup
根據 `queue.h` 的描述:
```c=146
/**
* q_delete_dup() - Delete all nodes that have duplicate string,
* leaving only distinct strings from the original queue.
* @head: header of queue
*
* Reference:
* https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/
*
* Return: true for success, false if list is NULL.
*/
```
一開始誤解了 `q_delete_dup` 函式的功能,以為它是要刪除所有具有重複字串的節點。也就是說,若有兩個節點存有相同字串,就刪除其中一個,留下另一個。因此,在解 [LeetCode 81](https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii) 時,我寫下了這樣的程式碼。
```c
struct ListNode* deleteDuplicates(struct ListNode* head){
if (!head) return NULL;
for (struct ListNode** pp = &head; *pp && (*pp)->next; pp = &(*pp)->next) {
if ((*pp)->val == (*pp)->next->val) {
*pp = (*pp)->next; // remove node
}
}
return head;
}
```
後來才發現我誤解了題目,其實正確的作法應是若有兩個節點帶有相同資料,就都要刪除。為避免其他人誤解函式功能,或許未來可以改進一下程式碼的註解。
而正確的實作如下:
```c
struct ListNode* deleteDuplicates(struct ListNode* head){
if (!head || !head->next) return head;
struct ListNode** pp = &head;
while (*pp && (*pp)->next) {
if ((*pp)->val != (*pp)->next->val) {
pp = &(*pp)->next; // move one step
continue;
}
while ((*pp)->next && (*pp)->val == (*pp)->next->val) {
*pp = (*pp)->next; // remove mode
}
*pp = (*pp)->next; // remove mode
}
return head;
}
```
這個實作可以順利通過 LeetCode 的所有測試,接著讓我們繼續將其修改為 Linux 雙向鏈結串列的版本,並好好處理記憶體的釋放
```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 head;
struct list_head **pp = &head->next;
while ((*pp)->next != head) {
element_t *curr = list_entry(*pp, element_t, list);
element_t *next = list_entry((*pp)->next, element_t, list);
if (STREQ(curr->value, next->value)) {
pp = &(*pp)->next;
continue;
}
while ((*pp)->next && STREQ(curr->value, next->value)) {
element_t *tmp = list_entry(*pp, element_t, list);
list_del(*pp);
free(tmp->value), tmp->value = NULL;
free(tmp), tmp = NULL;
}
element_t *tmp = list_entry(*pp, element_t, list);
list_del(*pp);
free(tmp->value), tmp->value = NULL;
free(tmp), tmp = NULL;
}
return true;
}
```
其中為了方便,新宣告一個巨集做字串的比對
```c
#define STREQ(left, right) (!strcmp((left), (right)))
```
<s>
</s>
### q_swap
把串列中相鄰兩個節點交換順序可以拆成兩個步驟:
1. 把前面的節點自串列移除
2. 把移除的節點插入到後面節點的後面
因此 `q_swap` 把串列上相鄰節點兩兩一組做交換,可以透過走訪迴圈的同時重複上述兩個步驟來完成,實作如下:
```c
void q_swap(struct list_head *head)
{
// https://leetcode.com/problems/swap-nodes-in-pairs/
if (!head || list_empty(head) || list_is_singular(head))
return;
struct list_head *node;
list_for_each(node, head) {
if (node->next == head)
break;
list_move(node, node->next);
}
}
```
調整一下可以讓我們的程式碼更加精簡
```c
void q_swap(struct list_head *head)
{
// https://leetcode.com/problems/swap-nodes-in-pairs/
if (!head || list_empty(head) || list_is_singular(head))
return;
struct list_head *node;
list_for_each(node, head->next)
list_move_tail(node, node->prev);
}
```
### q_reverse
基於剛才對 `list_move` 和 `list_move_tail` 的理解,可以在走訪串列期間持續把節點加入 `head` 的後面,來達成 `q_reverse` 的效果
```c
void q_reverse(struct list_head *head)
{
if (!head || list_empty(head) || list_is_singular(head))
return;
struct list_head *node;
list_for_each(node, head)
list_move(node, head);
}
```
### q_reverseK
每經過 k 個節點,就重新設定一次 `group_head`,作為接下來要插入節點的基準
```c
void q_reverseK(struct list_head *head, int k)
{
// https://leetcode.com/problems/reverse-nodes-in-k-group/
if (!head || list_empty(head) || list_is_singular(head))
return;
struct list_head *node, *group_head;
int i = 0;
list_for_each(node, head) {
if (i == 0)
group_head = node->prev;
list_move(node, group_head);
i = (i + 1) % k;
}
}
```
<s>
</s>
:::danger
不要急著張貼程式碼,你應該闡述你的想法、中間的嘗試,和研讀過的相關材料,「開發紀錄」著重過程。
:notes: jserv
:::
### q_sort
```c
```
### q_descend
```c
```
### q_merge
```c
```