# 2024-02-{20,27} 問答簡記 ## 不同長度的 linked list 是否可以合併? > [jouae](https://github.com/jouae) 從 [案例探討: LeetCode 21. Merge Two Sorted Lists](https://hackmd.io/@sysprog/c-linked-list#%E6%A1%88%E4%BE%8B%E6%8E%A2%E8%A8%8E-LeetCode-21-Merge-Two-Sorted-Lists) 延伸,我們將討論文中程式碼對不同長度的單向鏈結串列可否做合併?可以的話怎麼做到的? ### 回顧 [C99 規格書(ISO/IEC 9899:TC3)](https://www.open-std.org/jtc1/sc22/WG14/www/docs/n1256.pdf) * 6.5.13 Logical AND operator >The && operator shall yield 1 if both of its operands compare unequal to 0; otherwise, it yields 0. The result has type *int*. 這邊要注意的是經由 && 邏輯運算子後,得到的型態是 *int* * 6.8.5 Iteration statements >$while$ $($ $expression$ $)$ statement $do$ statement $while$ $($ $expression$ $)$ ; $for$ $($ $expression_{opt}$ ; $expression_{opt}$ ; $expression_{opt}$ $)$ statement $for$ $($ $declaration$ $expression_{opt}$ ; $expression_{opt}$ $)$ statement C99規格書中描述 controlling expression 的限制為 >The controlling expression of an iteration statement shall have *scalar type*. 這邊 *scalar type* 根據 C99 6.2.5 Type 第 21 項 (page 36) 定義為 >Arithmetic types and pointer types are collectively called *scalar types*. Array and structure types are collectively called *aggregate types*. 而 Arithmetic types 在同個 section 下,第 18 項定義為 >Integer and floating types are collectively called arithmetic types. * 有意思的是在[C++11規格書](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2011/n3242.pdf)中, 5.14 Logical AND operator 下的規定是 `bool` 型態 >The && operator groups left-to-right. The operands are both contextually converted to type bool. 可以看到文中 `mergeTwoLists(*L1, *L2)` 輸入只有兩個 `ListNode` 結構體的指標。而內部實作的部分則由 `while(L1 && L2)` 實踐,所以當兩者皆非 null pointer 時,比較完大小順序就可以將兩者合併。 考慮其控制表達式的否命題即為: ``` L1 or L2 is a null pointer ``` 說明停止條件為 L1 或是 L2 指向 null。兩者同時指向 null 的情形只會發生在傳入函式的 L1 和 L2 皆是 null pointer 。 以下圖例簡易說明兩不同長度的單向串列是如何合併的,`L1=[4, 5]` , `L2=[1, 6, 7]`, `ptr` 就像針線的針,會依據節點數值大小把針頭(指標)穿過比較後的節點: ```graphviz digraph structs { node[shape=record] rankdir=LR ptr [label="<data> ptr"] L2_a [label="<data> 1|<header> L2"]; L2_b [label="<data> 6"]; L2_c [label="<data> 7"]; L1_a [label="<data> 4|<header> L1"]; L1_b [label="<data> 5"]; L1_a -> L1_b -> null1; L2_a -> L2_b -> L2_c -> null2; } ``` 比較 L1 和 L2 大小。 `ptr->next` 指向 4 , `ptr` 串接後的數列為 `[4]` ```graphviz digraph structs { node[shape=record] rankdir=LR ptr [label="<data> ptr"] L2_a [label="<data> 1|<header> L2"]; L2_b [label="<data> 6"]; L2_c [label="<data> 7"]; L1_a [label="<data> 4"]; L1_b [label="<data> 5|<header> L1"]; ptr->L1_a -> L1_b -> null1; L2_a -> L2_b -> L2_c -> null2; } ``` 比較 L1 和 L2 大小。 `ptr->next` 指向 5 , `ptr` 串接後的數列為 `[4, 5]` ```graphviz digraph structs { node[shape=record] rankdir=LR ptr [label="<data> ptr"] L2_a [label="<data> 1|<header> L2"]; L2_b [label="<data> 6"]; L2_c [label="<data> 7"]; L1_a [label="<data> 4"]; L1_b [label="<data> 5"]; null1 [label="<data> null1|<header> L1"] L1_a -> L1_b -> null1; L2_a -> L2_b -> L2_c -> null2; ptr -> L1_a } ``` 跳出迴圈,判斷 `ptr->next` 該接到哪裡。由於 L1 目前為 null , `ptr->next` 指向 L2 串接後的數列為 `[4, 5, 1, 6, 7]` 合併完成。 ## 能否使用 linked list 去實作 quicksort > joec1368 針對 Linux 核心的場景,強調是否可以達成 [in-place algorithm](https://en.wikipedia.org/wiki/In-place_algorithm) ## 何時用到 bubble sort 或 insertion sort? > wilicw * 已經接近排序好的 list。 * 任務優先權沒有顯著變化 ## Linus 提到的 bad taste [The mind behind Linux | Linus Torvalds | TED](https://youtu.be/o8NPllzkFhE?feature=shared&t=870) 中的程式碼為何是 bad taste? * 避免 `head` 為 `null`。 * 使用 indirect pointer 來減少特殊案例的處理 ## 排序程式碼練習 > c24094031@gs.ncku.edu.tw 考慮以下程式碼,使用 [list.h](https://github.com/sysprog21/lab0-c/blob/master/list.h) ```c #include <stddef.h> #include <stdint.h> #include "list.h" struct listitem { uint16_t i; struct list_head list; }; static inline int cmpint(const void *p1, const void *p2) { const uint16_t *i1 = (const uint16_t *) p1; const uint16_t *i2 = (const uint16_t *) p2; return *i1 - *i2; } static void list_insert_sorted(struct listitem *entry, struct list_head *head) { struct listitem *item = NULL; if (list_empty(head)) { list_add(&entry->list, head); return; } list_for_each_entry (item, head, list) { if (cmpint(&entry->i, &item->i) < 0) { list_add_tail(&entry->list, &item->list); return; } } list_add_tail(&entry->list, head); } static void list_insertsort(struct list_head *head) { struct list_head list_unsorted; struct listitem *item = NULL, *is = NULL; INIT_LIST_HEAD(&list_unsorted); list_splice_init(head, &list_unsorted); /* Put your code here */ } ``` > :warning: 上方程式碼不完整 ## 為什麼不用在環狀鏈結串列中使用 swap 就可以做到交換? > [jouae](https://github.com/jouae) 最簡單實作的 swap 如下: ```c void swap(int *a, int *b) { int tmp; tmp = *a; *a = *b; *a = tmp; } ``` 可以看到上述的部分需要額外一個變數 `tmp` 來暫存交換的值。 而在單向鏈結串列中,不用做到額外變數,只要使用間接指標( indirect pointer )。參見[你所不知道的 C 語言: linked list 和非連續記憶體——Merge Sort 的實作](https://hackmd.io/@owlfox/BJkrdnExL/https%3A%2F%2Fhackmd.io%2Fs%2FSkE33UTHf#Merge-Sort-%E7%9A%84%E5%AF%A6%E4%BD%9C): ```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; } ``` ## merge sort 實作何處可用 `list_splice`? > [a1157857220](https://github.com/a1157857220) ## Timsort 什麼時候用到 `list_splice`? > [jouae](https://github.com/jouae) 在 Python 官方文件中 listsort.txt 紀錄這個由 Peters, Tim 開發的 Timsort 跟實驗內容,以下摘入自開頭片段: > This describes an adaptive, stable, natural mergesort, modestly called timsort. 在 Timsort 中,討論的基本單位稱作為 run ,為一非遞減或是嚴格遞減的數列。例如: ```cpp [60, 50, 40, 1, 2, 3] ``` 可以拆解成兩個 run: ```cpp run 1: [60, 50, 40] run 2: [1, 2, 3] ``` 而向上述原始數列就已經有排序的子數列,這些子數列稱作為 natural run ,而其餘沒有排序過的元素,會先蒐集起來至一定數量後,藉由插入排序( insert sort )來排序成為一個 run 。同時為避免比較次數過多, Tim 在實作的過程中有限制每個 run 的上限為 64。 在過往學員 [yanjiew1](https://hackmd.io/@yanjiew/linux2023q1-timsort#Timsort-%E9%81%8B%E4%BD%9C%E6%96%B9%E5%BC%8F) 紀錄中,詳述 Timsort 流程。 回頭來看 `list_splice(*list, *head)`,由兩個輸入組成 `*list` 跟 `*head` ,兩者皆是 `list_head` 結構體的指標,作用為將 `list` 這個節點包含其後續節點,繫( splice )於 `head` 之後。所以在進行合併各個 run 的過程中, `list_splice` 就在此時上場。 在 Linux list_sort.c 中合併的實踐是由 `*merge(void *priv, list_cmp_func_t cmp, struct list_head *a, struct list_head *b)` 達成。由於並非環狀鏈結串列的合併,跟我們預期 `list_splice` 的運作不一樣, `list_splice` 針對環狀結構操作。 **注意**:在此的 `*a` 和 `*b` 為 null-terminated singly-linked list 。詳見原始碼中 `list_sort()` 的部分。 在 `merge_final()` 部分中,則還原了原本串列的環狀結構。 在函數的一開始先把單向串列改成雙向串列 ```c // 將 a 鏈結在 tail 之後 // 並將 a 的前一個節點指向 tail 形成 doubly linked tail->next = a; a->prev = tail; // 更新 tail 位置為 a tail = a; a = a->next; ``` 函式的最後將雙向串列頭尾相連形成環狀結構 ```c /* And the final links to make a circular doubly-linked list */ tail->next = head; head->prev = tail; ``` 整個過程簡化的話,就是 `list_splice` 的功能。 * [Jserv. 你所不知道的 C 語言: linked list 和非連續記憶體——Linux 核心的 list_sort 實作](https://hackmd.io/@sysprog/c-linked-list#Linux-%E6%A0%B8%E5%BF%83%E7%9A%84-list_sort-%E5%AF%A6%E4%BD%9C) * [visitorckw. Linux 核心專題: 改進 `lib/list_sort.c`](https://hackmd.io/@sysprog/Hy5hmaKBh) * [yanjiew1. Timsort 研究與對 Linux 核心貢獻嘗試](https://hackmd.io/@yanjiew/linux2023q1-timsort) * [yanjiew1. linux23q1-timsort](https://github.com/yanjiew1/linux23q1-timsort/blob/main/timsort.c) * [Peters, Tim. listsort.txt](https://svn.python.org/projects/python/trunk/Objects/listsort.txt) * [Peters, Tim. [Python-Dev] Sorting. Python Developers Mailinglist.](https://mail.python.org/pipermail/python-dev/2002-July/026837.html) ## 為何 `list.h` 選定 0x00100100 與 0x00200200 作為無效的地址? > [Jimmy01240397](https://github.com/Jimmy01240397) 我在做 `git log | grep <address>` 之類的操作後依然沒有詳細說明。只知道該 address 第一次出現是在 commit `9a69abf80edf2ea0dac058cab156879d29362788` "menuconfig: Add "breadcrumbs" navigation aid" 的位置 ```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 } ``` [linux/poison.h](https://github.com/torvalds/linux/blob/master/include/linux/poison.h) 的註解: > These are non-NULL pointers that will result in page faults under normal circumstances, used to verify that nobody uses non-initialized list entries. 當 `list_del` 執行時,會將節點的的 prev 和 next 指向一個特定的地址,當存取到該地址時,會觸發 [page fault](https://en.wikipedia.org/wiki/Page_fault)。 在《[Linux Device Drivers 3/e](https://lwn.net/Kernel/LDD3/)》第四章 Debugging Techniques 的引言提到,Linux 核心的程式碼不易在除錯器中運作、錯誤也不易重現,不過一旦引發錯誤時,可能會讓整個系統崩潰,而造成錯誤的證據則隨之消失。 除錯的的其一手法就是將這種有毒 (poison) 的值設在使用前後,當該數值變更時,我們就會知道非預期的狀況發生。 > 諺語: One man's meat is another man's poison 有了這樣的設計,我們便可在除錯時知道 : 若 `node->next` 與 `node->prev` 分別指向 `LIST_POISON1` 與 `LIST_POISON2` 的話,就代表發生 use-after-free 的情況,像是 [lib/list_debug.c](https://github.com/torvalds/linux/blob/master/lib/list_debug.c) 便是 Linux 核心中對應的除錯程式碼。 若無 `LIST_POISONING` 的設計,在 `list_del()` 時將 `node->next` 和 `node->prev` 都指向 NULL,除錯階段就無法得知此時的 NULL 代表的是 uninitialized 抑或被釋放後才指派的。 至於不直接釋放節點的原因,可參閱 [Kernel Self-Protection](https://www.kernel.org/doc/html/latest/security/self-protection.html) 中 Memory poisoning 的部份得知: > When releasing memory, it is best to poison the contents, to **<font color="#000">avoid reuse attacks that rely on the old contents of memory</font>**. E.g., clear stack on a syscall return (`CONFIG_GCC_PLUGIN_STACKLEAK`), wipe heap memory on a free. This frustrates many uninitialized variable attacks, stack content exposures, heap content exposures, and use-after-free attacks. ## [2024q1 第 2 週測驗題](https://hackmd.io/@sysprog/linux2024-quiz2)第 1 題 > [weihsinyeh](https://github.com/weihsinyeh) 建立二元樹需要有透過得知中序(in-order)與前序 (pre-order) 或是後序 (post-order)才能具體說明一個獨一無二的二元樹的原因說明舉例如下。 前序是 `[A B C]` 透過前序知道 `A` 是 中間的那個點,但卻無法透過前序知道 `B` `C` 相對於A的左邊還是右邊。 因此產生的二元樹可能如下中序分別是 `[B C A]`, `[B A C]` , `[A B C]` ``` A A A / / \ \ B B C B \ \ C C (1) (2) (3) ``` 第一個樹:由中序知道 `A` 是 中間的那個點,`B` `C` 相對於`A`在左邊。所以都在 `A` 的左子樹中。 第二個樹:由中序知道 `A` 是 中間的那個點,`B` 相對於 `A` 在左邊,所以在 `A` 的左子樹中。`C` 相對於`A`在右邊,所以在 `A` 的右子樹中。 第三個樹:由中序知道 `A` 是 中間的那個點,`B` `C` 相對於`A`在右邊。所以都在 `A` 的右子樹中。 所以 DFS 中的實作是透過 preorder 會藉由 `find` 找到`preorder[pre_low]` 在`inorder`中的位置 `idx`。 由圖得知在 `idx` 左邊的都會被放到左子樹,在`idx`右邊的都會被放到右子樹。 因此對應到左子樹的在中序涵蓋的範圍 : `[in_low,idx-1]`,右子樹在中序涵蓋的範圍 : `[idx+1,in_high]` ```c tn->left = dfs(preorder, pre_low + 1, pre_low + (idx - in_low), inorder, in_low, idx - 1, in_heads, size); tn->right = dfs(preorder, pre_high - (in_high - idx - 1), pre_high, inorder, idx + 1, in_high, in_heads, size); ``` 而觀察剛剛的前序可以知道他的分佈的規則就是 `[中點 左子樹 右子樹]`,從這裡就可以看到只要知道左子樹有多少個,右子樹有多少個,就可以知道在前序的範圍了。 而因為 `pre_low` 是中點 左子樹在前序的涵蓋範圍 : `[pre_low + 1, pre_low + (idx - in_low)]` 其實就是`pre_low`往後左子樹的在中序涵蓋的範圍 : `[in_low,idx-1]` 右子樹在前序的涵蓋範圍 : `[pre_high - (in_high - idx - 1), pre_high]` 其實就是`pre_high`往前右子樹的在中序涵蓋的範圍 : `[idx+1,in_high]` 也因此從這樣的分析其實知道如果題目提供的只有後序與中序,我只要從後序的最後一個往前讀。 改變範圍即可因為後序的分佈是 `[左子樹 右子樹 中點]`,`post_high`是中點。 所以左子樹在後序的涵蓋範圍:`[post_low, post_low + (idx - 1 - in_low)]` 右子樹在後序的涵蓋範圍:`[post_high - 1 - (in_high - (idx+1)) , post_high-1]` 所以這樣也就明白中序在建立二元樹中缺一不可的角色,而前序與後序則是可以只挑其中一個。 而 `find` 函式則是用 hash table 去紀錄`in-order`的位置而已,可換成其他資料結構來實作。