# 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