# 2024q1 Homework1 (lab0)
contributed by < `96121503` >
### Reviewed by `allenliao666`
- 開發紀錄中有多處句尾沒有句號,應統一使用句號
- `q_insert_tail` 段落描述程式碼版本間差異可使用 git diff ,以方便閱讀
- `q_delete_dup` 錯誤使用縮排
:::success
謝謝同學,已修正問題。
:::
## 開發環境
```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): 16
On-line CPU(s) list: 0-15
Vendor ID: GenuineIntel
Model name: 12th Gen Intel(R) Core(TM) i5-12500H
CPU family: 6
Model: 154
Thread(s) per core: 2
Core(s) per socket: 12
Socket(s): 1
Stepping: 3
CPU max MHz: 4500.0000
CPU min MHz: 400.0000
BogoMIPS: 6220.80
```
## 開發過程
在把更改過的檔案更新至 Github 的時候跳出下圖的結果
> -- Capitalize the subject line
發現沒注意到標題首字需要大寫,故回去看如何寫註解的規則。
根據[如何寫一個-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/)可以知道規則有以下:
1. 用一行空白行分隔標題與內容
2. 限制標題最多只有 50 字元
3. **標題開頭要大寫**
4. 標題不以句點結尾
5. 以祈使句撰寫標題
6. 內文每行最多 72 字
7. 用內文解釋 what 以及 why vs. how
## 針對佇列操作的程式碼實作
### q_new()
使用 `malloc` 來配置 `struct list_head` 大小的記憶體空間給最初的節點,為了避免 `head` 指標為 NULL 而導致 `malloc` 無法成功配置記憶體空間的情況,所以使用 if-statement 判斷此節點是否為 NULL,若不為 NULL 則使用 `list.h` 的函式 `INIT_LIST_HEAD` 進行節點的初始化。
```c
struct list_head *q_new()
{
struct list_head *head = malloc(sizeof(struct list_head));
if (head)
INIT_LIST_HEAD(head);
return head;
}
```
### q_free()
:::danger
避免過多的中英文混用,已有明確翻譯詞彙者,例如「鏈結串列」(linked list) 和「佇列」(queue),就使用該中文詞彙,英文則留給變數名稱、人名,或者缺乏通用翻譯詞彙的場景。
:::
根據要求,需要把全部佇列所配置的記憶體釋放掉。
首先會檢查 `list_head` 是否為空,如果是空的話,就直接離開這個函式。接下來,會使用 `list.h` 中的 `list_for_each_entry_safe` 來安全地走訪所有節點。這個函式會先刪除 `list_head`,然後使用`q_release_element` 函式來移除 `element_t` 裡的結構。最後釋放 `list head` 中的 `head`。
確保了在遍歷並刪除所有節點之後,進行資源釋放。
:::danger
避免非必要的項目縮排 (即 `* ` 和 `- `),以清晰、明確,且流暢的漢語書寫。
:::
```c
void q_free(struct list_head *head) {
if (!head)
return;
element_t *entry, *safe;
list_for_each_entry_safe(entry, safe, head, list) {
list_del(&entry->list);
q_release_element(entry);
}
free(head);
}
```
### `q_insert_head`
根據要求,在佇列開頭(head)插入給定的新節點。
:::danger
避免非必要的項目縮排 (即 `* ` 和 `- `),以清晰、明確,且流暢的漢語書寫。
<s>已更改</s>
沒做到的事,不要輕易宣稱。以下行文是否流暢?你如何展現學了二十餘年的漢語呢?
:::
檢查 `list_head` 是否為空,若為空就回傳 false。接著,為 node 配置`element_t`大小的記憶體空間,並檢查配置是否成功。
同時,我們也要為字串配置記憶體空間,其大小由`str_size`計算而得,再次確認配置是否成功。
在進行記憶體配置後,我們需要從指標 s 的記憶體位置複製到新節點的 value 位置。這裡要使用 memcpy 函數,將 s 中的字串複製到新節點的 value 中。需要注意的是,我們在計算`str_size`時已經包含了結尾的空字串\0,因此在複製字串時,我們只需要複製前面的字元,即 `str_size` - 1。
最後,使用 `list_add` 函數將新的節點插入到head所指向的節點之前。這樣就完成了將新節點加入到鍵結串列中的操作。
```c
bool q_insert_head(struct list_head *head, char *s)
{
element_t *node = malloc(sizeof(element_t));
if (!head || !node)
return false;
int str_size = (strlen(s) + 1) * sizeof(char);
node->value = malloc(str_size);
if (!node->value) {
free(node);
return false;
}
memcpy(node->value, s, str_size - 1);
node->value[str_size - 1] = '\0';
list_add(&node->list, head);
return true;
}
```
### q_insert_tail
此題與 `q_insert_head` 做法相似,只是改成在結尾的地方插入新的節點,所以我們只要改成最後面 `list_add_tail` 的地方。
```diff
bool q_insert_head(struct list_head *head, char *s)
{
element_t *node = malloc(sizeof(element_t));
if (!head || !node)
return false;
int str_size = (strlen(s) + 1) * sizeof(char);
node->value = malloc(str_size);
if (!node->value) {
free(node);
return false;
}
memcpy(node->value, s, str_size - 1);
node->value[str_size - 1] = '\0';
- list_add(&node->list, head);
+ list_add_tail(&node->list, head);
return true;
}
```
### q_remove_head
根據要求,在佇列開頭移去節點。
**刪除與移除的概念**可以參考 [2024 年 Linux 核心設計/實作課程作業 —— lab0 (A)](https://hackmd.io/@sysprog/linux2024-lab0/%2F%40sysprog%2Flinux2024-lab0-a) 提及的內容,了解刪除和移除主要的差異為**目標節點還是否存在於記憶體空間**。
"delete" 和 "remove" 看似都是「刪去某物」,在 Linux 核心的 [include/list.h](https://github.com/torvalds/linux/blob/master/include/linux/list.h) 中,這二個動作並存,但實際行為卻不同。依據 [Difference between "delete" and "remove"](https://english.stackexchange.com/questions/52508/difference-between-delete-and-remove) 的解釋,可知:
* "remove" 這動作對於 linked list 來說,是斷開節點和節點的關聯,也就是說原本若是 A B C,在 remove(B) 操作完成後,就會變成 A C 的節點關係。特別注意到 B 這個節點其實還存在於記憶體中,只是我們無法從節點間的關係看出來。於是 "remove" 可解讀為 "take away" (將某物帶走)
* "delete" 則有 "erase" 的涵義,若用於 linked list,則指某個節點將被「抹除」,對應的記憶體空間可能會被釋放 (release) 或回收 (reclaim),並非只是指標的變化
程式步驟:
首先要確保所處理的鏈結串列不是空的。因此,我們會檢查 `head` 指標以及目前的鏈結串列是否為空。如果為空就傳回 `NULL`,表示無有效資料可處理。
接著,在進行字串複製之前,我們需要檢查 `bufsize` 是否存在。若存在才會進行字串的複製操作。在這個過程中,我們必須確保在字串尾端添加 \0,以標示字串的結尾。在計算最大複製字符數時,我們會將所要複製的最大字符數減去結尾字符的佔位。
最後從鏈結串列中刪除移除節點指向的節點時,會回傳指向移除節點的指標。
```c
if (!head || list_empty(head))
return NULL;
element_t *remove_node = list_entry(head->next, element_t, list);
if (bufsize) {
strncpy(sp, remove_node->value, bufsize - 1);
sp[bufsize - 1] = '\0';
}
list_del(&remove_node->list);
return remove_node;
```
### q_remove_tail
此題與 `q_remove_head` 的做法相似,只要更改 `list_entry` 第一個參數 head->next 更改為 head->prev。
```diff
if (!head || list_empty(head))
return NULL;
- element_t *remove_node = list_entry(head->next, element_t, list
+ element_t *remove_node = list_entry(head->prev, element_t, list);
if (bufsize) {
strncpy(sp, remove_node->value, bufsize - 1);
sp[bufsize - 1] = '\0';
}
list_del(&remove_node->list);
return remove_node;
```
### q_size
根據要求,計算佇列的節點總量。
首先檢查串列的 `head` 是否為空,如果為空就回傳 `NULL`。接著我們可以使用 `list_for_each` 來走訪整個串列,並在每次走訪過一個節點時將計數器加一。最後,我們回傳計數器的值,這個值即為串列中節點的數量。
```c
if (!head) {
return 0;
}
int len = 0;
struct list_head *node;
list_for_each (node, head)
len++;
return len;
```
### q_delete_mid
根據要求,移除佇列中間的節點。
可以使用兩個指標 `left` & `right` 分別指向鏈結串列的頭尾,各自向反方向走訪節點尋找中間的節點,須考慮兩種情況:
* Case1: 節點數為奇數,兩個指標會相遇,使用 left != right 判斷
* Case2: 節點數為偶數,兩個指標會相鄰,使用 left->next != right 判斷
### q_delete_dup
在已經排序的狀況,移走佇列中具備重複內容的節點
:::danger
避免非必要的項目縮排 (即 `* ` 和 `- `),以清晰、明確,且流暢的漢語書寫。
:::
在檢查`head`和目前的鏈結串列是否為空的情況下,並利用`list_is_singular`確認是否僅有一個節點存在。接著,使用`list_for_each_entry_safe`來逐一走訪每一個節點。在走訪過程中,需要確認是否存在重複內容的節點,這可以透過以下步驟實現:
首先,利用`flag`這個標誌位來標記重複的情況。同時,利用字串指標儲存下一個節點的內容,並進行比較。若下一個節點不等於`head`且與目前節點的字串內容不同,則繼續迭代。若下一個節點與目前節點的字串內容相同,則將`flag`設置為1。當`flag`等於1時,則刪除該節點並釋放相應的資源。
這樣的流程確保了在鏈結串列中不僅能夠有效地檢查重複內容,並且能夠在需要時適當地刪除重複節點。
```c
if (!head)
return false;
if (list_empty(head) || list_is_singular(head))
return true;
element_t *node, *safe;
bool flag = 0;
list_for_each_entry_safe (node, safe, head, list) {
char *str = list_entry(node->list.next, element_t, list)->value;
if (node->list.next != head && !strcmp(str, node->value)) {
list_del(&node->list);
q_release_element(node);
flag = 1;
} else if (flag) {
list_del(&node->list);
q_release_element(node);
flag = 0;
}
}
return true;
```
### q_swap
根據要求,交換佇列中鄰近的節點。
#### swap nodes in pairs
根據提示,先理解 [leetcode - swap nodes in pairs](https://leetcode.com/problems/swap-nodes-in-pairs/),題目為單向鏈結串列的兩兩交換。
實作流程:建立一個名稱為 dummy 的節點指向第一個節點,用於儲存暫時的結果,`cur` 指標指向用來判別當下節點與該節點的下一個節點是否為 NULL,成立則停止交換。交換則使用 `first` 和 `second` 指標指向目標節點與目標的下一個節點,把兩者交換後把 `cur` 指標往後兩個節點更新。
```graphviz
digraph singly_linked_list {
rankdir=LR;
node [shape=record];
// Define the node structure
node [label="{<data> data | <next> next}"];
// Define nodes
dummy [label="dummy"];
node0 [label="{<data> 1 | <next> }"];
node1 [label="{<data> 2 | <next> }"];
node2 [label="{<data> 3 | <next> }"];
node3 [label="{<data> 4 | <next> }"];
// Connect nodes
dummy:next -> node0:data;
node0:next -> node1:data;
node1:next -> node2:data;
node2:next -> node3:data;
// Optional: Add a dummy node to represent the end of the list
null_node [label="NULL",shape=plaintext];
// Connect the last node to NULL
node3:next -> null_node;
// Add a cur pointer to the dummy node
cur [label="cur",shape=plaintext];
cur -> dummy;
}
```
:::spoiler 實作程式碼
```c
struct ListNode* swapPairs(struct ListNode* head) {
struct ListNode *dummy=(struct ListNode*)malloc(sizeof(struct ListNode));
dummy->next = head;
struct ListNode *cur = dummy;
while(cur->next!=NULL && cur->next->next!=NULL)
{
struct ListNode *first=cur->next;
struct ListNode *second=cur->next->next;
first->next = second->next;
cur->next = second;
second->next = first;
cur = cur->next->next;
}
return dummy->next;
}
```
:::
#### 回到 swap 實作
因為是雙向鏈結串列,所以我不用建立一個新的節點和 `curr` 來判斷是否指向 NULL,並且可以利用 `list.h` 裡的函式 `list_del()` 和 `list_add()` 來刪除與加入節點,之後發現有 `list_move()` 函式可以做到相同的動作,故更改程式。
我使用 `node1`, `node2` 以及 `safe` 指標分別指向頭部的下一個和下下個節點,safe 則指向下一輪交換的首節點。
```diff
void q_swap(struct list_head *head)
{
struct list_head *node1, *node2, *safe;
for (node1 = head->next, node2 = node1->next, safe = node2->next;
node1 != head && node2 != head;
node1 = safe, node2 = node1->next, safe = node2->next) {
- list_del(node1);
- list_add(node1,node2);
+ list_move(node1, node2);
}
}
```
### q_reverse