# 2023q1 Homework1 (quiz1) contributed by < [`Shiritai`](https://github.com/Shiritai) > ## 開發環境 ```bash $ gcc --version gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.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: 12th Gen Intel(R) Core(TM) i3-12100 CPU family: 6 Model: 151 Thread(s) per core: 2 Core(s) per socket: 4 Socket(s): 1 Stepping: 5 CPU max MHz: 5500.0000 CPU min MHz: 800.0000 BogoMIPS: 6604.80 ``` ## 測驗一 ### 作答部分 * `AAA` 由 `pivot` 變數名稱與其在程式碼其他地方的行為,推測其為快速排序法的錨點,再由使用的 `list` API 猜測程式碼想取第一項目為錨點,我們需以正確的方式使用 `list` API。 於 `list.h` 中,`list_first_entry` 的說明如下: ```c= /** * list_first_entry() - Get first entry of the list * @head: pointer to the head of the list * @type: type of the entry containing the list node * @member: name of the list_head member variable in struct @type * * Return: @type pointer of first entry in list */ #define list_first_entry(head, type, member) \ list_entry((head)->next, type, member) ``` 清楚地提到 `AAA` 應該放擁有 `list_head` 成員的節點型別: `struct item` * `BBB` 承上,第二參數為 `list_head` 成員於 `item` 中的名稱,即 `list`。 * `CCC` `CCC` 出現的位置與形式讓人猜測為走訪所有節點,以下為所有走訪節點的巨集: ```c= list_for_each(node, head) list_for_each_entry(entry, head, member) list_for_each_safe(node, safe, head) list_for_each_entry_safe(entry, safe, head, member) ``` 可發現參數數量不同,依據文件註解所述,可總結出 * `_entry` 表透過 `list_head` 成員走訪其 container * `_safe` 表使用同時兩節點走訪,目的在於可以更改走訪過程中後者節點的指標,也就可以對其進行「移除」、「刪除」等操作。 不過要回答這題只需要知道其需要四個參數,唯一符合的只有 `list_for_each_entry_safe`。 * `DDD` `CCC` 以降的迴圈所執行的行為可以猜測為快速排序法的 partition,透過觀察執行 `DDD` 的條件,可以發現是對相較 `pivot` 小者的操作。另觀察 `list_less` 和 `list_greater` 起初都是空串列,可以推測這兩個首節點應該負責 partition 的任務,推測 `DDD` 應為將 `itm` 插入 `list_less` 中,故 `DDD` 似乎可為任何插入節點的 `list` API。 但要注意的是,題目要求 stable sort,原本的順序應該被保留,應該將較晚進行判斷的節點放入 `list_less` 的後面,故 `DDD` 為 `list_move_tail`。 * `EEE` 承上題,同理 `EEE` 為 `list_move_tail`。 * `FFF` 快速排序法為 divide and conquer 的演算法,conquer 完後要將原本切成兩份的串列併回一條。 ```graphviz digraph { head; pivot[color=red]; list_less -> back[dir="both"]; n1_[label="...", shape = plaintext ]; back -> n1_[dir="both"]; list_less -> front[dir="both"]; front -> n1_[dir="both"]; _back[label="back"]; list_greater -> _back[dir="both"]; _n1_[label="...", shape = plaintext ]; _back -> _n1_[dir="both"]; _front[label="front"]; list_greater -> _front[dir="both"]; _front -> _n1_[dir="both"]; } ``` 觀察最後三行,第一行將 `pivot` 放回目前為空串列的 `head`。 ```graphviz digraph { pivot[color=red]; head -> pivot[dir=both]; list_less -> back[dir="both"]; n1_[label="...", shape = plaintext ]; back -> n1_[dir="both"]; list_less -> front[dir="both"]; front -> n1_[dir="both"]; _back[label="back"]; list_greater -> _back[dir="both"]; _n1_[label="...", shape = plaintext ]; _back -> _n1_[dir="both"]; _front[label="front"]; list_greater -> _front[dir="both"]; _front -> _n1_[dir="both"]; } ``` 第二行由函式註解得知為將 `list_less` 加入 `head` 串列的前方。 ```c= /** * list_splice() - Add list nodes from a list to beginning of another list * @list: pointer to the head of the list with the node entries * @head: pointer to the head of the list * * All nodes from @list are added to the beginning of the list of @head. * It is similar to list_add but for multiple nodes. The @list head is not * modified and has to be initialized to be used as a valid list head/node * again. */ static inline void list_splice(struct list_head *list, struct list_head *head) ``` 至此,我們已經完成合併步驟的一半,剩下另一半「大者」也須併回原串列。 ```graphviz digraph { pivot[color=red]; head -> pivot[dir=both]; list_less; pivot -> back[dir="both"]; n1_[label="...", shape = plaintext ]; back -> n1_[dir="both"]; head -> front[dir="both"]; front -> n1_[dir="both"]; _back[label="back"]; list_greater -> _back[dir="both"]; _n1_[label="...", shape = plaintext ]; _back -> _n1_[dir="both"]; _front[label="front"]; list_greater -> _front[dir="both"]; _front -> _n1_[dir="both"]; } ``` 如此可猜測 `FFF` 為 `list_splice_tail`,如此一來便可將「大者」以正確的順序合併。 ```graphviz digraph DG { rankdir="LR"; list_less; list_greater; head -> front[dir="both"]; n1_[label="...", shape = plaintext ]; front -> n1_[dir="both"]; n1_ -> back[dir="both"]; back -> pivot[dir="both"]; _front[label="front"]; pivot -> _front[dir="both"]; _n1_[label="...", shape = plaintext ]; _front -> _n1_[dir="both"]; _back[label="back"]; _n1_ -> _back[dir="both"]; _back -> head[dir="both"]; pivot[color=red]; } ``` ### 程式碼運作原理 作答部分已說明,在此對沒有解釋的部分進行補充。 * 邊界情形 遞迴初始條件為 0~1 個節點。 :::info 其實可以只考慮空串列的情況。注意若只有一節點,則該節點會作為 `pivot`,不會繼續遞迴呼叫導致無窮遞迴。不過為了效能考量,提早檢查單節點串列的情況可減少函式呼叫次數。 ::: ```c= if (list_empty(head) || list_is_singular(head)) return; ``` ### 改進實作 快速排序的優化很多種,比如 * 調整取 `pivot` 策略 * 三路快排 * hybrid sort 我的以下改進實作基於 `lab0-c` 的框架,進而可以和 `q_sort` (merge + insertion sort) 和純 insertion sort 進行比較。 以下為串列之快速排序的實作,有以下特點。 * 以首個節點為 pivot 以保持穩定排序 * 採三路快排,與 pivot 相等的節點會串在以 pivot 為第一節點 (`list_middle` 之下) 的後方,不需參與後續的遞迴呼叫。 * partition 時計算各個分區的大小,以此為基礎決定後續要採取 insertion sort 或者 quick sort。 ```c= void q_quicksort(struct list_head *head, bool descend) { if (!head || list_empty(head) || list_is_singular(head)) return; // optimization: use head as "list_less" struct list_head list_middle, list_greater; INIT_LIST_HEAD(&list_middle); INIT_LIST_HEAD(&list_greater); element_t *pivot = list_first_entry(head, element_t, list); list_move(&pivot->list, &list_middle); element_t *cur, *nxt; int l_cnt, r_cnt; l_cnt = r_cnt = 0; list_for_each_entry_safe (cur, nxt, head, list) { // optimization: 3-way quick partition int order = strcmp(cur->value, pivot->value); if (!order) { // "=" list_move_tail(&cur->list, &list_middle); } else if (order > 0) { // ">" list_move_tail(&cur->list, &list_greater); ++r_cnt; } else { // "<" ++l_cnt; } } // optimization: hybrid sort // Use quick sort + insertion sort // threshold to use insertion sort optimization static const int THRESHOLD = 32; if (l_cnt > THRESHOLD) { q_quicksort(head, descend); } else if (l_cnt) { q_i_sort(head->next, head->prev, descend); } if (r_cnt > THRESHOLD) { q_quicksort(&list_greater, descend); } else if (r_cnt) { q_i_sort(list_greater.next, list_greater.prev, descend); } list_splice_tail(&list_middle, head); list_splice_tail(&list_greater, head); } ``` ## 測驗二 ## 測驗三