# 2024q1 Homework1 (lab0) contributed by < [`yulin0625`](https://github.com/yulin0625) > ### Review by `164253` `q_remove_head` 和 `q_remove_tail` 有許多重複部分 `q_delete_mid` 中的快慢指針可以改為兩個指針從頭尾向中間靠近 `q_ascend` 和 `q_descend` 中也有許多重複 ## 開發與實驗環境 ```shell $ gcc --version gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.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: 39 bits physical, 48 bits virtual Byte Order: Little Endian CPU(s): 12 On-line CPU(s) list: 0-11 Vendor ID: GenuineIntel Model name: 12th Gen Intel(R) Core(TM) i5-12400 CPU family: 6 Model: 151 Thread(s) per core: 2 Core(s) per socket: 6 Socket(s): 1 Stepping: 2 CPU max MHz: 4400.0000 CPU min MHz: 800.0000 BogoMIPS: 4992.00 ``` ## 使用的結構體 ### 雙向鏈節串列實作 :::warning 是「雙向鏈結串列」。 留意各式細節,唯有重視小處並步步為營,方可挑戰原始程式碼規模超越三千七百萬行的 Linux 核心 ::: 使用 `list_head` 以及 `element_t` 這兩種結構體來實作雙向鏈節 #### `list_head` ```c struct list_head { struct list_head *prev; struct list_head *next; }; ``` ```graphviz digraph list_head { rankdir=LR; node[shape=record, style=bold]; subgraph cluster_list_head { label="list_head"; style=bold; head [label="{<prev>prev|<next>next}"]; } } ``` #### `element_t` ```c typedef struct { char *value; struct list_head list; } element_t; ``` ```graphviz digraph list { rankdir=LR; node[shape=record, style=bold]; subgraph cluster_0 { node [shape=record]; value [label="value", width=1.35]; subgraph cluster_00 { label="list_head"; style=bold; head [label="{<prev>prev|<next>next}"]; } style=bold; label=element_t; } } ``` 從這兩種結構體的定義中,我們可以了解到本次作業中佇列的實作方式是將結構體 `list_head` 嵌入到新的結構體 `element_t` 中。這種方法類似於 Linux 核心的 Linked list 實作方式。 這樣設計的好處是只要將 `list_head` 納入到新的結構體的一個成員中,就可以操作這個佇列,而不需要自行維護一套雙向鏈結串列。 ```graphviz digraph list { rankdir=LR; node[shape=record, style=bold]; subgraph cluster_0 { label="list_head"; style=bold; head0 [label="{<prev>prev|<next>next}"]; } subgraph cluster_1 { node [shape=record]; value1 [label="value", width=1.35]; subgraph cluster_10 { label="list_head"; style=bold; head1 [label="{<prev>prev|<next>next}"]; } style=bold; label=element_t; } subgraph cluster_2 { node [shape=record]; value2 [label="value", width=1.35]; subgraph cluster_20 { label="list_head"; style=bold; head2 [label="{<prev>prev|<next>next}"]; } style=bold; label=element_t; } subgraph cluster_3 { node [shape=record]; value3 [label="value", width=1.35]; subgraph cluster_30 { label="list_head"; style=bold; head3 [label="{<prev>prev|<next>next}"]; } style=bold; label=element_t; } head0:next -> head1:prev; head1:next -> head2:prev; head2:next -> head3:prev; head3:next -> head0:prev; } ``` ### 佇列管理 使用結構體 `queue_contex_t` 及 `queue_chain_t` 管理多個佇列 #### `queue_contex_t` ```c typedef struct { struct list_head *q; struct list_head chain; int size; int id; } queue_contex_t; ``` :::danger 避免非必要的項目縮排 (即 `* `),以清晰、明確,且通順的漢語書寫。 > 已修正 ::: `q`:指向佇列 `head` 節點的指標 `chain`:用於將不同佇列的頭節點連接在一起 `size`:表示這個佇列的長度 `id`:唯一的識別號,用於區分不同的佇列 ```graphviz graph { rankdir=RL; node[shape=record, style=bold]; subgraph cluster_0 { label=queue_contex_t; style=bold; node [shape=record]; q [label="q", width=1.35]; subgraph cluster_00 { label="chain"; style=bold; h [label="{<prev>prev|<next>next}"]; } s [label="size", width=1.35]; id [label="id", width=1.35]; } } ``` #### `queue_chain_t` ```c typedef struct { struct list_head head; int size; } queue_chain_t; static queue_chain_t chain = {.size = 0}; ``` ```graphviz graph list { rankdir=LR; style=bold; node[shape=record, style=bold]; // queue_chain_t subgraph cluster_1 { style=bold; label="queue_chain_t"; node [shape=record]; s1 [label="size", width=1.35]; subgraph cluster_10 { label="chain"; style=bold; h1 [label="{<prev>prev|<next>next}"]; } } } ``` `queue_chain_t` 利用 `chain` 將各個 `queue_contex_t` 串連成一個雙向鍊結串列,如此一來便能管理多個佇列,示意圖如下: ```graphviz digraph list { rankdir=LR; style=bold; node[shape=record, style=bold]; // queue_chain_t subgraph cluster_1 { style=bold; label="queue_chain_t"; node [shape=record]; s1 [label="size", width=1.35]; subgraph cluster_10 { label="chain"; style=bold; h1 [label="{<prev>prev|<next>next}"]; } } // queue_contex_t subgraph cluster_2 { style=bold; label=queue_contex_t; node [shape=record]; q2 [label="q", width=1.35]; subgraph cluster_20 { label="chain"; style=bold; h2 [label="{<prev>prev|<next>next}"]; } s2 [label="size", width=1.35]; id2 [label="id", width=1.35]; } // queue_contex_t subgraph cluster_3 { style=bold; label=queue_contex_t; node [shape=record]; q3 [label="q", width=1.35]; subgraph cluster_30 { label="chain"; style=bold; h3[label="{<prev>prev|<next>next}"]; } s3 [label="size", width=1.35]; id3 [label="id", width=1.35]; } a [label="......", color=transparent] a -> h1:prev h1:next -> h2:prev; h2:next -> h3:prev; h3:next -> a //{rank=same; s1; s2; s3} } ``` ## 指定的佇列操作 ### `q_new` > commit [2201f29](https://github.com/sysprog21/lab0-c/commit/2201f294a4eeda28eb3eda7587da4e57b6712a04) :::danger 使用你 fork 後得到的 GitHub repository 的 commit 超連結 ::: 建立新的「空」佇列 :::danger 避免非必要的項目縮排 (即 `* ` 和 `- `),以清晰、明確,且流暢的漢語書寫。 ::: 1. 使用 `malloc` 配置 `list_head` 大小的記憶體空間,並由指標 `head` 指向 2. 若空間分配失敗(`malloc` 回傳 NULL),則回傳 `NULL` 3. 使用 `INIT_LIST_HEAD` 初始化 `head` ### `q_free` > commit [0fa78c5](https://github.com/sysprog21/lab0-c/commit/0fa78c540de3c65ebc0e02733dc4b599be47da5a) 釋放佇列所佔用的記憶體 使用 `list_for_each_entry_safe` 走訪每個節點,再利用 `q_release_element` 釋放節點所佔用的記憶體 ### `q_insert_head` > commit [b4290b3](https://github.com/sysprog21/lab0-c/commit/b4290b3e0e1dd54a8f3413fc597549a3039529fa) 在佇列前端插入值為 `s` 的新節點 :::danger 避免非必要的項目縮排 (即 `* ` 和 `- `),以清晰、明確,且流暢的漢語書寫。 ::: 使用 `melloc` 配置 `element_t` 大小的記憶體空間,若配置失敗則回傳 `false` 使用函式 `strdup` 複製字串資料,若複製失敗則回傳 `false` 使用 `list_add` 將節點加入佇列的前端 ### `q_insert_tail` > commit [b4290b3](https://github.com/sysprog21/lab0-c/commit/b4290b3e0e1dd54a8f3413fc597549a3039529fa) 在佇列尾端插入值為 `s` 的新節點 我發現 `q_insert_tail` 實際上與 `q_insert_head` 幾乎相同,唯一的區別在於新元素應該從佇列的哪個位置添加。因此,可以重用 `q_insert_head` 的程式碼。 ```c /* Insert an element at tail of queue */ bool q_insert_tail(struct list_head *head, char *s) { if (!head) { return false; } q_insert_head(&head->prev, s); } ``` :::danger 改進你的漢語表達。 ::: ### `q_remove_head` > commit [b4290b3](https://github.com/sysprog21/lab0-c/commit/b4290b3e0e1dd54a8f3413fc597549a3039529fa) 將佇列的第一個 `entry` 移除,並將 `entry` 的 `value` 複製到 `buffer` 中 :::danger 避免非必要的項目縮排 (即 `* ` 和 `- `),以清晰、明確,且流暢的漢語書寫。 ::: 使用 `list_first_entry` 取得佇列的第一個 `entry` 使用 `list_del` 將該 `entry` 由佇列中刪除 使用 `strncpy` 複製該 `entry` 的 `value` 至 `buffer`中 :::danger 注意行文的空白字元! ::: 發現原本的方法在buffer大小不夠時會產生字串結尾無'\0'的狀況,修改方式如下: [7ceb10a](https://github.com/sysprog21/lab0-c/commit/7ceb10a547274ece326883340e493983230be0ac) ```diff @@ -79,7 +79,8 @@ element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize) list_del(&element->list); if (sp && bufsize) { - strncpy(sp, element->value, bufsize); + strncpy(sp, element->value, bufsize - 1); + sp[bufsize - 1] = '\0'; } return element; ``` ### `q_remove_tail` > commit [b4290b3](https://github.com/sysprog21/lab0-c/commit/b4290b3e0e1dd54a8f3413fc597549a3039529fa) 將佇列的最後一個 `entry` 移除,並將 `entry` 的 `value` 複製到 `buffer` 中 與 `q_remove_head`類似,差別為將 `list_first_entry` 改為 `list_last_entry` ### `q_size` > commit [fc78b84](https://github.com/sysprog21/lab0-c/commit/fc78b84f89a17393f12c0bc20da2af408850b278) 取得佇列的長度 使用 `list_for_each` 走訪佇列的每個節點,並利用變數 `len` 紀錄節點數量 ### `q_delete_mid` > commit [8dc1568](https://github.com/sysprog21/lab0-c/commit/8dc1568dfd782deb7cadb1c69ac6cf7460352e32) 刪除佇列中間的節點 :::danger 避免非必要的項目縮排 (即 `* ` 和 `- `),以清晰、明確,且流暢的漢語書寫。 ::: 參考 [你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list#%E6%A1%88%E4%BE%8B%E6%8E%A2%E8%A8%8E-Leetcode-2095-Delete-the-Middle-Node-of-a-Linked-List) 所提到的**快慢指標**技巧找出佇列中間的節點 使用 `list_del` 將該節點由佇列刪除 使用 `q_release_element` 釋放該節點所佔用的記憶體 ### `q_delete_dup` > commit [b5f0c26](https://github.com/sysprog21/lab0-c/commit/b5f0c26a074c7e32f16ff40f701721e406d2c9ac) 刪除佇列中具有重複值的節點 實作方式: 1. 使用 `list_for_each_entry_safe` 走訪佇列中的每個節點 2. 使用 `strcmp` 比較該節點與右側節點是否有相同的值 3. 若兩節點有相同的值,則將 `equal` 設為 `true`,並將該節點刪除 4. 若兩節點沒有相同的值,則判斷 `equal` 是否為 `true`, ### `q_reverse` > commit [dd6f602](https://github.com/sysprog21/lab0-c/commit/dd6f602c5b63ce408dc2fbf9a564df3c9a2d6924) 反轉佇列中的元素 逐一將每個節點移動至佇列的最前方,等同於將佇列反轉 使用 `list_for_each_entry_safe` 走訪每個佇列的節點,並利用 `list_move` 將節點移動至佇列的最前方 ### `q_reverseK` > commit [ab10d48](https://github.com/sysprog21/lab0-c/commit/ab10d48aabd127750a631c0df98d5788aa5266f0) 將佇列每 `k` 個節點反轉一次 將佇列以 `k` 為單位,切割成數個子佇列,再將每個子佇列反轉,即可獲得 `reverseK` 的效果 1. 走訪佇列的每個節點,並以指標 `cut` 紀錄子佇列的開頭處 2. 若經過 `k` 個點,則利用 `list_cut_position` 將子佇列切出,並複製至新的佇列 `tmp` 3. 使用 `q_reverse` 將 `tmp` 佇列反轉 4. 使用 `list_splice` 將 `tmp` 佇列插入回原本的佇列 ### `q_swap` > commit [c9e9b0b](https://github.com/sysprog21/lab0-c/commit/c9e9b0b29dc4586e9c1459f85a75118b02be3808) 將佇列每2個節點反轉一次 `q_swap` 為 `q_reverseK` k=2 時的特例 ### `q_ascend` > commit [6a45a0c](https://github.com/sysprog21/lab0-c/commit/6a45a0c395bbc54ec44fd86d4ef0b7997a6ab594) 移除每個節點,只要該節點右側的任何地方存在一個嚴格小於它的節點。 題目要求「若某節點的右側存在值小於該節點的其他節點,則必須移除該節點」。 如果按照原始規則,需要使用兩層迴圈來實作。然而,我們可以將這個條件簡化為「由右至左檢查每個節點,若該節點的左側節點**大於**該節點,則將左側節點移除」。這樣的話,只需要一層迴圈即可實作。 ### `q_descend` > commit [6a45a0c](https://github.com/sysprog21/lab0-c/commit/6a45a0c395bbc54ec44fd86d4ef0b7997a6ab594) 移除每個節點,只要該節點右側的任何地方存在一個嚴格大於它的節點。 與 `q_ascend` 實作方式類似,差別為將條件改為「由右至左檢查每個節點,若該節點的左側節點**小於**該節點,則將左側節點移除」。 ### `q_sort` > commit [601f6b5](https://github.com/sysprog21/lab0-c/commit/601f6b5b5e82e54e98ab315a874e77c9ab41b430) 將佇列中的元素按照升序/降序排序 採用 merge sort 遞迴演算法來實作。 :::danger 如何確保目前的測試已涵蓋排序演算法的最差狀況? ::: ### `q_merge` > commit [2552906](https://github.com/sysprog21/lab0-c/commit/2552906dc138bd97c594ca95c44424e4da773048) 將所有的佇列合併成一個排序的佇列 將所有的佇列合併為一個佇列,再將該佇列用 `q_sort` 進行排序 ## 以 [Valgrind](https://valgrind.org/) 分析記憶體問題 執行 `make valgrind` 執行 massif: 分析 - Heap blocks - Heap administration blocks - Stack sizes ```shell valgrind --tool=massif ./qtest -f <trace file> ``` 使用 Valgrind 驗證程式後,沒有出現記憶體錯誤的狀況,但仍然會有錯誤訊息 `ERROR: Probably not constant time or wrong implementation` 使用 massif-visualizer 將數據圖形化 ```shell $ massif-visualizer massif.out.<pid> ``` 分析 trace-17-complexity.cmd ![Massif_result](https://hackmd.io/_uploads/B12IfKKC6.png) ## 使用 address sanitizer ```shell make clean make SANITIZER=1 ``` 之後執行 `make test` ,沒有出現任何記憶體相關錯誤訊息,檢查通過。 :::danger 尚未依據作業三指示,進行對應的 git rebase 操作! :::