--- tags: linux2021 --- # 2021q1 Homework1 (quiz1) contributed by < `catkitchen721` > [2021q1 第 1 週測驗題](https://hackmd.io/@sysprog/linux2021-quiz1) - [x] 解釋上述程式運作原理,搭配 Graphviz,比照 Linked List 練習題在 HackMD 筆記上視覺化展現; - [ ] 測試程式使用到 random,多執行幾次可發現輸出結果相仿,請修正,並引入其他 Pseudorandom number generator - [ ] 參考 Optimized QuickSort — C Implementation (Non-Recursive) 並重寫上述 quick sort 程式碼,避免使用遞迴呼叫 - [ ] Linux 核心內部也有 linked list 實作,但是 circulr doubly-linked list,linux-list 仿效 Linux 核心的實作並予以簡化,在 examples/ 目錄提供 quick sort 實作,請探討 Linux 的 linked list 和上述程式碼的落差,並改寫 linux-list 的 quick sort 範例,避免使用遞迴呼叫 - [ ] 研讀 Ching-Kuang Shene (冼鏡光) 教授撰寫的 A Comparative Study of Linked List Sorting Algorithms,思考高效率的 linked list 排序演算法,並落實於上述 (3) 的程式碼中 ## 測驗 1 1. 考慮一個單向 linked list,其結構定義為: ```clike typedef struct __node { int value; struct __node *next; } node_t; ``` * 該結構可表示成: ```graphviz digraph node_struct { rankdir=LR; node [shape=record]; node0 [label="node_t|{{value|<value>}|{next|<next>}}"]; node [shape=box]; node1 [style="invis"]; node0:next:c -> node1 [tailclip=false, arrowtail=dot, dir=both]; } ``` 2. 已知不存在 circular (環狀結構),下方程式碼嘗試對該單向 linked list 進行 Quick sort,預期依據遞增順序。 ```cpp static inline void list_add_node_t(node_t **list, node_t *node_t) { node_t->next = *list; *list = node_t; } static inline void list_concat(node_t **left, node_t *right) { while (*left) LLL; *left = right; } void quicksort(node_t **list) { if (!*list) return; node_t *pivot = *list; int value = pivot->value; node_t *p = pivot->next; pivot->next = NULL; node_t *left = NULL, *right = NULL; while (p) { node_t *n = p; p = p->next; list_add_node_t(n->value > value ? AAA : BBB, n); } quicksort(&left); quicksort(&right); node_t *result = NULL; list_concat(&result, left); CCC; *list = result; } ``` ### 將各函數分開分析如以下各點: #### `list_add_node_t` 函式 ```cpp static inline void list_add_node_t(node_t **list, node_t *node_t) { node_t->next = *list; *list = node_t; } ``` 首先注意到的是 ==static== 與 ==inline== 關鍵字,在過去我甚少利用到這些 specifiers ,正好利用這次機會查詢 C 語言規格書了解其使用方式: :::info * ## static > `節錄自 C 語言規格書 ISO/IEC 9899:1999 - 6.2.2 章節` > **6.2.2 Linkages of identifiers** > * An identifier declared in different scopes or in the same scope more than once can be made to refer to the same object or function by a process called linkage. There are three kinds of linkage: ==external==, ==internal==, and ==none==. > * In the set of translation units and libraries that constitutes an entire program, each declaration of a particular identifier with external linkage denotes the same object or function. ==Within one translation unit, each declaration of an identifier with internal linkage denotes the same object or function==. Each declaration of an identifier with no linkage denotes a unique entity. > * If the declaration of a file scope identifier for an object or a function contains the storage-class specifier ==static==, the identifier has ==internal linkage==. <br/> 看了以上對於 linkage 的說明我還是產生了疑惑,於第二段說明中,到底什麼是 **translation unit** ? <br/> 既然有疑惑就繼續往下查: <br/> > `節錄自 C 語言規格書 ISO/IEC 9899:1999 - 5.1.1.1 章節` > **5.1.1.1 Program structure** > * A C program need not all be translated at the same time. The text of the program is kept in units called source files, (or preprocessing files) in this International Standard. A source file together with all the headers and source files included via the preprocessing directive **#include** is known as a ==preprocessing translation unit==. After preprocessing, a preprocessing translation unit is called a ==translation unit==. <br/> 對於這段說明我的解讀是,一個 source file 與它 **#include** 的所有 header files, source files 合稱 ==preprocessing translation unit== ,到這邊還沒結束,最後一句提到要在 **preprocessing** 之後才稱作 ==translation unit==。 <br/> 又遇到問題了,什麼是 **preprocessing** ? <br/> > `節錄自 C 語言規格書 ISO/IEC 9899:1999 - 6.10 章節 - Semantics 項目` > **6.10 Preprocessing directives** > > **Semantics** > > The implementation can process and skip sections of source files conditionally, include other source files, and replace macros. These capabilities are called ==preprocessing==, because conceptually they occur before translation of the resulting translation unit. <br/> **prepocessing** 指的是,實作可以: 1. 依據**條件**處理與**跳過** source files 的片段,包括其他 source files 2. **替換 macros** <br/> * 對於 ==static== 關鍵字的總結: * 在一個 object 或一個 function 識別符前加上 **static** 關鍵字,則該識別符在同一個 translation unit 下指的是同一個 object 或 function。 * 同一個 translation unit 指的是,該識別符所在的 source file 經過條件跳過一些程式片段以及替換 macros 後(包括 **#include** 所包含的 header files 也會展開),該處理完的 source file 稱作一個 translation unit 。 ::: :::info * ## inline > `節錄自 C 語言規格書 ISO/IEC 9899:1999 - 6.7.4 章節 - Semantics 項目` > **6.7.4 Function specifiers** > > A function declared with an inline function specifier is an inline function. The function specifier may appear more than once; the behavior is the same as if it appeared only once. ==Making a function an inline function suggests that calls to the function be as fast as possible.== The extent to which such suggestions are effective is ==implementation-defined==. <br/> **inline** 關鍵字是一個函式( function )說明符( specifier ),若加在 function 識別符( identifier )之前,表示建議編譯器使得這個函式能呼叫地愈快愈好。但規格書也提到了實際效果是由**實作**定義的。 ### GCC 對於處理 inline function 的實作 * [Using the GNU Compiler Collection (GCC)](https://gcc.gnu.org/onlinedocs/gcc/) <br/> > `節錄自 GCC 官方文件 - 6.45 章節` > **6.45 An Inline Function is As Fast As a Macro** > By declaring a function inline, you can direct GCC to make calls to that function faster. ==One way GCC can achieve this is to integrate that function’s code into the code for its callers==. This makes execution faster by eliminating the function-call overhead; in addition, ==if any of the actual argument values are constant, their known values may permit simplifications at compile time so that not all of the inline function’s code needs to be included==. The effect on code size is less predictable; object code may be larger or smaller with function inlining, depending on the particular case. <br/> GCC 有兩種方式處理 inline function : 1. 直接將 function code 整合到呼叫程式的 code 中 2. 若有引數的值是常數,則會允許在 compile time 時以它們的已知值進行簡化,使得 function code 不用被全部包含進呼叫程式中。 <br/> 對於是否加上 **static** 關鍵字, GCC 也有進行說明: <br/> > `節錄自 GCC 官方文件 - 6.45 章節` > **6.45 An Inline Function is As Fast As a Macro** > ... > When an inline function is not static, then the compiler must assume that there may be calls from other source files; since a global symbol can be defined only once in any program, the function must not be defined in the other source files, so the calls therein cannot be integrated. Therefore, a non-static inline function is always compiled on its own in the usual fashion. <br/> GCC 解釋,若一個 inline function 不具 static 關鍵字,會如同 C 語言規格書所解釋,不將它當成一個具有 internal linkage 的 function ,可能會有其他 source file 呼叫這個 function ,因此 GCC 不會將這個 function 直接整合進呼叫程式中,而是直接視為 inline 關鍵字沒有效果。 ::: ##### 小結: 1. static 使得這些 functions 有 internal linkage,不會從其他 translation unit 呼叫 2. inline 使得這些體積較小的 functions 能以較快的速度呼叫 回到原題目: ```cpp static inline void list_add_node_t(node_t **list, node_t *node_t) { node_t->next = *list; *list = node_t; } ``` ==(注意到參數中的 node_t 應為原資料結構 node_t 的指標型態)== * 若 list 原本為空: ```graphviz digraph node_struct { rankdir=LR; node [shape=record]; node0 [label="<id>|{{value|<value>}|{next|<next>}}"]; node [shape=plaintext]; null0 [label="NULL"]; node [shape=box]; node_t; node1 [label="list"]; invis0 [style="invis"]; { rank=same; node1 -> null0; invis0; } node_t -> node0:id; node0:next:c -> invis0 [tailclip=false, arrowtail=dot, dir=both]; } ``` ```cpp node_t->next = *list; ``` ```graphviz digraph node_struct { rankdir=LR; node [shape=record]; node0 [label="<id>|{{value|<value>}|{next|<next>}}"]; node [shape=plaintext]; null0 [label="NULL"]; node [shape=box]; node_t; node1 [label="list"]; { rank=same; node1 -> null0; } node_t -> node0:id; node0:next:c -> null0 [tailclip=false, arrowtail=dot, dir=both]; } ``` ```cpp *list = node_t; ``` ```graphviz digraph node_struct { rankdir=LR; node [shape=record]; node0 [label="<id>|{{value|<value>}|{next|<next>}}"]; node [shape=plaintext]; null0 [label="NULL"]; node [shape=box]; node_t; invis0 [style="invis"]; node1 [label="list"]; { rank=same; node1; invis0; } node1 -> node_t; node_t -> node0:id; invis0 -> node0 [style="invis"]; node0:next:c -> null0 [tailclip=false, arrowtail=dot, dir=both]; } ``` * 若 list 原本不為空: ```graphviz digraph node_struct { rankdir=LR; node [shape=record]; node0 [label="<id>|{{value|<value>}|{next|<next>}}"]; node [shape=box]; node_t; node1 [label="list"]; node2 [label="node-in-list"]; invis0 [style="invis"]; { rank=same; node1 -> node2; invis0; } node_t -> node0:id; node0:next:c -> invis0 [tailclip=false, arrowtail=dot, dir=both]; } ``` ```cpp node_t->next = *list; ``` ```graphviz digraph node_struct { rankdir=LR; node [shape=record]; node0 [label="<id>|{{value|<value>}|{next|<next>}}"]; node [shape=box]; node_t; node1 [label="list"]; node2 [label="node-in-list"]; { rank=same; node1 -> node2; } node_t -> node0:id; node0:next:c -> node2 [tailclip=false, arrowtail=dot, dir=both]; } ``` ```cpp *list = node_t; ``` ```graphviz digraph node_struct { rankdir=LR; node [shape=record]; node0 [label="<id>|{{value|<value>}|{next|<next>}}"]; node [shape=box]; node_t; node1 [label="list"]; node2 [label="node-in-list"]; invis0 [style="invis"]; { rank=same; node1; invis0; } node_t -> node0:id; invis0 -> node0 [style="invis"]; node1 -> node_t:id; node0:next:c -> node2 [tailclip=false, arrowtail=dot, dir=both]; } ``` 可以發現兩者的結果皆是在 list 開頭插入 node_t 。 --- #### `list_concat` 函式 ```cpp= static inline void list_concat(node_t **left, node_t *right) { while (*left) left = &((*left)->next) /*LLL*/ *left = right; } ``` * 假設 left 指向 NULL ,圖像化表示成: ```graphviz digraph node_struct { rankdir=LR; node [shape=box]; left; node [shape=plaintext]; null0[label="NULL"]; { rank=same; left -> null0; } } ``` right 指向一串 nodes : ```graphviz digraph node_struct { rankdir=LR; node [shape=box]; right;rnode0;rnode1;rnode2; r1 [label="rnode1_ptr"];r2 [label="rnode2_ptr"]; invis0 [style=invis]; node [shape=plaintext]; null0[label="NULL"]; { rank=same; right -> rnode0; } { rank=same; r1 -> rnode1; } { rank=same; r2 -> rnode2; } { rank=same; null0 -> invis0 [style=invis]; } rnode0 -> r1; right -> r1 [style=invis]; rnode0 -> rnode1 [style=invis]; rnode1 -> r2; r1 -> r2 [style=invis]; rnode1 -> rnode2 [style=invis]; rnode2 -> null0; r2 -> null0 [style=invis]; rnode2 -> invis0 [style=invis]; } ``` 放在一起: ```graphviz digraph node_struct { rankdir=LR; node [shape=box]; left; right;rnode0;rnode1;rnode2; r1 [label="rnode1_ptr"];r2 [label="rnode2_ptr"]; invis0 [style=invis]; node [shape=plaintext]; null0[label="NULL"]; null1[label="NULL"]; { rank=same; right -> rnode0; } { rank=same; r1 -> rnode1; } { rank=same; r2 -> rnode2; } { rank=same; null0 -> invis0 [style=invis]; } { rank=same; left -> null1; } rnode0 -> r1; right -> r1 [style=invis]; rnode0 -> rnode1 [style=invis]; rnode1 -> r2; r1 -> r2 [style=invis]; rnode1 -> rnode2 [style=invis]; rnode2 -> null0; r2 -> null0 [style=invis]; rnode2 -> invis0 [style=invis]; null1 -> right [style=invis]; } ``` 首先, 第 2 行 `while (*left)` 會因為 left 指向 NULL 而跳離迴圈,因此可以直接看第 4 行 `*left = right;` ,這句會將 left 指向的記憶體空間內容改為 right 這個指標。 ```graphviz digraph node_struct { rankdir=LR; node [shape=box]; left; right [label=< <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0"> <TR><TD PORT="right">right</TD></TR> </TABLE>>]; rnode0;rnode1;rnode2; r1 [label="rnode1_ptr"];r2 [label="rnode2_ptr"]; invis0 [style=invis]; node [shape=plaintext]; null0[label="NULL"]; { rank=same; left -> right; right:right -> rnode0; } { rank=same; r1 -> rnode1; } { rank=same; r2 -> rnode2; } { rank=same; null0 -> invis0 [style=invis]; } rnode0 -> r1; right -> r1 [style=invis]; rnode0 -> rnode1 [style=invis]; rnode1 -> r2; r1 -> r2 [style=invis]; rnode1 -> rnode2 [style=invis]; rnode2 -> null0; r2 -> null0 [style=invis]; rnode2 -> invis0 [style=invis]; } ``` 這樣就可以將 left 指向的記憶體空間置換成 right 。 --- ```cpp= static inline void list_concat(node_t **left, node_t *right) { while (*left) left = &((*left)->next) /*LLL*/ *left = right; } ``` * 假設 left 所指向的記憶體空間**不**為 NULL ```graphviz digraph node_struct { rankdir=LR; node [shape=box]; left; right;rnode0;rnode1;rnode2; lnode0;lnode0_ptr;lnode1;lnode1_ptr; r1 [label="rnode1_ptr"];r2 [label="rnode2_ptr"]; invis0 [style=invis]; invis1 [style=invis]; node [shape=plaintext]; null0[label="NULL"]; null1[label="NULL"]; { rank=same; right -> rnode0; } { rank=same; r1 -> rnode1; } { rank=same; r2 -> rnode2; } { rank=same; null0 -> invis0 [style=invis]; } { rank=same; left -> lnode0_ptr -> lnode0; } { rank=same; lnode1_ptr -> lnode1; } { rank=same; null1 -> invis1 [style=invis]; } rnode0 -> r1; right -> r1 [style=invis]; rnode0 -> rnode1 [style=invis]; rnode1 -> r2; r1 -> r2 [style=invis]; rnode1 -> rnode2 [style=invis]; rnode2 -> null0; r2 -> null0 [style=invis]; rnode2 -> invis0 [style=invis]; lnode0 -> lnode1_ptr; lnode1 -> null1; null1 -> right [style=invis]; lnode0_ptr -> lnode1_ptr [style=invis]; lnode0 -> lnode1 [style=invis]; lnode1_ptr -> null1 [style=invis]; lnode1 -> invis1 [style=invis]; } ``` 則第 2 行的 `while (*left)` 不會馬上跳離迴圈,繼續往下追蹤: ```cpp left = &((*left)->next) ``` ```graphviz digraph node_struct { rankdir=LR; node [shape=box]; left;sleft[label="lnode0_ptr"]; right;rnode0;rnode1;rnode2; lnode0;lnode1;lnode1_ptr; r1 [label="rnode1_ptr"];r2 [label="rnode2_ptr"]; invis0 [style=invis]; invis1 [style=invis]; node [shape=plaintext]; null0[label="NULL"]; null1[label="NULL"]; { rank=same; right -> rnode0; } { rank=same; r1 -> rnode1; } { rank=same; r2 -> rnode2; } { rank=same; null0 -> invis0 [style=invis]; } { rank=same; left -> sleft [style=invis]; sleft -> lnode0; } { rank=same; lnode1_ptr -> lnode1; } { rank=same; null1 -> invis1 [style=invis]; } rnode0 -> r1; right -> r1 [style=invis]; rnode0 -> rnode1 [style=invis]; rnode1 -> r2; r1 -> r2 [style=invis]; rnode1 -> rnode2 [style=invis]; rnode2 -> null0; r2 -> null0 [style=invis]; rnode2 -> invis0 [style=invis]; lnode0 -> lnode1_ptr; lnode1 -> null1; null1 -> right [style=invis]; sleft -> lnode1_ptr [color=blue, label="next", fontcolor=blue]; lnode0 -> lnode1 [style=invis]; lnode1_ptr -> null1 [style=invis]; lnode1 -> invis1 [style=invis]; left -> lnode1_ptr; } ``` 整理後: ```graphviz digraph node_struct { rankdir=LR; node [shape=box]; left;sleft[label="lnode0_ptr"]; right;rnode0;rnode1;rnode2; lnode0;lnode1;lnode1_ptr; r1 [label="rnode1_ptr"];r2 [label="rnode2_ptr"]; invis0 [style=invis]; invis1 [style=invis]; node [shape=plaintext]; null0[label="NULL"]; null1[label="NULL"]; { rank=same; right -> rnode0; } { rank=same; r1 -> rnode1; } { rank=same; r2 -> rnode2; } { rank=same; null0 -> invis0 [style=invis]; } { rank=same; sleft -> lnode0; } { rank=same; left -> lnode1_ptr -> lnode1; } { rank=same; null1 -> invis1 [style=invis]; } rnode0 -> r1; right -> r1 [style=invis]; rnode0 -> rnode1 [style=invis]; rnode1 -> r2; r1 -> r2 [style=invis]; rnode1 -> rnode2 [style=invis]; rnode2 -> null0; r2 -> null0 [style=invis]; rnode2 -> invis0 [style=invis]; lnode0 -> lnode1_ptr; lnode1 -> null1; null1 -> right [style=invis]; sleft -> lnode1_ptr [style=invis]; lnode0 -> lnode1 [style=invis]; lnode1_ptr -> null1 [style=invis]; lnode1 -> invis1 [style=invis]; } ``` 迴圈第一圈完成了,這邊可以針對 LLL 這題的 \(d\) 選項做個解析: :::info 如果這題使用了 \(d\) 選項, `*left = (*left)->next` ,則圖示會變成如下所示: 1. ```graphviz digraph node_struct { rankdir=LR; node [shape=box]; left; right;rnode0;rnode1;rnode2; lnode0;lnode0_ptr;lnode1;lnode1_ptr; r1 [label="rnode1_ptr"];r2 [label="rnode2_ptr"]; invis0 [style=invis]; invis1 [style=invis]; node [shape=plaintext]; null0[label="NULL"]; null1[label="NULL"]; { rank=same; right -> rnode0; } { rank=same; r1 -> rnode1; } { rank=same; r2 -> rnode2; } { rank=same; null0 -> invis0 [style=invis]; } { rank=same; left -> lnode0_ptr -> lnode0; } { rank=same; lnode1_ptr -> lnode1; } { rank=same; null1 -> invis1 [style=invis]; } rnode0 -> r1; right -> r1 [style=invis]; rnode0 -> rnode1 [style=invis]; rnode1 -> r2; r1 -> r2 [style=invis]; rnode1 -> rnode2 [style=invis]; rnode2 -> null0; r2 -> null0 [style=invis]; rnode2 -> invis0 [style=invis]; lnode0 -> lnode1_ptr; lnode1 -> null1; null1 -> right [style=invis]; lnode0_ptr -> lnode1_ptr [style=invis]; lnode0 -> lnode1 [style=invis]; lnode1_ptr -> null1 [style=invis]; lnode1 -> invis1 [style=invis]; } ``` 2. ```graphviz digraph node_struct { rankdir=LR; node [shape=box]; left; right;rnode0;rnode1;rnode2; lnode1;lnode1_ptr [label=< <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0"> <TR><TD PORT="here">lnode1_ptr</TD></TR> </TABLE>>]; r1 [label="rnode1_ptr"];r2 [label="rnode2_ptr"]; invis0 [style=invis]; invis1 [style=invis]; node [shape=plaintext]; null0[label="NULL"]; null1[label="NULL"]; { rank=same; right -> rnode0; } { rank=same; r1 -> rnode1; } { rank=same; r2 -> rnode2; } { rank=same; null0 -> invis0 [style=invis]; } { rank=same; left -> lnode1_ptr; lnode1_ptr:here -> lnode1; } { rank=same; null1 -> invis1 [style=invis]; } rnode0 -> r1; right -> r1 [style=invis]; rnode0 -> rnode1 [style=invis]; rnode1 -> r2; r1 -> r2 [style=invis]; rnode1 -> rnode2 [style=invis]; rnode2 -> null0; r2 -> null0 [style=invis]; rnode2 -> invis0 [style=invis]; lnode1 -> null1; null1 -> right [style=invis]; lnode1_ptr -> null1 [style=invis]; lnode1 -> invis1 [style=invis]; } ``` 會直接將 left 所指的空間蓋掉,造成 lnode0_ptr 的值不見,再也無法取得 lnode0 這個 node 。 ::: 進行迴圈第 2 圈, `while (*left)` 此時的 \*left 是剛剛的 lnode1_ptr 指標,不為空,因此進入迴圈: ```cpp left = &((*left)->next) ``` ```graphviz digraph node_struct { rankdir=LR; node [shape=box]; left;head[label="lnode0_ptr"]; right;rnode0;rnode1;rnode2; lnode0;lnode1;lnode1_ptr; r1 [label="rnode1_ptr"];r2 [label="rnode2_ptr"]; invis0 [style=invis]; invis1 [style=invis]; node [shape=plaintext]; null0[label="NULL"]; null1[label="NULL"]; { rank=same; right -> rnode0; } { rank=same; r1 -> rnode1; } { rank=same; r2 -> rnode2; } { rank=same; null0 -> invis0 [style=invis]; } { rank=same; head -> lnode0; } { rank=same; left -> lnode1_ptr [style=invis]; lnode1_ptr -> lnode1; } { rank=same; null1 -> invis1 [style=invis]; } rnode0 -> r1; right -> r1 [style=invis]; rnode0 -> rnode1 [style=invis]; rnode1 -> r2; r1 -> r2 [style=invis]; rnode1 -> rnode2 [style=invis]; rnode2 -> null0; r2 -> null0 [style=invis]; rnode2 -> invis0 [style=invis]; lnode0 -> lnode1_ptr; lnode1 -> null1; null1 -> right [style=invis]; head -> lnode1_ptr [style=invis]; lnode0 -> lnode1 [style=invis]; lnode1_ptr -> null1 [color=blue, label="next", fontcolor=blue]; lnode1 -> invis1 [style=invis]; left -> null1; } ``` 整理後: ```graphviz digraph node_struct { rankdir=LR; node [shape=box]; left;head[label="lnode0_ptr"]; right;rnode0;rnode1;rnode2; lnode0;lnode1;lnode1_ptr; r1 [label="rnode1_ptr"];r2 [label="rnode2_ptr"]; invis0 [style=invis]; invis1 [style=invis]; node [shape=plaintext]; null0[label="NULL"]; null1[label="NULL"]; { rank=same; right -> rnode0; } { rank=same; r1 -> rnode1; } { rank=same; r2 -> rnode2; } { rank=same; null0 -> invis0 [style=invis]; } { rank=same; head -> lnode0; } { rank=same; lnode1_ptr -> lnode1; } { rank=same; left -> null1; null1 -> invis1 [style=invis]; } rnode0 -> r1; right -> r1 [style=invis]; rnode0 -> rnode1 [style=invis]; rnode1 -> r2; r1 -> r2 [style=invis]; rnode1 -> rnode2 [style=invis]; rnode2 -> null0; r2 -> null0 [style=invis]; rnode2 -> invis0 [style=invis]; lnode0 -> lnode1_ptr; lnode1 -> null1; null1 -> right [style=invis]; head -> lnode1_ptr [style=invis]; lnode0 -> lnode1 [style=invis]; lnode1_ptr -> null1 [style=invis]; lnode1 -> invis1 [style=invis]; } ``` 最後迴圈第 3 次檢查後,此時的 \*left 為 NULL ,因此直接跳第 4 行: ```cpp *left = right; ``` ```graphviz digraph node_struct { rankdir=LR; node [shape=box]; left;head[label="lnode0_ptr", fontname="times-bold"]; right [label=< <TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0"> <TR><TD PORT="here">right</TD></TR> </TABLE>>];rnode0;rnode1;rnode2; lnode0;lnode1;lnode1_ptr; r1 [label="rnode1_ptr"];r2 [label="rnode2_ptr"]; invis0 [style=invis]; invis1 [style=invis]; node [shape=plaintext]; null0[label="NULL"]; { rank=same; right:here -> rnode0; } { rank=same; r1 -> rnode1; } { rank=same; r2 -> rnode2; } { rank=same; null0 -> invis0 [style=invis]; } { rank=same; head -> lnode0; } { rank=same; lnode1_ptr -> lnode1; } { rank=same; left -> right; } rnode0 -> r1; right -> r1 [style=invis]; rnode0 -> rnode1 [style=invis]; rnode1 -> r2; r1 -> r2 [style=invis]; rnode1 -> rnode2 [style=invis]; rnode2 -> null0; r2 -> null0 [style=invis]; rnode2 -> invis0 [style=invis]; lnode0 -> lnode1_ptr; lnode1 -> right; head -> lnode1_ptr [style=invis]; lnode0 -> lnode1 [style=invis]; lnode1_ptr -> right [style=invis]; lnode1 -> invis1 [style=invis]; } ``` 如前面的分析,因為 left 所指的記憶體空間不變,是直接以 right 覆蓋,因此 lnode1 也能指到 right ,成功串接。 串接結束後,原本的 **lnode0_ptr** 指標就能直接取得串列的頭。 --- #### `quicksort` 函式 ```cpp= void quicksort(node_t **list) { if (!*list) return; node_t *pivot = *list; int value = pivot->value; node_t *p = pivot->next; pivot->next = NULL; node_t *left = NULL, *right = NULL; while (p) { node_t *n = p; p = p->next; list_add_node_t(n->value > value ? &right : &left, n); /*AAA, BBB*/ } quicksort(&left); quicksort(&right); node_t *result = NULL; list_concat(&result, left); list_concat(&result, pivot); /*CCC*/ list_concat(&result, right); /*CCC*/ *list = result; } ``` :::info 在從頭開始分析之前,先注意到剛剛解析的 `list_concat` 函式用法: ```cpp node_t *result = NULL; list_concat(&result, left); ``` 其中 result 就相當於剛剛分析的 **lnode0_ptr** ,而 \&result 就相當於 **left** 這個指標的指標。剛才的最後提到了 **lnode0_ptr** 能繼續指向串列開頭,因此串接完後, result 能夠繼續維持串列開頭位址,不因為串列連接而失去串列開頭。 ::: ```cpp if (!*list) return; ``` 檢查 list 是否指向 NULL ,若是,則直接 return 。 ```cpp node_t *pivot = *list; int value = pivot->value; node_t *p = pivot->next; pivot->next = NULL; ``` 假設 list 為: ```graphviz digraph quicksort { rankdir=LR; # node [shape=record]; # value [label="value|"]; node [shape=box]; list; p_1;n_1 [label="6"]; p_2;n_2 [label="2"]; p_3;n_3 [label="9"]; # pivot; # p; invis0 [style=invis]; node [shape=plaintext]; null0 [label="NULL"]; { rank=same; list -> p_1 -> n_1; } { rank=same; p_2 -> n_2; } { rank=same; p_3 -> n_3; } { rank=same; null0 -> invis0 [style=invis]; } n_1 -> p_2; p_1 -> p_2 [style=invis]; n_1 -> n_2 [style=invis]; n_2 -> p_3; p_2 -> p_3 [style=invis]; n_2 -> n_3 [style=invis]; n_3 -> null0; p_3 -> null0 [style=invis]; n_3 -> invis0 [style=invis]; } ``` pivot 設為 list 開頭: ```graphviz digraph quicksort { rankdir=LR; # node [shape=record]; # value [label="value|"]; node [shape=box]; list; p_1;n_1 [label="6"]; p_2;n_2 [label="2"]; p_3;n_3 [label="9"]; pivot [color=blue]; # p; invis0 [style=invis]; node [shape=plaintext]; null0 [label="NULL"]; { rank=same; list -> p_1 -> n_1; n_1 -> pivot [arrowtail=normal, dir=both, arrowhead=none]; } { rank=same; p_2 -> n_2; } { rank=same; p_3 -> n_3; } { rank=same; null0 -> invis0 [style=invis]; } n_1 -> p_2; p_1 -> p_2 [style=invis]; n_1 -> n_2 [style=invis]; n_2 -> p_3; p_2 -> p_3 [style=invis]; n_2 -> n_3 [style=invis]; n_3 -> null0; p_3 -> null0 [style=invis]; n_3 -> invis0 [style=invis]; } ``` pivot 指向的 value 賦值給 value 變數: ```graphviz digraph quicksort { rankdir=LR; node [shape=record]; value [label="value|6", color="#225522"]; node [shape=box]; list; p_1;n_1 [label="6"]; p_2;n_2 [label="2"]; p_3;n_3 [label="9"]; pivot [color=blue]; # p; invis0 [style=invis]; node [shape=plaintext]; null0 [label="NULL"]; { rank=same; list -> p_1 -> n_1; n_1 -> pivot [arrowtail=normal, dir=both, arrowhead=none]; } { rank=same; p_2 -> n_2; } { rank=same; p_3 -> n_3; } { rank=same; null0 -> invis0 [style=invis]; } n_1 -> p_2; p_1 -> p_2 [style=invis]; n_1 -> n_2 [style=invis]; n_2 -> p_3; p_2 -> p_3 [style=invis]; n_2 -> n_3 [style=invis]; n_3 -> null0; p_3 -> null0 [style=invis]; n_3 -> invis0 [style=invis]; } ``` p 指到 pivot->next 所指的位置: ```graphviz digraph quicksort { rankdir=LR; node [shape=record]; value [label="value|6", color="#225522"]; node [shape=box]; list; p_1;n_1 [label="6"]; p_2;n_2 [label="2"]; p_3;n_3 [label="9"]; pivot [color=blue]; p [color="#dd5522"]; invis0 [style=invis]; node [shape=plaintext]; null0 [label="NULL"]; { rank=same; list -> p_1 -> n_1; n_1 -> pivot [arrowtail=normal, dir=both, arrowhead=none]; } { rank=same; p_2 -> n_2; n_2 -> p [arrowtail=normal, dir=both, arrowhead=none]; } { rank=same; p_3 -> n_3; } { rank=same; null0 -> invis0 [style=invis]; } n_1 -> p_2; p_1 -> p_2 [style=invis]; n_1 -> n_2 [style=invis]; n_2 -> p_3; p_2 -> p_3 [style=invis]; n_2 -> n_3 [style=invis]; n_3 -> null0; p_3 -> null0 [style=invis]; n_3 -> invis0 [style=invis]; } ``` 將 pivot 所指的 node 斷開於串列: ```graphviz digraph quicksort { rankdir=LR; node [shape=record]; value [label="value|6", color="#225522"]; node [shape=box]; list; p_1;n_1 [label="6"]; p_2;n_2 [label="2"]; p_3;n_3 [label="9"]; pivot [color=blue]; p [color="#dd5522"]; invis0 [style=invis]; invis1 [style=invis]; node [shape=plaintext]; null0 [label="NULL"]; null1 [label="NULL"]; { rank=same; list -> p_1 -> n_1; n_1 -> pivot [arrowtail=normal, dir=both, arrowhead=none]; } { rank=same; p_2 -> n_2; n_2 -> p [arrowtail=normal, dir=both, arrowhead=none]; } { rank=same; p_3 -> n_3; } { rank=same; null0 -> invis0 [style=invis]; } { rank=same; null1 -> invis1 [style=invis]; } n_1 -> null1; p_1 -> null1 [style=invis]; null1 -> p_2 [style=invis]; p_1 -> null1 [style=invis]; n_1 -> invis1 [style=invis]; n_2 -> p_3; p_2 -> p_3 [style=invis]; n_2 -> n_3 [style=invis]; n_3 -> null0; p_3 -> null0 [style=invis]; n_3 -> invis0 [style=invis]; } ``` 以上這段的用意是找出一個 pivot 且將它由串列分離,並以 p 指標紀錄 pivot 的下一個 node ,同時將 pivot 指向的值紀錄在 value 變數中。 以下的程式片段則是繼續以 p 指向的串列,依據 pivot node 的值,分為左右兩串列,左串列都將會比 pivot 小或相等於 pivot ,右串列則會比 pivot 都大。 ```cpp node_t *left = NULL, *right = NULL; while (p) { node_t *n = p; p = p->next; list_add_node_t(n->value > value ? &right : &left, n); /*AAA, BBB*/ } ``` 以上的例子做完後會如下所示: ```graphviz digraph quicksort { rankdir=LR; node [shape=record]; value [label="value|6", color="#225522"]; node [shape=box]; list; p_1;n_1 [label="6"]; p_2;n_2 [label="2"]; p_3;n_3 [label="9"]; pivot [color=blue]; p [color="#dd5522"]; left [color=blue];right [color=blue]; invis0 [style=invis]; invis1 [style=invis]; invis2 [style=invis]; node [shape=plaintext]; null0 [label="NULL"]; null1 [label="NULL"]; null2 [label="NULL"]; null3 [label="NULL"]; { rank=same; list -> p_1 -> n_1; n_1 -> pivot [arrowtail=normal, dir=both, arrowhead=none]; } { rank=same; left -> p_2 -> n_2; } { rank=same; right -> p_3 -> n_3; } { rank=same; null0 -> invis0 [style=invis]; } { rank=same; null1 -> invis1 [style=invis]; } { rank=same; null3 -> invis2 [style=invis]; } n_1 -> null1; p_1 -> null1 [style=invis]; null1 -> p_2 [style=invis]; p_1 -> null1 [style=invis]; n_1 -> invis1 [style=invis]; n_2 -> null3; null3 -> p_3 [style=invis]; invis2 -> n_3 [style=invis]; p_2 -> null3 [style=invis]; n_2 -> invis2 [style=invis]; n_3 -> null0; p_3 -> null0 [style=invis]; n_3 -> invis0 [style=invis]; p -> null2; } ``` ```cpp quicksort(&left); quicksort(&right); ``` 但 left 與 right 都只保證比 pivot 大或小,並不保證串列內部是排序好的,因此針對 left 與 right 進行遞迴呼叫 quicksort 函式。 ```cpp node_t *result = NULL; list_concat(&result, left); list_concat(&result, pivot); /*CCC*/ list_concat(&result, right); /*CCC*/ *list = result; ``` 利用上面所解釋的 list_concat 函式串接三串列,且此函式保證了 result 指標能時時保持指在串列開頭,因此最後只要將 list 指到已串接完成的 result 串列開頭即可。 --- 3. 對應的測試程式如下: ```cpp= static bool list_is_ordered(node_t *list) { bool first = true; int value; while (list) { if (first) { value = list->value; first = false; } else { if (list->value < value) return false; value = list->value; } list = list->next; } return true; } static void list_display(node_t *list) { printf("%s IN ORDER : ", list_is_ordered(list) ? " " : "NOT"); while (list) { printf("%d ", list->value); list = list->next; } printf("\n"); } int main(int argc, char **argv) { size_t count = 20; node_t *list = NULL; while (count--) list = list_make_node_t(list, random() % 1024); list_display(list); quicksort(&list); list_display(list); if (!list_is_ordered(list)) return EXIT_FAILURE; list_free(&list); return EXIT_SUCCESS; } ``` ### 將各函數分開分析如以下各點: #### `list_is_ordered` 函式 ```cpp static bool list_is_ordered(node_t *list) { bool first = true; int value; while (list) { if (first) { value = list->value; first = false; } else { if (list->value < value) return false; value = list->value; } list = list->next; } return true; } ``` 首先看到 return type `bool` 以及 return value `true` 與 `false` , > `節錄自 C 語言規格書 ISO/IEC 9899:1999 - 7.16 章節` > **7.16 Boolean type and values <stdbool.h>** > The header <stdbool.h> defines four macros. The macro **bool** expands to **\_Bool**. > `節錄自 C 語言規格書 ISO/IEC 9899:1999 - 6.2.5 章節` > **6.2.5 Types** > * An object declared as type **\_Bool** is large enough to store the values 0 and 1. > * The remaining three macros are suitable for use in #if preprocessing directives. They are **true** which expands to the integer constant 1, **false** which expands to the integer constant 0, and **\__bool_true_false_are_defined** which expands to the integer constant 1. **bool** 是可以儲存 0 或 1 的型態, **true** 被定義為 1 , **false** 被定義為 0 。要使用它們的話必須先 `#include <stdbool.h>` 。( **\_Bool** 則不用,它是 C 語言關鍵字) > `可參閱 C 語言規格書 6.4.1 Keywords 章節 - Syntax 項目` 此函式在迴圈中分為 `first` 條件與**非** `first` 兩種,因為除了第一次以外,其他次都必須與 value 做比較,第一次進迴圈時 value 是尚未初始化的,也還沒取得第一次的 value ,自然無法做比較。 此函式是為了檢查串列是否已經由小排到大: 第一圈先將第一個 node 的值賦予 value 。 ```graphviz digraph ordered { rankdir=LR; node [shape=record]; value [label="value|8"]; node [shape=box]; n0 [label="8"]; n1 [label="2"]; n2 [label="6"]; node [shape=plaintext]; list; { rank=same; list;n0; } n0 -> n1 -> n2; } ``` 第二圈檢查第二個 node 的值是否 $\leq$ value ,否則 return false ,此處正好大於 value ,直接 return false ,判斷此串列尚未排序好。 ```graphviz digraph ordered { rankdir=LR; node [shape=record]; value [label="value|8"]; node [shape=box]; n0 [label="8"]; n1 [label="2", fontcolor=red]; n2 [label="6"]; node [shape=plaintext]; list; { rank=same; list;value;n1; } n0 -> n1 -> n2; } ``` 若為以下例子: ```graphviz digraph ordered { rankdir=LR; node [shape=record]; value [label="value|2"]; node [shape=box]; n0 [label="2"]; n1 [label="6"]; n2 [label="8"]; node [shape=plaintext]; list; { rank=same; list;n0; } n0 -> n1 -> n2; } ``` 第二圈檢查第二個 node 的值 $<$ value ,則將 value 改為第二個 node 的值,繼續往下比較。 ```graphviz digraph ordered { rankdir=LR; node [shape=record]; value [label="value|6"]; node [shape=box]; n0 [label="2"]; n1 [label="6"]; n2 [label="8"]; node [shape=plaintext]; list; { rank=same; list;value;n1; } n0 -> n1 -> n2; } ``` 若最後都沒有 return false ,則 return true ,判斷此串列已排序。 ```graphviz digraph ordered { rankdir=LR; #node [shape=record]; #value [label="value|6"]; node [shape=box]; n0 [label="2"]; n1 [label="6"]; n2 [label="8"]; node [shape=plaintext]; list;NULL; { rank=same; list;NULL; } n0 -> n1 -> n2 -> NULL; } ``` #### `list_display` 函式 ```cpp static void list_display(node_t *list) { printf("%s IN ORDER : ", list_is_ordered(list) ? " " : "NOT"); while (list) { printf("%d ", list->value); list = list->next; } printf("\n"); } ``` 這個函式就比較單純,先以 `list_is_ordered` 函式判斷串列是否排好序,再依序印出各 node 的值。 #### `main` 函式 ```cpp int main(int argc, char **argv) { size_t count = 20; node_t *list = NULL; while (count--) list = list_make_node_t(list, random() % 1024); list_display(list); quicksort(&list); list_display(list); if (!list_is_ordered(list)) return EXIT_FAILURE; list_free(&list); return EXIT_SUCCESS; } ``` 首先觀察到的是 `random()` 函式,經由 man 手冊我觀察到了以下資訊: > `節錄自 Linux man-pages 5.05` > **RANDOM(3)** > * **CONFORMING TO** > POSIX.1-2001, POSIX.1-2008, 4.3BSD. 可以發現這個函式並不包含於 C 語言標準,因此它只能使用於符合以上標準的系統上。 至於 `EXIT_FAILURE` 與 `EXIT_SUCCESS` 兩個 macro , C 語言規格書寫道: > `節錄自 C 語言規格書 ISO/IEC 9899:1999 - 7.20 章節` > **7.20 General utilities <stdlib.h>** > * The header **<stdlib.h>** declares five types and several functions of general utility, and defines several macros. > ... > * The macros defined are **NULL** (described in 7.17); **EXIT_FAILURE** and **EXIT_SUCCESS** which expand to integer constant expressions that can be used as the argument to the exit function to return unsuccessful or successful termination status, respectively, to the host environment; ... 直接去找 `stdlib.h` 檔案(我的裝置上這個檔案位於 `/usr/include/stdlib.h` ),搜尋 `EXIT_SUCCESS` 關鍵字: ```cpp /* We define these the same for all machines. Changes from this to the outside world should be done in `_exit'. */ #define EXIT_FAILURE 1 /* Failing exit status. */ #define EXIT_SUCCESS 0 /* Successful exit status. */ ``` 因此為了使用這兩個 macro 務必先 `#include <stdlib.h>` 。 回到函式本身,先來看前面的一句敘述: ```cpp size_t count = 20; ``` **size_t** 是一個型態, > `節錄自 C 語言規格書 ISO/IEC 9899:1999 - 6.5.3.4 章節` > **6.5.3.4 The sizeof operator** > * The value of the result is implementation-defined, and its type (an unsigned integer type) is **size_t**, defined in <stddef.h> (and other headers). 實際去查找 `stddef.h` 檔案,發現並不在 `/usr/include/` 中,而是在 `/usr/lib/gcc` 的子資料夾中,是由 GCC 所實作的檔案: ```cpp #ifndef __SIZE_TYPE__ #define __SIZE_TYPE__ long unsigned int #endif #if !(defined (__GNUG__) && defined (size_t)) typedef __SIZE_TYPE__ size_t; ``` 對於 `__GNUG__` 這個 macro ,查閱 GCC 官方文件的 [CPP Manual](https://gcc.gnu.org/onlinedocs/cpp/): > `節錄自 GCC 官方文件 - CPP Manual - 3.7.2 章節` > **3.7.2 Common Predefined Macros** > * **\_\_GNUG\_\_** The GNU C++ compiler defines this. Testing it is equivalent to testing (**\_\_GNUC\_\_** && **\_\_cplusplus**). 這個 macro 是 GNU C++ 編譯器會定義的,因此 size_t 在「不是 GNU C++ 編譯器」或「沒有定義 size_t」的情況下會將 size_t 定義為 `long unsigned int` 型態。 回到函式, ```cpp size_t count = 20; node_t *list = NULL; while (count--) list = list_make_node_t(list, random() % 1024); ``` 由程式可以猜測這邊想要隨機產生 20 個介在 0 ~ 1023 的數字,並加入到 list 中,這邊題目沒有定義,稍後會補上這個函式的詳細定義。 ```cpp list_display(list); quicksort(&list); list_display(list); ``` 這邊程式會呼叫 `list_display` 函式印出排序前的數字,並以 `quicksort` 函式真正執行排序,再印出排序後的結果。 ```cpp if (!list_is_ordered(list)) return EXIT_FAILURE; ``` 這邊是以 `list_is_ordered` 函式驗證排序是否正確,若失敗則 return EXIT_FAILURE 。 ```cpp list_free(&list); return EXIT_SUCCESS; ``` 若剛才的驗證沒有失敗,則呼叫 `list_free` 函式(而這個函式題目也是尚未定義的,稍後會補上詳細定義,猜測函式的目的是要釋放 `list_make_node_t` 函式配置的記憶體空間),最後視為成功而 return EXIT_SUCCESS 。 ### 補充尚未定義的函式 #### `list_make_node_t` 函式 ```cpp static node_t *list_make_node_t(node_t *list, int n) { node_t *tmp = (node_t *)malloc(sizeof(node_t)); tmp->value = n; tmp->next = list; return tmp; } ``` 這次 `list` 參數以 `node_t *` 型態傳入, `list` 這個指標只會複製它的值進入函式,不會修改到傳入的指標本身,因此只能選擇將新配置的 node 指標 return 回去。 #### `list_free` 函式 ```cpp static void list_free(node_t **list) { while (*list) { node_t *tmp = *list; *list = (*list)->next; free(tmp); } } ``` 傳入的參數是 node_t 指標的指標 `list` ,可以直接更動到 `*list` 的值,先檢查 `*list` 是否為空,若否,則先以 `tmp` 儲存 `*list` 指標值,這是稍後要釋放的空間,再將 `*list` 指標往後移動一個 node ,再來就是釋放 tmp 中配置的空間,最後重新回迴圈開頭檢查 `*list` 是否為空,直到所有 node 釋放完畢。 ### 實際執行 ```shell NOT IN ORDER : 1016 84 706 124 326 483 763 498 939 186 205 809 236 74 255 81 115 105 966 359 IN ORDER : 74 81 84 105 115 124 186 205 236 255 326 359 483 498 706 763 809 939 966 1016 ``` ### Valgrind 檢查記憶體洩漏 ```shell ==44461== Memcheck, a memory error detector ==44461== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al. ==44461== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info ==44461== Command: ./list ==44461== NOT IN ORDER : 1016 84 706 124 326 483 763 498 939 186 205 809 236 74 255 81 115 105 966 359 IN ORDER : 74 81 84 105 115 124 186 205 236 255 326 359 483 498 706 763 809 939 966 1016 ==44461== ==44461== HEAP SUMMARY: ==44461== in use at exit: 0 bytes in 0 blocks ==44461== total heap usage: 21 allocs, 21 frees, 1,344 bytes allocated ==44461== ==44461== All heap blocks were freed -- no leaks are possible ==44461== ==44461== For lists of detected and suppressed errors, rerun with: -s ==44461== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0) ``` 結果: `list_free` 順利執行,成功釋放已配置的記憶體空間。