--- tags: DYKC, C, CLANG, C LANGUAGE, linked list, leetcode --- # [你所不知道的 C 語言](https://hackmd.io/@sysprog/c-prog/): linked list 和非連續記憶體 Copyright (**慣C**) 2018, 2022 [宅色夫](http://wiki.csie.ncku.edu.tw/User/jserv) ==[直播錄影](https://youtu.be/pTcRq__iKzI)== ## 前言 「[你所不知道的 C 語言](https://hackmd.io/@sysprog/c-prog/)」講座致力於引導學員思索專業的程式開發議題,「linked list 和非連續記憶體操作」探討以下: * 檢驗學員對於 C 語言指標操作的熟悉程度 * linked list (鏈結串列) 本質上就是對非連續記憶體的操作,乍看僅是一種單純的資料結構,但對應的演算法變化多端,像是「如何偵測 linked list 是否存在環狀結構?」和「如何對 linked list 排序並確保空間複雜度為 $O(1)$ 呢?」 * linked list 的操作,例如走訪 (traverse) 所有節點,反映出 Locality of reference (cache 用語) 的表現和記憶體階層架構 (memory hierarchy) 高度相關,學員很容易從實驗得知系統的行為,從而思考其衝擊和效能改進方案 無論是作業系統核心、C 語言函式庫內部、程式開發框架,到應用程式,都不難見到 linked list 的身影,包含多種針對效能和安全議題所做的 linked list 變形,又還要考慮到應用程式的泛用性 (generic programming),是很好的進階題材。 ## 從 Linux 核心的藝術談起 [Linus Torvalds 在 TED 2016 的訪談](http://www.ted.com/talks/linus_torvalds_the_mind_behind_linux?language=zh-tw) ![](https://i.imgur.com/PYHZ8h0.png) > 「我不是願景家,我是工程師。我對那些四處遊蕩、望著雲端的人沒有任何意見。但是我是看著地面的人,我想修補就在我面前的坑洞,以免跌進去。」 Linus Torvalds 在 TED 訪談中提及自己的思維模式、性格與 Linux 和 Git 背後的心路歷程。 於 [14:10](https://youtu.be/o8NPllzkFhE?t=850) 時,他提到程式設計的 "good taste" 為何。 :::info 對照 CMU 教材 ==[Linked Lists](https://www.andrew.cmu.edu/course/15-121/lectures/Linked%20Lists/linked%20lists.html)== ![](https://i.imgur.com/Qof9hCw.png) * 3 exceptional cases, we need to take care of: - list is empty - delete the head node - node is not in the list 以下的討論不涵蓋 list is empty 和 node is not in the list 的狀況 ::: 原本的程式碼 (10 行) ```c void remove_list_node(List *list, Node *target) { Node *prev = NULL; Node *current = list->head; // Walk the list while (current != target) { prev = current; current = current->next; } // Remove the target by updating the head or the previous node. if (!prev) list->head = target->next; else prev->next = target->next; } ``` 直觀的寫法會有特例,也就是第一筆資料與中間的資料的移去操作不同。 若要移去的是第一筆資料,那就需要把指標指向第一個節點;而若是要移除中間的資料,則須要把指標指向目標的前一個節點。 - [ ] 有「品味」的版本 (4 行) ```c void remove_list_node(List *list, Node *target) { // The "indirect" pointer points to the *address* // of the thing we'll update. Node **indirect = &list->head; // Walk the list, looking for the thing that // points to the node we want to remove. while (*indirect != target) indirect = &(*indirect)->next; *indirect = target->next; } ``` Linus Torvalds 換個角度來思考,通過指標的指標 (或稱「間接指標」,即 indirect pointer) 來操作,如此一來,上面的特例就不存在。 :::success 依據中華民國教育部《[重編國語辭典修訂本](https://dict.revised.moe.edu.tw/)》,「[雙](https://dict.revised.moe.edu.tw/dictView.jsp?ID=9189)」作為量詞時,指計算成對物品的單位,因此間接指標不該被稱為「雙」指標:一個指標指向到另一個指標,在記憶體存取的角度來看,實屬階層 (hierarchy) 關係,而非「成對」或「對等」的存在 ::: Linus Torvalds 的解說: > People who understand pointers just use a "pointer to the entry pointer", and initialize that with the address of the list_head. And then as they traverse the list, they can remove the entry without using any conditionals, by just doing`*pp = entry->next`. 在 [15:25](https://youtu.be/o8NPllzkFhE?t=925) 時,他說: > It does not have the if statement. And it doesn't really matter -- I don't want you understand why it doesn't have the if statement, but I want you to understand that sometimes you can see a problem in a different way and rewrite it so that a special case goes away and becomes the normal case. And that's good code. But this is simple code. This is CS 101. This is not important -- although, details are important. 重點是有時候我們可以從不同的角度來詮釋問題,然後重寫,那麼例外就會消失,這樣就是好的程式。 原本的方式是,有個指向前一元素的指標、另一個目前位置的指標,再利用 `while` 敘述逐一走訪鏈結串列,找到 `target` 時停下來。這會有個分支判斷 `prev` 是否為 `NULL`,若為 `NULL` 就代表 target 是鏈結串列的 head,因此需要把鏈結串列的 head 指向下個元節點;若非 `NULL` 就把前個節點的 `next` 設為目前的下一個節點。 ![](https://hackmd.io/_uploads/Sy7cn-yht.png) 而 Linus Torvalds 的想法則是拿一個指標指向「節點裡頭指向下個節點 的指標」,以「要更新的位址」為思考點來操作。 有個指標的指標 `indirect`,一開始指向 head,之後一樣走訪 list,解指標看是不是我們要的 target,如果 `*indirect` 就是我們要移去的元素,代表 `indirect` 現在指向前一個節點裡面的 `next` 指標,因此把 `*indirect` 設為 target 的下個節點,即可完成整個操作: ![](https://hackmd.io/_uploads/HyuZ6Z13t.png) > 圖解出處: [The mind behind Linux 筆記](https://hackmd.io/@Mes/The_mind_behind_Linux) 解說: [Linked lists, pointer tricks and good taste](https://github.com/mkirchner/linked-list-good-taste) > [符合 Linux 核心風格的調整](https://github.com/felipec/linked-list-good-taste) 以下是拆解:我們先做一個指向 head 的指標: `Node **indirect = &list->head;` ```mermaid graph LR subgraph linked list head==>node1==>node2 end subgraph pointer to pointer indirect==>head end ``` 在走訪 list 時,先進行 `*indirect` (dereference `indirect` 找出其指向的 node,此刻為 head), 接著 `(*indirect)->next` (找出它的 `->next` ,這邊為 head 的 next,即 `node1`),然後 `indirect = &((*indirect)->next)` (將其 reference 存回 `indirect` ,此刻 `indirect` 就變更為指向 `node1` 的指標): ```mermaid graph LR subgraph linked list head==>node1==>node2 end subgraph pointer to pointer indirect==>node1 end ``` 如此一來 head 從頭到尾都沒有動,直接回傳 head 就好。 :::info 從「要更新什麼位置的資料」思考,無論是 head 或者非 head,更新的是同一類型的資料,不用特別操作,自然省下額外的處理 ::: 延續上述討論,我們嘗試撰寫完整的 C 程式,過程中思考「如何撰寫更優雅的程式碼」。首先是結構定義: ```c typedef struct list_entry { int value; struct list_entry *next; } list_entry_t; ``` 要從給定的鏈結串列中,移走 (remove) 符合特定數值的節點時,可撰寫以下程式碼: ```c list_entry_t *remove(list_entry_t *head, int value) { if (!head) return NULL; if (head->value == value) return head->next; list_entry_t *prev = head; list_entry_t *cur = head->next; while (cur) { if (cur->value == value) { prev->next = cur->next; return head; } prev = cur; cur = cur->next; } return head; } ``` 這段程式碼考慮到開頭的節點是否被移走,於是要有對應的程式碼來處理特例,最終回傳新的開頭節點。改進方式為,引入一個暫時節點,使其在給定的開頭節點之前,這樣就不用特別撰寫程式碼來處理特例: ```c list_entry_t *remove(list_entry_t *head, int value) { list_entry_t dummy = {.next = head}; for (list_entry_t *prev = &dummy; prev->next; prev = prev->next) { if (prev->next->value == value) { prev->next = prev->next->next; break; } } return dummy.next; } ``` 不過這段程式碼依舊可改進,因為我們根本不用回傳新的開頭節點,相反的,可將函式原型改為 `void remove(list_entry_t **head, int value)`,藉由間接指標來更動傳入的鏈結串列開頭節點。 > 這部分的程式碼給學員當練習題。 針對鏈結串列的新增節點的操作,考慮以下程式碼: ```c void append(int value, list_entry_t **head) { list_entry_t *direct = *head; list_entry_t *prev = NULL; list_entry_t *new = malloc(1 * sizeof(list_entry_t)); new->value = value, new->next = NULL; while (direct) { prev = direct; direct = direct->next; } if (prev) prev->next = new; else *head = new; } ``` 函式的參數列表中,之所以用 `list_entry_t **head`,而非 `list_entry_t *head`,是因為原本傳入的 `head` 可能會被變更,但 C 語言在參數傳遞時永遠都是傳遞數值 (副本),於是若接受 `list_entry_t *head` 做為參數,那就要提供 `append` 函式的傳回值,也就是說,該函式原型宣告變為: ```c list_entry_t *append(int value, list_entry_t *head); ``` 如此就不優雅且顯得累贅。運用上述 indirect pointer 的技巧,我們可重寫 `append` 函式如下: ```c void append_indirect(int value, list_entry_t **head) { list_entry_t **indirect = head; list_entry_t *new = malloc(1 * sizeof(list_entry_t)); new->value = value, new->next = NULL; while (*indirect) indirect = &((*indirect)->next); *indirect = new; } ``` 延伸閱讀: * [Applying the Linus Torvalds "Good Taste" Coding Requirement](https://medium.com/@bartobri/applying-the-linus-tarvolds-good-taste-coding-requirement-99749f37684a) * [Linus on Understanding Pointers](https://grisha.org/blog/2013/04/02/linus-on-understanding-pointers/) ### 案例探討: [LeetCode 21. Merge Two Sorted Lists](https://leetcode.com/problems/merge-two-sorted-lists/) [LeetCode 21. Merge Two Sorted Lists](https://leetcode.com/problems/merge-two-sorted-lists/) 簡述: > Merge two sorted linked lists and return it as a sorted list. The list should be made by splicing together the nodes of the first two lists. > (給定二個已排序的 linked list,傳回合併過後的 linked list) 直觀的做法是,提供一個暫時節點,依序將內含值較小的節點串上,最後回傳暫時節點指向的次個節點: ```c struct ListNode *mergeTwoLists(struct ListNode *L1, struct ListNode *L2) { struct ListNode *head = malloc(sizeof(struct ListNode)); struct ListNode *ptr = head; while (L1 && L2) { if (L1->val < L2->val) { ptr->next = L1; L1 = L1->next; } else { ptr->next = L2; L2 = L2->next; } ptr = ptr->next; } ptr->next = L1 ? L1 : L2; return head->next; } ``` 倘若我們想避免配置暫時節點的空間 (即上方程式碼中的 `malloc`),該怎麼做?運用上述 indirect pointer 的技巧: ```c struct ListNode *mergeTwoLists(struct ListNode *L1, struct ListNode *L2) { struct ListNode *head; struct ListNode **ptr = &head; for (; L1 && L2; ptr = &(*ptr)->next) { if (L1->val < L2->val) { *ptr = L1; L1 = L1->next; } else { *ptr = L2; L2 = L2->next; } } *ptr = (struct ListNode *)((uintptr_t) L1 | (uintptr_t) L2); return head; } ``` 觀察使用 indirect pointer 版本的程式碼,其中 `if-else` 的程式碼都是將 ptr 指向下一個要接上的節點,之後將節點更新到下一個,不過要為 L1 跟 L2 分開寫同樣的程式碼,該如何簡化?可以再使用一個指標的指標來指向 L1 或 L2。 ```c struct ListNode *mergeTwoLists(struct ListNode *L1, struct ListNode *L2) { struct ListNode *head = NULL, **ptr = &head, **node; for (node = NULL; L1 && L2; *node = (*node)->next) { node = (L1->val < L2->val) ? &L1: &L2; *ptr = *node; ptr = &(*ptr)->next; } *ptr = (struct ListNode *)((uintptr_t) L1 | (uintptr_t) L2); return head; } ``` 注意 `node = L1->val < L2->val? &L1: &L2` 不能寫成 `node = &(L1->val < L2->val? L1: L2)`,依據 [C99 規格書](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1124.pdf) `6.5.15` 的註腳: > A conditional expression does not yield an lvalue 因此無法使用 `&` (address of) 去取得 `L1->val < L2->val? L1: L2` 的地址,只能分開取得 L1 和 L2 的地址。 | expression | dereference | 說明 | | ------------------------ | ------------------------ | --------------------------------------------------------------- | | `ptr = &head;` | `head` | `ptr` 指向 head | | `node = &L1;` | `L1` | 假設比較完 `L1` 跟 `L2` 後,<br> `node` 指向 `L1` | | `*ptr = *node;` | `head = L1;` | 將要合併節點更新到 `head` | | `*node = (*node)->next;` | `L1 = L1->next;` | 因為要合併剩下的節點,所以<br>將節點更新到 `next` | | `ptr = &(*ptr)->next;` | `ptr = &(head)->next;` | 將 `ptr` 指向 `head->next` 讓<br>下一輪迴圈更新 `head->next` | [LeetCode 23. Merge k Sorted Lists](https://leetcode.com/problems/merge-k-sorted-lists/) 則將 [LeetCode 21. Merge Two Sorted Lists](https://leetcode.com/problems/merge-two-sorted-lists/) 指定的 2 個 linked list 擴充為 k 個的合併,其本質就是將分割好的 sorted lists 合併成一條,示意如下: ```graphviz digraph G { result[label="1|2|3|4|5|6|7|8", shape=record, height=.1] node31[label="2|5", shape=record, height=.1] node32[label="4|6|8", shape=record, height=.1] node33[label="1|7", shape=record, height=.1] node34[label="3", shape=record, height=.1,width=.4] node41[label="2|4|5|6|8", shape=record, height=.1] node42[label="1|3|7", shape=record, height=.1] subgraph cluster_3 { style=filled; color="#a6cee3"; label="sorted lists"; node32; node34; node33; node31; } node31 -> node41 node32 -> node41 node33 -> node42 node34 -> node42 subgraph cluster_2 { style=filled; color="#b2df8a"; label="merge"; node41 node41 node42 node42 node41->result node42->result } } ``` 顯然在 `merge` 階段可延用上述 `mergeTwoLists` 函式,至於 list 合併的方向就是演算法勝出的關鍵,可能的思路如下: - [ ] naive 直觀地用第一條串列接上剩下的串列,這樣會導致 `lists[0]` 愈來愈長,立即會遇到的問題是:多數的情況下合併速度會愈來愈慢。 ```c struct ListNode *mergeKLists(struct ListNode **lists, int listsSize) { if (listsSize == 0) return NULL; for (int i = 1; i < listsSize; i++) lists[0] = mergeTwoLists(lists[0], lists[i]); return lists[0]; } ``` - [ ] 頭跟尾兩兩合併 從固定第一條串列改成頭跟尾兩兩合併,直到剩一條為止,比起前一方法的每次都用愈來愈長的串列跟另一條串列合併,頭尾合併在多數的情況下兩條串列的長度比較平均,合併會比較快。 當合併完頭尾後,偶數長度會少一半,奇數長度則為 `(listsSize + 1) / 2`,奇數更新的方式也可以用在偶數長度上。 ```c struct ListNode *mergeKLists(struct ListNode **lists, int listsSize) { if (listsSize == 0) return NULL; while (listsSize > 1) { for (int i = 0, j = listsSize - 1; i < j; i++, j--) lists[i] = mergeTwoLists(lists[i], lists[j]); listsSize = (listsSize + 1) / 2; } return lists[0]; } ``` - [ ] 分段合併 除了頭尾合併,還可以分段存取下個要合併的串列,分別合併 `lists[i]` 跟 `lists[i + interval]`,為確保內層迴圈不會重複存取,索引值 `i` 會更新為 `i + interval * 2`,當內層迴圈合併完之後會把結果分別存到 `lists[interval*0]`, `lists[interval*2]`, `lists[interval*4]`, `lists[interval*6]`, 等等,示意如下: ```graphviz digraph G { { node[shape=none,label="interval = 1"]; i1 node[shape=none,label="interval = 2"]; i2 node[shape=none,label="interval = 4"]; i4 node[shape=none,label="interval = 8"]; i8 } interval1[label="<f0>L0|<f1>L1|<f2>L2|<f3>L3|<f4>L4|<f5>L5|<f6>L6|<f7>L7", shape=record, fixedsize=false,width=6] interval2[label="<f0>L01|<f1>|<f2>L23|<f3>|<f4>L45|<f5>|<f6>L67|<f7>", shape=record, fixedsize=false,width=6] interval4[label="<f0>L0123|<f1>|<f2>|<f3>|<f4>L4567|<f5>|<f6>|<f7>", shape=record, fixedsize=false,width=6] interval8[label="<f0>result|<f1>|<f2>|<f3>|<f4>|<f5>|<f6>|<f7>", shape=record, fixedsize=false,width=6] i1->i2[style=invis] i2->i4[style=invis] i4->i8[style=invis] interval1:f0 -> interval2:f0 interval1:f1 -> interval2:f0 interval1:f2 -> interval2:f2 interval1:f3 -> interval2:f2 interval1:f4 -> interval2:f4 interval1:f5 -> interval2:f4 interval1:f6 -> interval2:f6 interval1:f7 -> interval2:f6 interval1:f7 -> interval2:f7[style=invis] interval2:f0 -> interval4:f0 interval2:f2 -> interval4:f0 interval2:f4 -> interval4:f4 interval2:f6 -> interval4:f4 interval2:f7 -> interval4:f7[style=invis] interval4:f0 -> interval8:f0 interval4:f4 -> interval8:f0 interval4:f7 -> interval8:f7[style=invis] } ``` 因此,外層迴圈在更新 interval 時,也要變成 2 倍,以確保進入內層迴圈存取 lists 的正確位置。 ```c struct ListNode *mergeKLists(struct ListNode **lists, int listsSize) { if (listsSize == 0) return NULL; for (int interval = 1; interval < listsSize; interval *= 2) for (int i = 0; i + interval < listsSize; i += interval * 2) { lists[i] = mergeTwoLists(lists[i], lists[i + interval]); } return lists[0]; } ``` - [ ] Divide and Conquer 由於 lists 中的串列已排序,可視為 sorted element,直接進行 merge sort: ```c struct ListNode *mergeKLists(struct ListNode **lists, int listsSize) { if (!listsSize) return NULL; if (listsSize <= 1) return *lists; int m = listsSize >> 1; struct ListNode *left = mergeKLists(lists, m); struct ListNode *right = mergeKLists(lists + m, listsSize - m); return mergeTwoLists(left, right); } ``` ### 案例探討: [Leetcode 2095. Delete the Middle Node of a Linked List](https://leetcode.com/problems/delete-the-middle-node-of-a-linked-list/) [Leetcode 2095. Delete the Middle Node of a Linked List](https://leetcode.com/problems/delete-the-middle-node-of-a-linked-list/) 簡述: > You are given the head of a linked list. Delete the middle node, and return the head of the modified linked list. (給定一條鏈結串列,並刪除中間的節點) 若不考慮釋放掉中間的節點,可用指標的指標,搭配快慢指標的技巧來實作: ```c struct ListNode *deleteMiddle(struct ListNode *head) { struct ListNode **indir = &head; for (struct ListNode *fast = head; fast && fast->next; fast = fast->next->next) indir = &(*indir)->next; *indir = (*indir)->next; return head; } ``` 倘若要釋放中間的節點,可以再用一個指標 `prev` 跟隨於 `indir` 之後,當迴圈走訪完,`indir` 會指向鏈結串列中間的節點,之後 `prev` 再指向 `indir` 的下個節點,整個執行流程如下: ![](https://i.imgur.com/Qof9hCw.png) 對應的程式碼: ```c struct ListNode *deleteMiddle(struct ListNode *head) { if (!head->next) return NULL; struct ListNode **indir = &head, *prev = NULL; for (struct ListNode *fast = head; fast && fast->next; fast = fast->next->next) { prev = *indir; indir = &(*indir)->next; } prev->next = (*indir)->next; free(*indir); return head; } ``` 上述的程式碼看似正確,一旦提交後則會被 [Leetcode](https://leetcode.com/) 內建的 [Address Sanitizer](https://github.com/google/sanitizers) 偵測到 [heap-use-after-free](https://github.com/google/sanitizers/wiki/AddressSanitizerExampleUseAfterFree)。 考慮給定的鏈結串列為 `[1, 3, 4, 7, 1, 2, 6]`,上述的程式碼在迴圈結束後,`*indir` 會指向內容為 `7` 的節點 (以下記做 node~7~),`prev` 緊隨其後指向內容為 `4` 的節點 (以下記做 node~4~),換言之,`prev->next` 就是 `*indir`。 找出 `indir` 跟 `prev` 所指向的節點與關係後,可知 `prev->next = (*indir)->next;` 相當於 `(*indir) = (*indir)->next;`,即 `*indir` 從 node~4~ 指向 node~1~。 查閱 [C99 規格書](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1124.pdf#page=44) 6.2.4.2 > If an object is referred to outside of its lifetime, the behavior is undefined. The value of a pointer becomes indeterminate when the object it points to (or just past) reaches the end of its lifetime. 因此 `free(*indir)` 會釋放 node~1~ 對應的記憶體,因此在驗證階段時,會被偵測到 [heap-use-after-free](https://github.com/google/sanitizers/wiki/AddressSanitizerExampleUseAfterFree)。 > 延伸閱讀 [你所不知道的 C 語言:指標篇](https://hackmd.io/@sysprog/c-pointer) 若要修正這個問題,可藉由新的指標 `next` 來保存 `(*indir)->next`,方可在 `indir` 釋放掉後再以 `next` 更新。 ```c struct ListNode *deleteMiddle(struct ListNode *head) { if (!head->next) return NULL; struct ListNode **indir = &head, *prev = NULL; for (struct ListNode *fast = head; fast && fast->next; fast = fast->next->next) { prev = *indir; indir = &(*indir)->next; } struct ListNode *next = (*indir)->next; free(*indir); prev->next = next; // *indir = next return head; } ``` 倘若要釋放中間的節點,需額外用一個指標記錄欲釋放的物件。執行完 for 迴圈後,鏈結串列和 `indir` 如下圖。其中 `indir` 指向的是指向 node~7~ 的指標 (即 node~4~ 的 `next` 指標): ```graphviz digraph g{ rankdir=LR; node [shape=record]; 4 [label = "{<data>4 |<ref>}"]; 7 [label = "{<data>7 |<ref>}"]; 1 [label = "{<data>1 |<ref>}"]; node [shape=none] none1 [label = ""]; none2 [label = ""]; indir; edge[weight=2]; none1 -> 4 [arrowhead=vee]; 4:ref:c -> 7 [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; 7:ref:c -> 1 [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; 1:ref:c -> none2 [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; edge[weight=1]; indir -> 4:ref; } ``` 當我們要從鏈結串列中移去並釋放 node~7~,只要用一個指標 `del` 指向 node~7~ (即 `del = *indir`),並將 node~4~ 的 `next` 指標更改為指向 node~1~ (即 `*indir = (*indir)->next`),示意圖如下: ```graphviz digraph g{ rankdir=LR; node [shape=record]; 4 [label = "{<data>4 |<ref>}"]; 7 [label = "{<data>7 |<ref>}"]; 1 [label = "{<data>1 |<ref>}"]; node [shape=none] none1 [label = ""]; none2 [label = ""]; indir; del; edge[weight=2]; none1 -> 4 [arrowhead=vee]; 4:ref:c -> 1 [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; 1:ref:c -> none2 [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; edge[weight=1]; indir -> 4:ref del -> 7; 7:ref:c -> 1 [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; } ``` 最後再釋放 node~7~ 所在的空間,對應的程式碼如下: ```c struct ListNode *deleteMiddle(struct ListNode *head) { struct ListNode **indir = &head; for (struct ListNode *fast = head; fast && fast->next; fast = fast->next->next) indir = &(*indir)->next; struct ListNode *del = *indir; *indir = (*indir)->next; free(del); return head; } ``` ### 案例探討: [LeetCode 86. Partition List](https://leetcode.com/problems/partition-list/) >Given the head of a linked list and a value x, partition it such that all nodes less than x come before nodes greater than or equal to x. > >You should preserve the original relative order of the nodes in each of the two partitions. > (給定一個鏈結串列跟整數 x,將串列分割成兩組,比 x 小的節點置前,大於或等於 x 的節點置後,應維持分割前的順序) 例如輸入為 `[1, 4, 3, 2, 5, 2]`, x = 3 ```graphviz digraph input_list { rankdir=LR; node[shape=record,arrowhead=vee] 1 [label="{<data> 1| <ref>}" ] 4 [label="{<data> 4| <ref>}"] 3 [label="{<data> 3| <ref>}"] 2 [label="{<data> 2| <ref>}"] 5 [label="{<data> 5| <ref>}"] node2 [label="{<data> 2| <ref>}"] empty [label="NULL", shape=none] edge[weight=2]; 1:ref:c -> 4 [arrowtail=dot, tailclip=false, dir=both] 4:ref:c -> 3 [arrowtail=dot, tailclip=false, dir=both] 3:ref:c -> 2 [arrowtail=dot, tailclip=false, dir=both] 2:ref:c -> 5 [arrowtail=dot, tailclip=false, dir=both] 5:ref:c -> node2 [arrowtail=dot, tailclip=false, dir=both] node2:ref:c -> empty } ``` 輸出 `[1, 2, 2, 4, 3, 5]` ```graphviz digraph output_list { rankdir=LR; node[shape=record,arrowhead=vee] 1 [label="{<data> 1| <ref>}"] 4 [label="{<data> 4| <ref>}"] 3 [label="{<data> 3| <ref>}"] 2 [label="{<data> 2| <ref>}"] 5 [label="{<data> 5| <ref>}"] node2 [label="{<data> 2| <ref>}"] empty [label="NULL", shape=none] edge[weight=2]; 1:ref:c -> 2 [arrowtail=dot, tailclip=false, dir=both] 2:ref:c -> node2 [arrowtail=dot, tailclip=false, dir=both] node2:ref:c -> 4 [arrowtail=dot, tailclip=false, dir=both] 4:ref:c -> 3 [arrowtail=dot, tailclip=false, dir=both] 3:ref:c -> 5 [arrowtail=dot, tailclip=false, dir=both] 5:ref:c -> empty } ``` > 第一個節點內容為 `1`,小於 `x` 的值,而後續的節點對應的數值均大於或等於 `x` 的值 直觀的做法是配置兩個暫時節點分別保存「節點的數值小於 `x`」和「節點的數值大於等於 `x`」的鏈結串列,分割後再以前者 (即節點數值均「小於 `x`」的鏈結串列) 合併另一者。 注意,「大於等於 `x`」的鏈結串列要將最後一個節點的 `next` 指向 `NULL`,例如範例的 node~5~ 的下個節點指向 node~2~,如果沒有將 node~5~ 的下一個指向 `NULL`,會在執行時期陷入無窮迴圈,從而被 LeetCode 驗證系統判定為超時。 ```c static struct ListNode *new_node() { struct ListNode *node = malloc(sizeof(struct ListNode)); node->val = 0, node->next = NULL; return node; } struct ListNode *partition(struct ListNode *head, int x) { struct ListNode *l1 = new_node(), *l2 = new_node(); struct ListNode *ptr1 = l1, *ptr2 = l2; for (; head; head = head->next) { if (head->val < x) { ptr1->next = head; ptr1 = ptr1->next; } else { ptr2->next = head; ptr2 = ptr2->next; } } ptr2->next = NULL; ptr1->next = l2->next; return l1->next; } ``` 我們可運用上述間接指標的技巧,分別指向兩條要合併的串列,從而避免動態記憶體配置: ```c struct ListNode *partition(struct ListNode *head, int x) { struct ListNode *l2 = NULL; struct ListNode **p1 = &head, **p2 = &l2; for (struct ListNode *node = head; node; node = node->next) { if (node->val < x) { *p1 = node; p1 = &(*p1)->next; } else { *p2 = node; p2 = &(*p2)->next; } } *p1 = l2; *p2 = NULL; return head; } ``` 不難發現,在 `if-else` 中的 p1 跟 p2 是一樣的操作,且 p1 跟 p2 都是指標的指標,可否進一步精簡程式碼?這時又可利用「指標的指標的指標」來簡化程式碼,但要注意 dereference 的型態是「指標」,抑或是「指標的指標」。 宣告「指標的指標的指標」`indir` 指向要被更新節點,如 `p1`,再 dereference 兩次 (`**indir`) 就會得到指標型態的 `p1` 所指向的節點並將 `node` 指派到 `p1`。 更新 p1 到下個節點前,應進行 dereference (`*indir`) 以取得指標的指標 p1,再透過 dereference indir 兩次並取得 next 後用 address-of 運算子得到指標的指標,再指派回 p1 即可完成更新。 ```c struct ListNode *partition(struct ListNode *head, int x) { struct ListNode *l2 = NULL; struct ListNode **p1 = &head, **p2 = &l2; for (struct ListNode *node = head; node; node = node->next) { struct ListNode ***indir = node->val < x ? (&p1): (&p2); **indir = node; *indir = &(**indir)->next; } *p1 = l2; *p2 = NULL; return head; } ``` | 表示式 | dereference | 說明 | | ---- | --- | --- | | `***indir = &p1;` | `p1` | 假設比較完 `node->val` 跟 `x`,`indir` 指向 `p1` | | `**indir = node;` | `(*p1) = node;` | 將小於 x 的節點更新到 `p1` | | `*indir = &(**indir)->next;` | `p1 = &(*p1)->next;` | 將 `p1` 指向 `p1->next` 讓下輪迴圈更新 `p1` | ## circular linked list 環狀鏈結串列 (circular linked list) 是鏈結串列的最後一個節點所指向的下一個節點,會是第一個節點,而不是指向 `NULL`: ![](https://hackmd.io/_uploads/Bk4-bly2K.png) 其優點為: * 從 head 找到 tail 的時間複雜度為 $O(n)$,但若新增一個 tail pointer (此為 last) 時間複雜度可降為 $O(1)$ > [示意動畫](https://youtu.be/kErHUGvFrNg) * 容易做到反向查詢 * 若要走訪整個 linked list,任何節點都可作為起始節點 * 避免保留 `NULL` 這樣特別的記憶體地址 (在沒有 MMU 的 [bare metal](https://en.wikipedia.org/wiki/Bare_machine) 環境中,`(void *) 0` 地址空間存取時,沒有特別的限制) 用「龜兔賽跑」([Floyd's Cycle detection](https://en.wikipedia.org/wiki/Cycle_detection))來偵測是否有 cycle 產生。 有 3 種狀態需要做討論 > * $a$ 為起始點 > * $b$ 為連接點 > * $c$ 為龜兔相遇位置 - [ ] 狀況 1: cycle 在中間 ```graphviz digraph { rankdir="LR"; empty1[label="",shape="circle", style="bold, filled", fillcolor="#ffffff"] empty2[label="",shape="circle", style="bold, filled", fillcolor="#ffffff"] empty3[label="",shape="circle", style="bold, filled", fillcolor="#ffffff"] node [ shape="circle", style="bold, filled", fillcolor="#dddddd" ]; a -> empty1 -> b; { rank="same"; b -> empty2; } empty3->c->empty2[dir=back] b->empty3[dir=back,rank=same, constraint="false"] } ``` - [ ] 狀況 2: 頭尾相連 ```graphviz digraph { rankdir="LR"; empty1[label="",shape="circle", style="bold, filled", fillcolor="#ffffff"] empty2[label="",shape="circle", style="bold, filled", fillcolor="#ffffff"] empty3[label="",shape="circle", style="bold, filled", fillcolor="#ffffff"] empty4[label="",shape="circle", style="bold, filled", fillcolor="#ffffff"] empty5[label="",shape="circle", style="bold, filled", fillcolor="#ffffff"] node [ shape="circle", style="bold, filled", fillcolor="#dddddd",fixedsize=true,width=0.53 ]; abc -> empty1 -> empty2; { rank="same"; empty2 -> empty3; } empty5->empty4->empty3[dir=back] abc->empty5[dir=back,rank=same, constraint="false"] } ``` - [ ] 狀況 3: 尾尾相連 ```graphviz digraph { rankdir="LR"; empty1[label="",shape="circle", style="bold, filled", fillcolor="#ffffff"] empty2[label="",shape="circle", style="bold, filled", fillcolor="#ffffff"] empty3[label="",shape="circle", style="bold, filled", fillcolor="#ffffff"] empty4[label="",shape="circle", style="bold, filled", fillcolor="#ffffff"] node [ shape="circle", style="bold, filled", fillcolor="#dddddd",fixedsize=true,width=0.53 ]; a -> empty1 -> empty2; { rank="same"; empty2 -> empty3; } bc->empty4->empty3[dir=back] bc->bc } ``` 我們需要求得 a, b, c 三點位置,才能進行處理。 假設 $\overline{ac}$ 距離為 $X$ ,這代表 tortoise 行經 $X$ 步,那麼 hare 走了 $2X$ 步,$X$ 數值為多少並不重要,只代表要花多少時間兩點才會相遇,不影響求出 $\mu$ 和 $\lambda$。 接下來要分成三個步驟來處理 1. tortoise 速度為每次一步,hare 為每次兩步,兩者同時從起點 $a$ 出發,相遇時可以得到點 $c$。若是上述「狀況 2: 頭尾相連」,在第 1 步結束就求完三點了 2. 兩點分別從點 $a$ 和 $c$ 出發,速度皆為一次一步,相遇時可得到點 $b$。因為 $\overline{ac}$ 長度為 $X$,那麼 $cycle$ $c$ 長度也為 $X$,相遇在點 $b$ 時,所走的距離剛好都是 $X - \overline{bc}$ 3. 從點 $b$ 出發,速度為一次一步,再次回到點 $b$ 可得到 cycle 的長度 - [ ] cycle finding 如果只需要判斷是否為 circular linked list,那麼只要執行上述的第 1 部分。 除了計算 $\mu$ 和 $\lambda$,還需要記錄整個串列的長度,若不記錄,會影響到後續進行 sorting 一類的操作。 ```c static inline Node *move(Node *cur) { return cur ? cur->next : NULL; } bool cycle_finding(Node *HEAD, Node **TAIL, int *length, int *mu, int *lambda) { // lambda is length // mu is the meet node's index Node *tortoise = move(HEAD); Node *hare = move(move(HEAD)); // get meet point while (hare && tortoise) { /* Maybe while (hare && tortoise && (hare != tortoise)) ?*/ tortoise = move(tortoise); hare = move(move(hare)); } // not loop if (!hare) { *TAIL = NULL; *length = 0; tortoise = HEAD; while (tortoise && (tortoise = move(tortoise))) (*length)++; return false; } // get mu *mu = 0; tortoise = HEAD; while (tortoise != hare) { (*mu)++; tortoise = tortoise->next; hare = hare->next; } // get lambda *lambda = 1; tortoise = move(tortoise); *TAIL = tortoise; while (tortoise != hare) { *TAIL = tortoise; (*lambda)++; tortoise = move(tortoise); } *length = *mu + *lambda; return true; } ``` > 延伸閱讀: [探索 Floyd Cycle Detection Algorithm](https://medium.com/@orionssl/%E6%8E%A2%E7%B4%A2-floyd-cycle-detection-algorithm-934cdd05beb9) LeetCode 相關題目: * [141. Linked List Cycle](https://leetcode.com/problems/linked-list-cycle/) * [142. Linked List Cycle II](https://leetcode.com/problems/linked-list-cycle-ii/) * [146. LRU Cache](https://leetcode.com/problems/lru-cache/) * [金刀的演算法手册](https://github.com/glodknife/algorithm) ## Merge Sort 的實作 Merge sort 是經典排序演算法,以串列為例,將串列分割成排序好的子串列,再將所有排序好的子串列合併在一起。 ```graphviz digraph G { compound=true node[shape=box, height=.1];sorted_1 sorted_2 sorted_3 sorted_4 sorted_5 sorted_6 sorted_7 sorted_8; node[shape=record, height=.1];input result divide_41 divide_42 divide_21 divide_22 divide_23 divide_24 merge_21 merge_22 merge_23 merge_24 merge_41 merge_42; input[label="<f0>2|<f1>5|<f2>4|<f3>6|<f4>8|<f5>1|<f6>7|<f7>3"] result[label="<f0>1|<f1>2|<f2>3|<f3>4|<f4>5|<f5>6|<f6>7|<f7>8"] divide_41[label="<f1>2|<f2>5|<f3>4|<f4>6"] divide_42[label="<f1>8|<f2>1|<f3>7|<f4>3"] divide_21[label="<f0>2|<f1>5"] divide_22[label="<f0>4|<f1>6"] divide_23[label="<f0>8|<f1>1"] divide_24[label="<f0>7|<f1>3"] sorted_1[label="1"] sorted_2[label="2"] sorted_3[label="3"] sorted_4[label="4"] sorted_5[label="5"] sorted_6[label="6"] sorted_7[label="7"] sorted_8[label="8"] merge_21[label="<f0>2|<f1>5"] merge_22[label="<f0>4|<f1>6"] merge_23[label="<f0>1|<f1>8"] merge_24[label="<f0>3|<f1>7"] merge_41[label="<f1>2|<f2>4|<f3>5|<f4>6"] merge_42[label="<f1>1|<f2>3|<f3>7|<f4>8"] subgraph cluster_0 { style=filled; color="#ef6548"; label="divide"; divide_pad[label="----------------------", style=invis] divide_pad -> divide_23[style=invis] input -> divide_41 input -> divide_42 divide_41 -> divide_21 divide_41 -> divide_22 divide_42 -> divide_23 divide_42 -> divide_24 } divide_21:f0 -> sorted_2 divide_21:f1 -> sorted_5 divide_22 -> sorted_4 divide_22 -> sorted_6 divide_23:f0 -> sorted_8 divide_23:f1 -> sorted_1 divide_24:f0 -> sorted_7 divide_24:f1 -> sorted_3 subgraph cluster_1 { style=filled; color="#a6cee3"; label="sorted lists"; sorted_1; sorted_2; sorted_3; sorted_4; sorted_5; sorted_6; sorted_7; sorted_8; } sorted_2 -> merge_21:f0 sorted_5 -> merge_21:f1 sorted_4 -> merge_22:f0 sorted_6 -> merge_22:f1 sorted_8 -> merge_23:f1[constraint=false] sorted_1 -> merge_23:f0 sorted_7 -> merge_24:f1 sorted_3 -> merge_24:f0 subgraph cluster_2 { style=filled; color="#b2df8a"; label="merge"; merge_pad[label="--------------------", style=invis] rank=same; merge_41; merge_pad; merge_42 rank=same; merge_41; merge_42; merge_pad; merge_22 -> merge_pad[style=invis] merge_23 -> merge_pad[style=invis] merge_pad -> result[style=invis] merge_21 -> merge_41 merge_22 -> merge_41 merge_23 -> merge_42 merge_24 -> merge_42 merge_41 -> result merge_42 -> result } } ``` 採用遞迴呼叫,搭配前述 slow-fast 指標將串列左右對半切分,直到分割成單一節點再合併成排序好的串列,對應實作如下: ```c static node_t *mergesort_list(node_t *head) { if (!head || !head->next) return head; node_t *slow = head; for (node_t *fast = head->next; fast && fast->next; fast = fast->next->next) slow = slow->next; node_t *mid = slow->next; slow->next = NULL; node_t *left = mergesort_list(head), *right = mergesort_list(mid); return mergeTwoLists(left, right); } void mergesort(node_t **list) { *list = mergesort_list(*list); } ``` ### 非遞迴的實作 如何將上述 merge sort 從遞迴轉成迭代?可善用之前探討過的 [merge K Lists](https://leetcode.com/problems/merge-k-sorted-lists/) 程式碼。 - [ ] 原始的迭代版 merge sort ```c void mergesort_iter(node_t **list) { node_t *head = *list; if (!head || !head->next) return; node_t *result = NULL; node_t *stack[1000]; int top = 0; stack[top] = head; while (top >= 0) { node_t *left = stack[top--]; if (left) { node_t *slow = left; node_t *fast; for (fast = left->next; fast && fast->next; fast = fast->next->next) slow = slow->next; node_t *right = slow->next; slow->next = NULL; stack[++top] = left; stack[++top] = right; } else result = mergeTwoLists(result, stack[top--]); } *list = result; } ``` naive 是將單一節點逐個合併,速度非常慢,所以改成將分割好的節點存進指標陣列 lists 改成頭尾合併來改善效能。 - [ ] 初步改進 ```c void mergesort_iter(node_t **list) { node_t *head = *list; if (!head || !head->next) return; int top = 0; int listsSize = 0; node_t *stack[1000] = {NULL}; node_t *lists[100000] = {NULL}; stack[top] = head; // divide to single node while (top >= 0) { node_t *left = stack[top--]; if (left) { node_t *slow = left; node_t *fast; for (fast = left->next; fast && fast->next; fast = fast->next->next) slow = slow->next; node_t *right = slow->next; slow->next = NULL; stack[++top] = left; stack[++top] = right; } else lists[listsSize++] = stack[top--]; } // merge K sorted lists while (listsSize > 1) { for (int i = 0, j = listsSize - 1; i < j; i++, j--) lists[i] = mergeTwoLists(lists[i], lists[j]); listsSize = (listsSize + 1) / 2; } *list = lists[0]; } ``` 觀察初步改進後的實作,可將迭代版 merge sort 拆成**分割階段**與**合併階段**來實作並持續改進,進而延伸出各種組合,接下來分別探討兩種階段的實作。 回顧 merge sort 的概念,將串列分割成排序好的子串列,再將所有排序好的子串列合併在一起。 * 分割 $\to$ 將 list 分割成 sorted lists * 合併 $\to$ 將 sorted lists 合併在一起 > 延伸閱讀: [Merge Sort 與它的變化](https://hackmd.io/@lambert-wu/list-merge-sort) ## Linked list 在 Linux 核心原始程式碼 [linux/list.h](https://github.com/torvalds/linux/blob/master/include/linux/list.h) 是 Linux 核心中相當實用的 circular doubly-linked list (雙向環狀鏈結串列,以下簡稱 `list`) 封裝,只要在自定義的結構中加入 `struct list_head`,就可以搭配 Linux 中一系列的 linked list 操作 (詳見[The Linux Kernel API - List Management Functions](https://www.kernel.org/doc/html/latest/core-api/kernel-api.html)) 來建立自定義結構的 linked list。在 Linux 行程 (process) 管理的相關實作中可見到應用,例如 [sched.h](https://github.com/torvalds/linux/blob/v5.15/include/linux/sched.h) 中約出現 20 處,程式碼部分摘錄如下: ```c=527 struct sched_entity { /* For load-balancing: */ struct load_weight load; struct rb_node run_node; struct list_head group_node; unsigned int on_rq; ... }; ``` list 的關鍵概念是,將結構體 `list_head` 嵌入到所需的結構體中,再藉由稍後會提及的 `container_of` 巨集得知 list 個別節點的地址。示意如下圖: ![](https://hackmd.io/_uploads/ByHFSCRit.png) 自 `Head` 開始,鏈結 list 各節點,個別節點皆嵌入 `list_head` 結構體,不過 `Head` 是個特例,無法藉由 `container_of` 巨集來找到對應的節點,因為後者並未嵌入到任何結構體之中,其存在是為了找到 list 整體。 好處在於,只要 `list_head` 納入新的結構體的一個成員,即可操作,且不用自行維護一套 doubly-linked list。 ![](https://hackmd.io/_uploads/rJwcLAAjK.png) > 參考: [Intrusive linked lists](https://www.data-structures-in-practice.com/intrusive-linked-lists/) > [FreeRTOS](http://wiki.csie.ncku.edu.tw/embedded/freertos) 的任務管理也採用 linked list 圖解如下: - [ ] `list_head` 結構體 ```c struct list_head { struct list_head *prev, *next; }; ``` ```graphviz digraph list_head { rankdir=LR; node[shape=record, style=bold]; head [label="{<prev>prev|<next>next}"]; } ``` - [ ] 在自行定義的結構體置入 `list_head` 物件 ```c typedef struct { char *value; struct list_head list; } my_data; ``` ```graphviz digraph list { rankdir=LR; node[shape=record, style=bold]; subgraph cluster_0 { node [shape=record]; value [label="{value}"]; head [label="{<prev>prev|<next>next}"]; style=bold; label=my_data } } ``` 簡化為下圖: ```graphviz digraph list_ele { rankdir=LR; node[shape=record]; node [shape=record]; head [label="value|{<left>prev|<right>next}", style=bold]; } ``` - [ ] `LIST_HEAD - Declare list head and initialize it` ```c #define LIST_HEAD(head) struct list_head head = {&(head), &(head)} ``` ```graphviz digraph list_init { rankdir=LR; node[shape=record]; style=bold node [shape=record]; head [label="{<left>prev|<right>next}", style="bold"]; head:right:e -> head:left:w[arrowhead=normal, arrowtail=normal, dir=both]; } ``` - [ ] `list_entry() - Calculate address of entry that contains list node` ```c #define list_entry(node, type, member) container_of(node, type, member) ``` ```graphviz digraph container { rankdir=LR; node[shape=record]; compound=true; style=bold; subgraph cluster_0 { container [label = "container" style=filled,color=white]; subgraph cluster_1 { node [shape=record]; element [label="|{|}", style="bold"]; label = "member" } } element -> container[ltail=cluster_1, lhead=cluster_0]; } ``` - [ ] `list_for_each - iterate over list nodes` ```c #define list_for_each(node, head) \ for (node = (head)->next; node != (head); node = node->next) ``` ```graphviz digraph ele_list { rankdir=LR; node[shape=record]; h [label="head", style=dashed, color=grey]; h -> e1:right:w [style=dashed, color=grey]; e1 [label="head node|{<left>prev|<right>next}", style="bold", color=grey]; e2 [label="cat|{<left>prev|<right>next}", style="bold"]; e3 [label="...", style="bold"]; e4 [label="eat|{<left>prev|<right>next}", style="bold"]; e5 [label="fat|{<left>prev|<right>next}", style="bold"]; e1:right:e -> e2:left:w[arrowhead=normal, arrowtail=normal, dir=both]; e2:right:e -> e3:left:w[arrowhead=normal, arrowtail=normal, dir=both]; e3:right:e -> e4:left:w[arrowhead=normal, arrowtail=normal, dir=both]; e4:right:e -> e5:left:w[arrowhead=normal, arrowtail=normal, dir=both]; e5:right:e -> e1:left:w[arrowhead=normal, arrowtail=normal, dir=both]; } ``` Linux 核心的 `list` 也在 [hash table](https://en.wikipedia.org/wiki/Hash_table) 的實作中出現,詳見 [List, HList, and Hash Table](https://danielmaker.github.io/blog/linux/list_hlist_hashtable.html)。 > 延伸閱讀: [hash table](https://hackmd.io/@ChialiangKuo/quiz6B-hash-table) ### 簡化的實作 Linux 核心原始程式碼變動快,為了後續探討方便,我們自 Linux 核心抽離出關鍵程式碼,建立 [sysprog21/linux-list](https://github.com/sysprog21/linux-list) 專案,讓學員得以理解箇中奧秘並進行相關實驗。 #### `container_of()` 為了提升程式碼的可攜性 (portability),C89/C99 提供 [offsetof](https://man7.org/linux/man-pages/man3/offsetof.3.html) 巨集,可接受給定成員的型態及成員的名稱,傳回「成員的位址減去 struct 的起始位址」。示意如下: ![macro:offsetof](https://hackmd.io/_uploads/SkfkiRJ2K.png) [offsetof](https://man7.org/linux/man-pages/man3/offsetof.3.html) 定義於 `<stddef.h>`。 巨集 `container_of` 則在 [offsetof](https://man7.org/linux/man-pages/man3/offsetof.3.html) 的基礎上,擴充為「給定成員的位址、struct 的型態,及成員的名稱,傳回此 struct 的位址」,示意如下圖: ![macro:container_of](https://hackmd.io/_uploads/rJcei0k2F.png) 在 `container_of` 巨集出現前,程式設計的思維往往是: 1. 給定結構體起始地址 2. 求出結構體特定成員的記憶體內容 3. 傳回結構體成員的地址,作日後存取使用 `container_of` 巨集則逆轉上述流程,例如 `list_entry` 巨集利用 `container_of` 巨集,從 `struct list_head` 這個公開介面,「反向」去存取到自行定義的結構體開頭地址。 > 延伸閱讀: [Linux 核心原始程式碼巨集: `container_of`](https://hackmd.io/@sysprog/linux-macro-containerof) #### `LIST_HEAD()` / `INIT_LIST_HEAD()` ```c #define LIST_HEAD(head) struct list_head head = {&(head), &(head)} static inline void INIT_LIST_HEAD(struct list_head *head) { head->next = head; head->prev = head; } ``` 初始化 `struct list_head`,先將 `next` 和 `prev` 都指向自身。`head` 指向的結構體之中的 `next` 成員表示 linked list 結構的開頭,而 `prev` 則指向結構體的結尾。 #### `list_add` / `list_add_tail` ```c static inline void list_add(struct list_head *node, struct list_head *head) { struct list_head *next = head->next; next->prev = node; node->next = next; node->prev = head; head->next = node; } static inline void list_add_tail(struct list_head *node, struct list_head *head) { struct list_head *prev = head->prev; prev->next = node; node->next = head; node->prev = prev; head->prev = node; } ``` 將指定的 `node` 插入 linked list `head` 的開頭或者結尾。 #### `list_del` ```c static inline void list_del(struct list_head *node) { struct list_head *next = node->next; struct list_head *prev = node->prev; next->prev = prev; prev->next = next; #ifdef LIST_POISONING node->prev = (struct list_head *) (0x00100100); node->next = (struct list_head *) (0x00200200); #endif } ``` 本函式將 `node` 從其所屬的 linked list 結構中移走。注意 `node` 本體甚至是包含 `node` 的結構所分配的記憶體,在此皆未被釋放,僅僅是將 `node` 從其原本的 linked list 連接移除。亦即,使用者需自行管理記憶體。 `LIST_POISONING` 巨集一旦定義,將讓已被移走的 `node` 節點再存取時,觸發作業系統的例外狀況:對於 `next` 或者 `prev` 的存取,會觸發執行時期的 invalid memory access (若系統禁止 predefined memory access)。 #### `list_del_init` ```c static inline void list_del_init(struct list_head *node) { list_del(node); INIT_LIST_HEAD(node); } ``` 以 `list_del` 為基礎,但移除的 `node` 會額外呼叫 `INIT_LIST_HEAD` 把 `prev` 和 `next` 指向自身。 #### `list_empty` ```c static inline int list_empty(const struct list_head *head) { return (head->next == head); } ``` 檢查 `head` 的 `next` 是否指向自身,確認 list 是否為 empty 狀態。 #### `list_is_singular` ```c static inline int list_is_singular(const struct list_head *head) { return (!list_empty(head) && head->prev == head->next); } ``` 若 `head` 非 empty 狀態且 `prev` 和 `next` 是同一個節點,表示 linked list 只有一個節點。 #### `list_splice` / `list_splice_tail` ```c static inline void list_splice(struct list_head *list, struct list_head *head) { struct list_head *head_first = head->next; struct list_head *list_first = list->next; struct list_head *list_last = list->prev; if (list_empty(list)) return; head->next = list_first; list_first->prev = head; list_last->next = head_first; head_first->prev = list_last; } static inline void list_splice_tail(struct list_head *list, struct list_head *head) { struct list_head *head_last = head->prev; struct list_head *list_first = list->next; struct list_head *list_last = list->prev; if (list_empty(list)) return; head->prev = list_last; list_last->next = head; list_first->prev = head_last; head_last->next = list_first; } ``` 將 `list` 的所有 node 都插入到 `head` 的開始 / 結束位置中。注意 `list` 本身仍維持原貌。 #### `list_splice_init` / `list_splice_tail_init` ```c static inline void list_splice_init(struct list_head *list, struct list_head *head) { list_splice(list, head); INIT_LIST_HEAD(list); } static inline void list_splice_tail_init(struct list_head *list, struct list_head *head) { list_splice_tail(list, head); INIT_LIST_HEAD(list); } ``` 這二個函式類似 `list_splice` 及 `list_splice_tail`,但移除的 `list` 會額外呼叫 `INIT_LIST_HEAD` 把 `prev` 和 `next` 指向自身。 #### `list_cut_position` ```c static inline void list_cut_position(struct list_head *head_to, struct list_head *head_from, struct list_head *node) { struct list_head *head_from_first = head_from->next; if (list_empty(head_from)) return; if (head_from == node) { INIT_LIST_HEAD(head_to); return; } head_from->next = node->next; head_from->next->prev = head_from; head_to->prev = node; node->next = head_to; head_to->next = head_from_first; head_to->next->prev = head_to; } ``` 將從 `head_from` 的第一個節點到 `node` 間的一系列節點都移動到 `head_to` 上。`head_to` 必須是 empty 狀態 (`next` 和 `prev` 都指向自己),否則可能發生 memory leak。 #### `list_move` / `list_move_tail` ```c static inline void list_move(struct list_head *node, struct list_head *head) { list_del(node); list_add(node, head); } static inline void list_move_tail(struct list_head *node, struct list_head *head) { list_del(node); list_add_tail(node, head); } ``` 將 `node` 從原本的 linked list 移動到另一個 linked list `head` 的開頭或尾端。 #### `list_entry` ```c #define list_entry(node, type, member) container_of(node, type, member) ``` `container_of` 等價的包裝,符合以 `list_` 開頭的命名慣例,此處的 entry 就是 list 內部的節點。 #### `list_first_entry` / `list_last_entry` ```c #define list_first_entry(head, type, member) \ list_entry((head)->next, type, member) #define list_last_entry(head, type, member) \ list_entry((head)->prev, type, member) ``` 取得 linked list 的開頭或者結尾的 entry。 #### `list_for_each` ```c #define list_for_each(node, head) \ for (node = (head)->next; node != (head); node = node->next) ``` 走訪整個 linked list。注意: `node` 和 `head` 不能在迴圈中被更改 (可能在多工環境中出現),否則行為不可預期。 #### `list_for_each_entry` ```c #ifdef __LIST_HAVE_TYPEOF #define list_for_each_entry(entry, head, member) \ for (entry = list_entry((head)->next, __typeof__(*entry), member); \ &entry->member != (head); \ entry = list_entry(entry->member.next, __typeof__(*entry), member)) #endif ``` 走訪包含 `struct list_head` 的另外一個結構之 entry。`node` 和 `head` 不能在迴圈中被更改,否則行為不可預期。 * 因為 `typeof` 之限制,只能在 GNUC 下使用 #### `list_for_each_safe` / `list_for_each_entry_safe` ```c #define list_for_each_safe(node, safe, head) \ for (node = (head)->next, safe = node->next; node != (head); \ node = safe, safe = node->next) #define list_for_each_entry_safe(entry, safe, head, member) \ for (entry = list_entry((head)->next, __typeof__(*entry), member), \ safe = list_entry(entry->member.next, __typeof__(*entry), member); \ &entry->member != (head); entry = safe, \ safe = list_entry(safe->member.next, __typeof__(*entry), member)) ``` 透過額外的 `safe` 紀錄每個迭代 (iteration) 所操作的節點的下一個節點,因此目前的節點可允許被移走,其他操作則同為不可預期行為。 ### Linux 核心風格 Linked List 應用案例 從一些範例來看 Linux 核心風格 linked list 實際的使用方式。 #### Quick sort (遞迴版本) 詳見 [sysprog21/linux-list](https://github.com/sysprog21/linux-list) 專案中的 [quick-sort.c](https://github.com/sysprog21/linux-list/blob/master/examples/quick-sort.c) 程式碼。 ```c static void list_qsort(struct list_head *head) { struct list_head list_less, list_greater; struct listitem *pivot; struct listitem *item = NULL, *is = NULL; if (list_empty(head) || list_is_singular(head)) return; INIT_LIST_HEAD(&list_less); INIT_LIST_HEAD(&list_greater); ``` 先確認 `head` 所承載的 linked list 有兩個以上 entry,否則就返回,不用排序。以 `INIT_LIST_HEAD` 初始化另外兩個 list 結構,它們分別是用來插入 entry 中比 pivot 小或者其他的節點。 ```c pivot = list_first_entry(head, struct listitem, list); list_del(&pivot->list); list_for_each_entry_safe (item, is, head, list) { if (cmpint(&item->i, &pivot->i) < 0) list_move_tail(&item->list, &list_less); else list_move_tail(&item->list, &list_greater); } ``` 藉由 `list_first_entry` 取得第一個 entry 選為 pivot: * `list_del` 將該 pivot entry 從 linked list 中移除 * 走訪整個 linked list,`cmpint` 回傳兩個指標中的值相減的數值,因此小於 `0` 意味著 `item->i` 的值比 `pivot` 的值小,加入 `list_less`,反之則同理 ```c list_qsort(&list_less); list_qsort(&list_greater); list_add(&pivot->list, head); list_splice(&list_less, head); list_splice_tail(&list_greater, head); } ``` 藉由遞迴呼叫將 `list_less` 和 `list_greater` 排序。在 `list_for_each_entry_safe` 中,`list_move_tail` 會將所有原本在 `head` 中的節點移出,因此首先 `list_add` 加入 `pivot`,再把已經排好的 `list_less` 放在 pivot 前,`list_greater` 放在 pivot 後,完成排序。 #### Quick sort (非遞迴) 參考 [Optimized QuickSort: C Implementation (Non-Recursive)](https://alienryderflex.com/quicksort/),嘗試實作非遞迴的 quick sort。 ```c static void list_qsort_no_recursive(struct list_head *head) { struct listitem *begin[MAX_LEN], *end[MAX_LEN], *L, *R; struct listitem pivot; int i = 0; begin[0] = list_first_entry(head, struct listitem, list); end[0] = list_last_entry(head, struct listitem, list); ``` `begin` 和 `end` 表示在 linked list 中的排序目標的開頭和結尾,因此最初是整個 linked list 的頭至尾。 ```c while (i >= 0) { L = begin[i]; R = end[i]; ``` `begin` 和 `end` 的效果類似 stack,會填入每一輪要處理的節點開頭至結尾 ,因此先取出該輪的頭尾至 `L` 和 `R`。 ```c if (L != R && &begin[i]->list != head) { // pivot is the actual address of L pivot = *begin[i]; if (i == MAX_LEN - 1) { assert(-1); return; } ``` 接著,以最開頭的節點作為 pivot。 * `i == MAX_LEN - 1` 的目的是額外檢查這輪如果填入 `begin` 和 `end` 是否會超出原本給定的陣列大小,因為我們所給予的空間是有限的 ```c while (L != R) { while (R->i >= pivot.i && L != R) R = list_entry(R->list.prev, struct listitem, list); if (L != R) { L->i = R->i; L = list_entry(L->list.next, struct listitem, list); } while (L->i <= pivot.i && L != R) L = list_entry(L->list.next, struct listitem, list); if (L != R) { R->i = L->i; R = list_entry(R->list.prev, struct listitem, list); } } ``` 否則的話,從結尾的點(`R`)一直往 `prev` 前進,找到比 `pivot` 值更小的節點的話就將其值移到開頭的 `L` 去。同理,從開頭的點(`L`)一直往 `next` 前進,找到比 `pivot` 值更大的節點的話,就將其值移到結尾的 `R` 去 * `L != R` 則負責判斷當 `L` 往 `next` 而 `R` 往 `prev` 移動碰在一起時,當 `L == R` 時,不再做上述的操作,離開迴圈 ```c L->i = pivot.i; begin[i + 1] = list_entry(L->list.next, struct listitem, list); end[i + 1] = end[i]; end[i++] = L; ``` 此時 `L` 所在地方是 `pivot` 的值應在的正確位置,因此將 `pivot` 的值填入 `L`。此時需要被處理的排序是 `pivot` 往後到結尾的一段,兩個點分別是 `L` 的 `next`,和這輪的 `end[i]` * 另一段則是 `pivot` 以前從原本的 `begin[i]` 到 `L` 一段 ```c } else i--; } } ``` 如果 `L == R` 或者 `&begin[i]->list == head`,表示此段 linked list 已經不需要再做處理,`i--` 類似於 pop stack 的操作。 ### Linux 核心原始程式碼 對比 [sysprog21/linux-list](https://github.com/sysprog21/linux-list) 與 [linux/list.h](https://github.com/torvalds/linux/blob/master/include/linux/list.h) 的實作,會發現最大的差異在於 `WRITE_ONCE` 巨集的使用。 我們可以比較 `list_add` 來探討差異的細節 - [ ] [sysprog21/linux-list](https://github.com/sysprog21/linux-list/blob/master/include/list.h#L94) ```c static inline void list_add(struct list_head *node, struct list_head *head) { struct list_head *next = head->next; next->prev = node; node->next = next; node->prev = head; head->next = node; } ``` - [ ] [linux/list.h](https://github.com/torvalds/linux/blob/master/include/linux/list.h#L84) ```c static inline void __list_add(struct list_head *new, struct list_head *prev, struct list_head *next) { if (!__list_add_valid(new, prev, next)) return; next->prev = new; new->next = next; new->prev = prev; WRITE_ONCE(prev->next, new); } static inline void list_add(struct list_head *new, struct list_head *head) { __list_add(new, head, head->next); } ``` 乍看之下,若將 `WRITE_ONCE(prev->next, new)` 替換成 `prev->next = new`,兩者一樣嗎? `WRITE_ONCE` 定義在 [linux/tools/include/linux/compiler.h](https://github.com/torvalds/linux/blob/master/tools/include/linux/compiler.h#L184) 路徑下,我們可以從註解中對其作用進行理解: > Prevent the compiler from merging or refetching reads or writes. The compiler is also forbidden from reordering successive instances of READ_ONCE and WRITE_ONCE, but only when the compiler is aware of some particular ordering. One way to make the compiler aware of ordering is to put the two invocations of READ_ONCE or WRITE_ONCE in different C statements. 藉由 `WRITE_ONCE` 和 `READ_ONCE` 的使用,可以避免編譯器合併、切割、甚至重排特定的記憶體讀寫操作。下面是 `WRITE_ONCE` 的定義: ```c #define WRITE_ONCE(x, val) \ ({ \ union { typeof(x) __val; char __c[1]; } __u = \ { .__val = (val) }; \ __write_once_size(&(x), __u.__c, sizeof(x)); \ __u.__val; \ }) ``` 我們可以再次看到 [statement expression](https://gcc.gnu.org/onlinedocs/gcc/Statement-Exprs.html) 技巧的應用。此外,可以看到 `WRITE_ONCE` 把 `val` 寫入 union 結構中,使其與一個 `char __c[1]` 共享空間。藉由以 union 為基礎的 [`type-punning`](https://en.wikipedia.org/wiki/Type_punning#Use_of_union) 技巧,可避免違反 strict aliasing 規則,使得 `__c` 能作為這個 union 的指標來使用,從而允許編譯器最佳化。 需注意 type punning 的方式有不同類型,而在 gcc 中,使用 union 進行 type punning 的方式被作為語言的擴展並合法(詳見 [gcc: nonbugs](https://gcc.gnu.org/bugs/#nonbugs)),該方式不會違反 gcc 的 [strict aliasing](https://en.wikipedia.org/wiki/Aliasing_(computing)#Conflicts_with_optimization) 規則。但在其他編譯器中則可能因為違反規則導致 undefined behavior。 > * [What is the strict aliasing rule?](https://stackoverflow.com/questions/98650/what-is-the-strict-aliasing-rule) > * [Unions and type-punning](https://stackoverflow.com/questions/25664848/unions-and-type-punning) 關鍵的 `__write_once_size` 的任務則是把 `__val` 寫入至 `x` 中,但通過巧妙的設計避免編譯器將其做錯誤的最佳化,細節請見下面的 `__write_once_size` 說明 ```c typedef __u8 __attribute__((__may_alias__)) __u8_alias_t; typedef __u16 __attribute__((__may_alias__)) __u16_alias_t; typedef __u32 __attribute__((__may_alias__)) __u32_alias_t; typedef __u64 __attribute__((__may_alias__)) __u64_alias_t; ``` 在深入 `__write_once_size` 之前,先來看看這個型別定義。 首先需要了解何謂 [aliasing](https://en.wikipedia.org/wiki/Aliasing_(computing)): 其所指為同一個位置可被多個 symbol 存取時,這種情形會造成編譯器最佳化的限制。根據 [Options That Control Optimization](https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html), `-fstrict-aliasing` 參數會讓編譯器假設程式撰寫者不會將相似類型 (例如 `int` 和 `unsigned int`) 以外的指標予以指向同一塊記憶體空間,以允許做更激進的最佳化,該參數在 `-O2`, `-O3`, `-Os` 下會預設開啟。 但我們可根據 [Specifying Attributes of Types](https://gcc.gnu.org/onlinedocs/gcc/Type-Attributes.html) 中所提到,使用 `__may_alias__` 告知編譯器此型別允許 aliasing 的發生,避免激進的最佳化導致非預期的程式行為,至於 Linux 這裡特別定義 `*_alias_t` 的目的可以參考註解: > Following functions are taken from kernel sources and break aliasing rules in their original form. > > While kernel is compiled with `-fno-strict-aliasing`, perf uses `-Wstrict-aliasing=3` which makes build fail under gcc 4.4. Using extra `__may_alias__` type to allow aliasing in this case. ```c static __always_inline void __write_once_size(volatile void *p, void *res, int size) { switch (size) { case 1: *(volatile __u8_alias_t *) p = *(__u8_alias_t *) res; break; case 2: *(volatile __u16_alias_t *) p = *(__u16_alias_t *) res; break; case 4: *(volatile __u32_alias_t *) p = *(__u32_alias_t *) res; break; case 8: *(volatile __u64_alias_t *) p = *(__u64_alias_t *) res; break; default: barrier(); __builtin_memcpy((void *)p, (const void *)res, size); barrier(); } } ``` 對於 1, 2, 4, 8 位元組的變數,可直接搭配 [`volatile`](https://en.wikipedia.org/wiki/Volatile_(computer_programming)) 提示編譯器不要最佳化這個寫入操作。`volatile` 所考慮的情境在於: 變數的值即使從程式中乍看不會改變,在每一次存取時實際則有可能被更動 (例如該變數可能指向某個 memory mapped I/O,會被硬體更動,或者被 signal handler 和主程式共用的全域變數),因此可避免寫入操作被錯誤最佳化。 對於其他長度的變數,則可以透過 memory barriers 搭配 `memcpy` 的方法來寫入。`barrier()` 如同其名稱是個屏障,告訴編譯器不能 `barrier()` 前的 load/store 移動到 `barrier()` 後,反之亦然。 值得一提的是,Linux 中採用這種 "volatile access" 而非直接將 object 標註為 volitile 屬性,在 [doc: volatile considered evil](https://lwn.net/Articles/233482/) 中有提及後者可能隱藏的問題。 延伸閱讀: * [並行程式設計: Atomics 操作](https://hackmd.io/@sysprog/concurrency-atomics) * [Nine ways to break your systems code using volatile](https://blog.regehr.org/archives/28) * [WRITE_ONCE in linux kernel lists](https://stackoverflow.com/questions/34988277/write-once-in-linux-kernel-lists) ### Linux 核心的 `list_sort` 實作 在 [linux/list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) 中,可見 Linux 特別針對系統層級的考量所設計的 linked list sort。接下來,嘗試探討其中的設計原理和可能的考量。 ```c /** * list_sort - sort a list * @priv: private data, opaque to list_sort(), passed to @cmp * @head: the list to sort * @cmp: the elements comparison function * ``` 先探討其中的實作內容。首先是需要的三個參數: * `priv` 是一個 pointer,可以用來傳輸 `cmp` 需要的參數 * `head` 要被排序的 list * `cmp` 是比較*自定義大小*的 function pointer ```c /*... * The comparison funtion @cmp must return > 0 if @a should sort after * @b ("@a > @b" if you want an ascending sort), and <= 0 if @a should * sort before @b *or* their original order should be preserved. It is * always called with the element that came first in the input in @a, * and list_sort is a stable sort, so it is not necessary to distinguish * the @a < @b and @a == @b cases. * * This is compatible with two styles of @cmp function: * - The traditional style which returns <0 / =0 / >0, or * - Returning a boolean 0/1. * The latter offers a chance to save a few cycles in the comparison * (which is used by e.g. plug_ctx_cmp() in block/blk-mq.c). * * A good way to write a multi-word comparison is:: * * if (a->high != b->high) * return a->high > b->high; * if (a->middle != b->middle) * return a->middle > b->middle; * return a->low > b->low; ``` 註解中說明了在比較 `a` `b` 時,如果 `a` 需置於 `b` 的之後,則 `cmp` 需回傳大於 0 的值。list_sort 是 [stable sort](https://en.wikipedia.org/wiki/Category:Stable_sorts),所以不必區分小於或等於 0 的分別。 最後則展示一個簡單的 multi-word comparision。 ```c /* This mergesort is as eager as possible while always performing at least * 2:1 balanced merges. Given two pending sublists of size 2^k, they are * merged to a size-2^(k+1) list as soon as we have 2^k following elements. * * Thus, it will avoid cache thrashing as long as 3*2^k elements can * fit into the cache. Not quite as good as a fully-eager bottom-up * mergesort, but it does use 0.2*n fewer comparisons, so is faster in * the common case that everything fits into L1. ``` 接著,註解中提到這裡的 merge sort 實作重點。方法會保持兩個要被 merge 的 list 至少是 `2 : 1` 的狀態 (較大的 list 至多是較小者的 2 倍),這可有效的降低比較的次數。list_sort 與一般的 fully-eager bottom-up mergesort 方法不同,後者只要出現兩個 $2^k$ 大小的 list,就會立刻被合併。而前者的方法是在出現兩個 $2^k$ 大小的 list 的時候,不立即合併,反之,一直等到下一個 $2^k$ 的 list 被蒐集起來時,前者的這兩個 linked list 才會被合併起來。只要這 `2 : 1` 的 list 可以完全被放到 cache 裡,就可避免 [cache thrashing](https://en.wikipedia.org/wiki/Thrashing_(computer_science))。 在 [lib/list_sort: Optimize number of calls to comparison function](https://www.mail-archive.com/linux-kernel@vger.kernel.org/msg1957556.html) 可看到更完整的敘述。 ```c /* The merging is controlled by "count", the number of elements in the * pending lists. This is beautiully simple code, but rather subtle. * * Each time we increment "count", we set one bit (bit k) and clear * bits k-1 .. 0. Each time this happens (except the very first time * for each bit, when count increments to 2^k), we merge two lists of * size 2^k into one list of size 2^(k+1). ``` 那麼前述的規則該怎麼維持呢? 方法中會透過一個 `count` 來紀錄 pending 的節點數量。當目前的 count + 1 後,會設置第 $k$ 個 bit 為 1,$k-1$ 至 0 bit 為 0 時(除了 `count` 為 $2^k - 1$ 的情境),我們就把兩個 $2^k$ 長度的 list 做合併。細節在後面探討程式碼時會再嘗試說明得更清楚。 對照程式碼的部份解釋: ```c= __attribute__((nonnull(2,3))) void list_sort(void *priv, struct list_head *head, int (*cmp)(void *priv, struct list_head *a, struct list_head *b)) { struct list_head *list = head->next, *pending = NULL; size_t count = 0; /* Count of pending */ if (list == head->prev) /* Zero or one elements */ return; /* Convert to a null-terminated singly-linked list. */ head->prev->next = NULL; ``` 首先破壞掉 linked list 的 circular 結構,最後的節點(`head->prev`) 的 `next` 指向 NULL。 * [`__attribute__((nonnull))`](https://www.keil.com/support/man/docs/armcc/armcc_chr1359124976631.htm) 讓 compiler 可以對指定位置的 pointer 是 NULL 時發出警告 ```c=16 do { size_t bits; struct list_head **tail = &pending; /* Find the least-significant clear bit in count */ for (bits = count; bits & 1; bits >>= 1) tail = &(*tail)->prev; /* Do the indicated merge */ if (likely(bits)) { struct list_head *a = *tail, *b = a->prev; a = merge(priv, (cmp_func)cmp, b, a); /* Install the merged result in place of the inputs */ a->prev = b->prev; *tail = a; } /* Move one element from input list to pending */ list->prev = pending; pending = list; list = list->next; pending->next = NULL; count++; } while (list); ``` 我們可先來觀察 `count` 與 merge 的進行之規律。注意到這裡的格式中,左邊是在該 count 的 iter 開始時的狀態,右邊則是經可能的 merge 後把自己加入 pending list 後的狀態。 * $2^0$ $\to$ count = 1 = $1_{2}$ $\to$ $2^0$ + $2^0$ (no merge) * $2^0$ + $2^0$ $\to$ count = 2 = $10_{2}$ $\to$ $2^1$ + $2^0$ (將 $10_{2}$ 加 1 的話 set bit 0,merge to 2^1) * $2^1$ + $2^0$ count = 3 = $11_{2}$ -> $2^1$ + $2^0$ + $2^0$ (no merge) * $2^1$ + $2^0$ + $2^0$ $\to$ count = 4 = $100_{2}$ $\to$ $2^1$ + $2^1$ + $2^0$ (將 $100_{2}$ 加 1 的話 set bit 0 $\to$ merge to $2^1$) * $2^1$ + $2^1$ + $2^0$ $\to$ count = 5 = $101_{2}$ $\to$ $2^2$ + $2^0$ + $2^0$ (將 $101_{2}$ 加 1 的話 set bit 1 $\to$ merge to $2^2$) * $2^2$ + $2^0$ + $2^0$ $\to$ count = 6 = $110_{2}$ $\to$ $2^2$ + $2^1$ + $2^0$ (將 $110_{2}$ 加 1 的話 set bit 0 $\to$ merge to $2^1$) 可觀察到,`count` 的最小位元的 `0` 假設在位置 k,根據規則就要 merge 出 $2^{k+1}$ 的 list (除了 `count` 為 $2^k - 1$)。而後面為 1 的 bit k - 1, k - 2 ... 0 可以代表 pending 裡有 $2^{k-1}, 2^{k-2} ...... 2^0$ 大小的 Llist 各一個,且因為 `*tail` 一開始就指向 $2^0$ 的那個 list,所以 `*tail` 往 prev 走 k 步就會找到 $2^k$ 大小的 list A,而 A 的 `prev` 是另一個 $2^k$ 大小的 list B,我們要把兩者 merge 在一起。 回到程式碼,for 迴圈中透過 `count` 最低位元的連續 `1` 找到此輪要 merge 的兩個 list,且 bits 若不為 `0` 剛好會等同於我們提到要 merge 的 `count`。最後。每一輪的 list 會把自己的 `next` 設為 `NULL` 而 `prev` 指向 pending,並更新成原本的 `next` 所指向的下一個 node 繼續流程。 下面來看 `merge` 具體的實作: ```c __attribute__((nonnull(2,3,4))) static struct list_head *merge(void *priv, cmp_func cmp, struct list_head *a, struct list_head *b) { struct list_head *head, **tail = &head; for (;;) { /* if equal, take 'a' -- important for sort stability */ if (cmp(priv, a, b) <= 0) { *tail = a; tail = &a->next; a = a->next; if (!a) { *tail = b; break; } } else { *tail = b; tail = &b->next; b = b->next; if (!b) { *tail = a; break; } } } return head; } ``` merge 接受參數的兩個 `list_head` 形態的 a 和 b 後,`tail` 初始時指向一個 dummy pointer。然後 for 迴圈比較 a 和 b。若 a 應該在 b 之前,則 `*tail` 改指向 a,且 `tail` 被更新以指向「指向 a 的下個節點的指標」,這個步驟等同把 a 加到一個新的 linked list 中,然後下一次改成比 a 的 `next` 和 b,反之也是類似道理。直到 `a` 或 `b` 的 `next` 為 NULL 時,把另一個加入 `*tail` 則完成。 繼續來看 `list_sort` 的實作: ```c=41 /* End of input; merge together all the pending lists. */ list = pending; pending = pending->prev; for (;;) { struct list_head *next = pending->prev; if (!next) break; list = merge(priv, (cmp_func)cmp, pending, list); pending = next; } /* The final merge, rebuilding prev links */ merge_final(priv, (cmp_func)cmp, head, pending, list); } ``` 當所有的節點都被加入 pending 後,把所有的 pending 都 merge 在一起。刻意留下 pending 中的其一 `pending` 和其他的 pending merge 在一起的 `list` 去做 `merge_final`,後者的目的是因為 `merge` 只有做單向(`next`)的鏈結,需要把 `prev` 也接去正確的點上,並且回復至 circular linked list 的狀態。 ## Fisher–Yates shuffle [Fisher–Yates shuffle](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle) 是用來將一段有限的數列做隨機排序(排列的結果是等概率的) 的演算法。 ### Pencil-and-paper method 最原始的方法是由 [Ronald Fisher](https://en.wikipedia.org/wiki/Ronald_Fisher) 和 [Frank Yates](https://en.wikipedia.org/wiki/Frank_Yates) 手寫,大致概念如下: * 假設初始的序列如下: | 隨機數範圍 | 選擇隨機數 | 原始序列 | 產生序列 | | ---------- | ---------- |:--------- | -------- | | | | 1 2 3 4 5 | | * 5 個數字的序列,因此從 1~5 間隨機選一數字,以這裡為例是 3,所以把原始序列從左開始數的第 3 個數字 3 放到結果中 | 隨機數範圍 | 選擇隨機數 | 原始序列 | 產生序列 | | ---------- | ---------- |:--------- | -------- | | 1-5 | 3 | 1 2 3 4 5 | 3 | * 4 個數字的序列,因此從 1~4 間隨機選一數字,以這裡為例是 4,所以把原始序列從左開始數的第 4 個數字 5 放到結果中 | 隨機數範圍 | 選擇隨機數 | 原始序列 | 產生序列 | | ---------- | ---------- | ------------- |:-------- | | 1-4 | 4 | 1 2 ~~3~~ 4 5 | 3 5 | * 重複這個流程,隨機數範圍為 0 (原始序列的內容都被搬到產生的序列中) ### Modern method 考慮到在計算機上的使用,Richard Durstenfeld 提出改進。因為原始方法「從左開始數的第 n 個數字」這個過程涉及對原始陣列的額外調整,所以時間複雜度會是 $O(n^2)$,這裡的時間複雜度則是 $O(n)$。此外,不需另外產生一個儲存的序列(inplace),而是透過 swap 的方式達到同等的效果。 :::warning 這裡的時間複雜度是以相鄰記憶體儲存的前題,考慮到資料結構的差異可能也會有複雜度的差異,必須思考自己使用的資料結構給出最恰當的演算法。 ::: * 假設初始的序列如下: | 隨機數範圍 | 選擇隨機數 | 原始序列 | | ---------- | ---------- |:--------- | | | | 1 2 3 4 5 | * 從 1~5 間隨機選一數字,例如這裡的 `3`,將原始序列從左開始數的第 3 個數字和最後 1 個數字對調 | 隨機數範圍 | 選擇隨機數 | 更新後序列 | | ---------- | ---------- |:--------- | | 1-5 | 3 | 1 2 5 4 3 | * 從 1~4 間隨機選一數字,以這裡為例是 2,所以把原始序列從左開始數的第 2 個數字和倒數第 2 個數字交換 | 隨機數範圍 | 選擇隨機數 | 更新後序列 | | ---------- | ---------- | ------------- | | 1-4 | 2 | 1 4 5 2 3 | * 重複這個流程,直到隨機數範圍為 0 ### 實作 ```c void shuffle(node_t **head) { srand(time(NULL)); // First, we have to know how long is the linked list int len = 0; node_t **indirect = head; while (*indirect) { len++; indirect = &(*indirect)->next; } // Append shuffling result to another linked list node_t *new = NULL; node_t **new_head = &new; node_t **new_tail = &new; while (len) { int random = rand() % len; indirect = head; while (random--) indirect = &(*indirect)->next; node_t *tmp = *indirect; *indirect = (*indirect)->next; tmp->next = NULL; if (new) { (*new_tail)->next = tmp; new_tail = &(*new_tail)->next; } else { new = tmp; } len--; } *head = *new_head; } ``` * 先走訪整個原始的 linked list,計算原本的 linked list 的長度 * 用一個 `new` 作為新的 linked list 的 dummy node * `new_head` 總是指向新 linked list 的頭,這是為了回傳時更新 head * `new_tail` 總是指向新 linked list 的尾端,這是為了重新 append 的時候不需要從頭找起 * 每次產生舊 linked list 長度範圍的隨機亂數 `n`,找到第 `n` 個 node,拆下來 append 到新的 linked list 上 * 重覆直到舊的 linked list 不存在節點,即 `len == 0` ## 開放原始碼專案中的實作 - [ ] Linux 核心的 [include/linux/list.h](https://github.com/torvalds/linux/blob/master/include/linux/list.h) 《[Linux Device Drivers](https://lwn.net/Kernel/LDD3/)》第 3 版的[第 11 章](https://static.lwn.net/images/pdf/LDD3/ch11.pdf)有 linked list 相關內容 * 特點 * doubly-linked list, circular * API 操作標的為 `struct list_head`,使用者需自行管理記憶體 * non thread-safe - [ ] [GLib](https://docs.gtk.org/glib/) 的 [GList](https://developer.gnome.org/glib/stable/glib-Doubly-Linked-Lists.html) [glist.h](https://gitlab.gnome.org/GNOME/glib/-/blob/main/glib/glist.h), [glist.c](https://gitlab.gnome.org/GNOME/glib/-/blob/main/glib/glist.c) * 特點 * doubly-linked list * API 操作標的為 pointer to entry data (as `void *`), glib 內部以 `struct Glist` 表示節點, 用法接近 C++ [std::list](https://en.cppreference.com/w/cpp/container/list) * 搭配的記憶體管理: [GLib memory slice](https://docs.gtk.org/glib/memory-slices.html) * 延伸資料結構: [GQueue](https://gitlab.gnome.org/GNOME/glib/-/blob/main/glib/gqueue.c), [GAsyncQueue](https://gitlab.gnome.org/GNOME/glib/-/blob/main/glib/gasyncqueue.c) * [CLib](https://github.com/aheck/clib) 重寫 GLib 裡頭的 GList, GHashTable 及 GString,改為 MIT 授權 ## 待整理 * [Why Linux kernel doubly-linked list is not just a simple linked list?!](https://medium.com/@m.zanoosi/why-linux-kernel-doubly-linked-list-is-not-just-a-simple-linked-list-fb8c43ff150)