# 2021q1 Homework1 (quiz1) contributed by < `AmyLin0210` > ###### tags: `2021q1 Linux` > [2021q1 第一周測驗題](https://hackmd.io/@sysprog/linux2021-quiz1) ## 解釋程式運作原理 ### 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; } ``` 將 list 接到 node_t 的後方,並將 node_t 變成新的 list head ```graphviz digraph structs { rankdir=LR; node[shape=box]; node1[label= node_t]; { head[label= "head" shape= plaintext, fontcolor=red] list0[label= A]; list1[label= B]; list2[label= C]; } { rank="same"; head->list0; } node1->list0->list1->list2 } ``` ### list_concat ```c= static inline void list_concat(node_t **left, node_t *right) { while (*left) left = &((*left)->next); *left = right; } ``` 在第二及第三行的迴圈中做的事情為,找到 left 最後方 node 的 next,並使 *left 指向那個位置 ```graphviz digraph structs { rankdir=LR; node[shape=box]; head[label= "*left" shape= plaintext, fontcolor=red]; null[label= "NULL" shape= plaintext]; l1[label= "L1"]; l2[label= "L2"]; l3[label= "L3"]; { rank="same"; head->null; } l1->l2->l3->null; } ``` 在第四行的地方,將 *left 指向 right,使得 left 與 right 連接起 ```graphviz digraph structs { rankdir=LR; node[shape=box]; head[label= "*left" shape= plaintext, fontcolor=red]; l1[label= "L1"]; l2[label= "L2"]; l3[label= "L3"]; r1[label= "R1"]; r2[label= "R2"]; { rank="same"; head->r1; } l1->l2->l3->r1->r2; } ``` 當遇到 *left 指向 NULL 的狀況時,指標的指標優勢便體現出來 - 當 *left 指向 NULL 時,會直接跳出迴圈,故指標位置示意圖如下 ```graphviz digraph structs { rankdir=LR; head[label= "*left" shape= plaintext, fontcolor=red]; null[label= "NULL" shape= plaintext]; { rank="same"; head->null; } } ``` - 在 `*left = right` 中直接將 *left 指向 right ```graphviz digraph structs { rankdir=LR; node[shape=box]; head[label= "*left" shape= plaintext, fontcolor=red]; { rank="same"; head->R1; } R1->R2 } ``` ### 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); } quicksort(&left); quicksort(&right); node_t *result = NULL; list_concat(&result, left); list_concat(&result, pivot); list_concat(&result, right); *list = result; } ``` 使用 quick sort 會取出一個作為比較的 pivot 值, 在這段程式碼中,將 list 的第一個 node 做為 pivot 取下, 並以他的 value 當接下來的比較基準。 ```graphviz digraph structs { rankdir=LR; node[shape=box]; A[label= "A"] B[label= "B"] C[label= "C"] pivot[label= "pivot"]; value[label= "value" shape= plaintext fontcolor="blue"]; p[label="p" shape= plaintext fontcolor="red"]; null[label= "NULL" shape= plaintext]; { rank="same"; value->pivot; } { rank="same"; p->A; } pivot->null; A->B->C } ``` 在 `list_add_node_t(n->value > value ? AAA : BBB, n)` 中, - 如果 n 的值比較大,將它與 right 進行連接, - 如果 n 的值比較小,將它與 left 進行連接。 ```flow right=>operation: 連接 right left=>operation: 連接 left cond=>condition: n->value > value cond(yes)->right cond(no)->left ``` 反覆對 right 與 left 這兩條 linked list 做 quick sort, 全部排序完後做將左右兩條串連起來。 - `list_concat(&result, left)` 由 list_concat 的函式介紹中,可以發現若第一個參數的值指向 NULL,透過函式執行後,會直接將第一個函式的值指向第二個參數的位置。 ```graphviz digraph structs { rankdir=LR; node[shape=box]; result[label= "result" shape= plaintext fontcolor="red"] { rank="same"; result->L1; } L1->L2->L3; } ``` - `list_concat(&result, pivot)` 將原先的 left 與 pivot 連起 ```graphviz digraph structs { rankdir=LR; node[shape=box]; result[label= "result" shape= plaintext fontcolor="red"] { rank="same"; result->L1; } L1->L2->L3->pivot; } ``` - `list_concat(&result, right)` 最後再將 right 連上 ```graphviz digraph structs { rankdir=LR; node[shape=box]; result[label= "result" shape= plaintext fontcolor="red"] { rank="same"; result->L1; } L1->L2->L3->pivot->R1->R2->R3; } ``` ### 測試程式中 random 結果相仿問題 > :::warning > 有什麼證據可說明「rand() 函式使用的是亂數表」呢?工程人員說話要精準確實。 > :notes: jserv > ::: >> The rand function computes a sequence of pseudo-random integers in the range 0 to RAND_MAX. > 以上節錄自 [ISO/IEC 9899 (a.k.a C99 Standard)](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf) 中的 7.20.2.1,由此可知 rand 函式使用了 pseudo-random integers 若按照題目給的程式碼修改編譯執行,由於 rand() 函式使用的是 pseudo-random integers,在程式執行時沒有給定不同的亂數種子,因此導致執行後結果相仿。 > NOT IN ORDER : 359 966 105 115 81 255 74 236 809 205 186 939 498 763 483 326 124 706 84 1016 IN ORDER : 74 81 84 105 115 124 186 205 236 255 326 359 483 498 706 763 809 939 966 1016 在這裡我的改進方式是,加入亂數種子 `srand((unsigned int)time(NULL))`。 由於 `time(NULL)` 會回傳目前時間距離 (00:00:00 UTC, January 1, 1970) 這個時間點的秒數,所以使得程式執行後的結果不同。 ## 非遞迴方式 參考 [Optimized QuickSort — C Implementation (Non-Recursive)](https://alienryderflex.com/quicksort/)。 ### doubly-linked list 由於此連結中使用到儲存數列的資料型態為陣列,會有從陣列最後方反過來尋找比對的狀況,所以我改變我的資料型態為 doubly-linked list。 :::danger 變更資料結構會造成不相容 (incompatibility),你需要考慮到這部分的衝擊。 :notes: jserv ::: ```cpp typedef struct __node { int value; struct __node *next; struct __node *prev; } node_t; ``` 我的 quick sort 程式碼: ```cpp void quicksort(node_t *head, node_t *tail, int length) { if (!head) return; node_t *beg[length], *end[length], *L, *R; int i=0, piv; beg[0] = head; end[0] = tail; while (i >= 0) { L = beg[i]; R = end[i]; if( L != R && L && R) { piv = beg[i]->value; while (L != R) { while ( R->value >= piv && L != R ) R = R->prev; if (L != R) { L->value = R->value; L = L->next; } while ( L->value <= piv && L != R ) L = L->next; if (L != R) { R->value = L->value; R->prev; } } L->value = piv; beg[i+1] = L->next; end[i+1] = end[i]; end[i++] = L; } else { i--; } } } ``` 傳入的三個參數分別為 - **head** : 此 linked list 的最前端 - **tail** : 此 linked list 的最末端 - **length** : 此 linked list 共有多少數 運作邏輯: 1. 每一對的 `beg` 與 `end` 會分別對應到要比較的數列最前方與最後方,並取 `beg` 的值作為 `piv`。 ```graphviz digraph structs { rankdir=LR; node[shape=box]; piv[label= "piv" shape= plaintext fontcolor="blue"] beg0[label= "beg[0]" shape= plaintext fontcolor="red"] end0[label= "end[0]" shape= plaintext fontcolor="red"] L[label= "L" shape= plaintext fontcolor="red"] R[label= "R" shape= plaintext fontcolor="red"] 161[label= "161" fontcolor="blue"] { rank= "same"; piv->161; } { rank= "same"; L->beg0->161; } { rank= "same"; R->end0->933; } 161->5->421->873->964->93->933; } ``` 2. 從 R 的地方開始向左方比較,找到第一個比 piv 值小的 ```graphviz digraph structs { rankdir=LR; node[shape=box]; L[label= "L" shape= plaintext fontcolor="red"] R[label= "R" shape= plaintext fontcolor="red"] 93[label= "93" fontcolor="red"] 161[label= "161" fontcolor="blue"] { rank= "same"; L->161; } { rank= "same"; R->93; } 161->5->421->873->964->93->933; piv[label= "piv:161" shape= plaintext fontcolor="blue"] } ``` 3. 將 L 的數值代換成剛剛找到的比 piv 小的數 ( R 的數值 ),並將 L 向右位移一個位置 ```graphviz digraph structs { rankdir=LR; node[shape=box]; L[label= "L" shape= plaintext fontcolor="red"] R[label= "R" shape= plaintext fontcolor="red"] R93[label= "93" fontcolor="red"] L93[label= "93" fontcolor="red"] { rank= "same"; L->5; } { rank= "same"; R->R93; } L93->5->421->873->964->R93->933; piv[label= "piv:161" shape= plaintext fontcolor="blue"] } ``` 4. 從 L 的地方開始向右方比較,找到第一個比 piv 值大的數值 ```graphviz digraph structs { rankdir=LR; node[shape=box]; L[label= "L" shape= plaintext fontcolor="red"] R[label= "R" shape= plaintext fontcolor="red"] R93[label= "93" fontcolor="red"] L93[label= "93" fontcolor="black"] 421[label= "421" fontcolor="red"] { rank= "same"; L->421; } { rank= "same"; R->R93; } L93->5->421->873->964->R93->933; piv[label= "piv:161" shape= plaintext fontcolor="blue"] } ``` 5. 將 R 的數值代換成剛剛找到的比 piv 大的數 ( L 的數值 ),並將 R 向左位移一個位置 ```graphviz digraph structs { rankdir=LR; node[shape=box]; L[label= "L" shape= plaintext fontcolor="red"] R[label= "R" shape= plaintext fontcolor="red"] R421[label= "421" fontcolor="red"] L421[label= "421" fontcolor="red"] { rank= "same"; L->L421; } { rank= "same"; R->964; } 93->5->L421->873->964->R421->933; piv[label= "piv:161" shape= plaintext fontcolor="blue"] } ``` 6. 重複步驟 2~5 直到 L 與 R 重疊 ```graphviz digraph structs { rankdir=LR; node[shape=box]; R[label= "R" shape= plaintext fontcolor="red"] L[label= "L" shape= plaintext fontcolor="red"] R421[label= "421" fontcolor="black"] L421[label= "421" fontcolor="red"] { rank= "same"; L->L421; } { rank= "same"; R->L421; } 93->5->L421->873->964->R421->933; piv[label= "piv:161" shape= plaintext fontcolor="blue"] } ``` 7. 將重疊處的數值換為 piv ```graphviz digraph structs { rankdir=LR; node[shape=box]; R[label= "R" shape= plaintext fontcolor="red"] L[label= "L" shape= plaintext fontcolor="red"] 161[label= "161" fontcolor= "blue"] { rank= "same"; L->161; } { rank= "same"; R->161; } 93->5->161->873->964->421->933; } ``` 8. 將最左邊的數丟為下次的比較起始位置,將重疊位置丟為下次比較的最終位置 ```graphviz digraph structs { rankdir=LR; node[shape=box]; beg0[label= "beg[0]" shape= plaintext fontcolor="red"] end0[label= "end[0]" shape= plaintext fontcolor="red"] 161[label= "161" fontcolor= "blue"] { rank= "same"; beg0->93; } { rank= "same"; end0->161; } 93->5->161->873->964->421->933; } ``` 9. 將重疊部份右移一位的數丟為下次的比較起始位置,將最右端位置丟為下次比較的最終位置 ```graphviz digraph structs { rankdir=LR; node[shape=box]; beg0[label= "beg[0]" shape= plaintext fontcolor="red"] end0[label= "end[0]" shape= plaintext fontcolor="red"] beg1[label= "beg[1]" shape= plaintext fontcolor="orange"] end1[label= "end[1]" shape= plaintext fontcolor="orange"] 161[label= "161" fontcolor= "blue"] { rank= "same"; beg0->93; } { rank= "same"; end0->161; } { rank= "same"; beg1->873; } { rank= "same"; end1->933; } 93->5->161->873->964->421->933; } ``` 10. 重複進行 1~9 直到排序完成 :::warning TODO: 1. 加上時間和空間複雜度的分析 2. 說明 [Optimized QuickSort — C Implementation (Non-Recursive)](https://alienryderflex.com/quicksort/) 指出進一步的效能提升機會 3. 避免修改為 doubly-linked list,沿用原本的 singly-linked list 設定 :notes: jserv ::: ### singly-linked list (array 思維) 在最初改動時,由於整體思路仍無法突破 array 的思維,所以程式碼十分冗長,需要用多個變數來存取每個階段的資訊。 邏輯如下: 1. 每一對的 `beg` 與 `end` 會分別對應到要比較的數列最前方與最後方,並取 `beg` 的值作為 `piv`。 ```graphviz digraph structs { rankdir=LR; node[shape=box]; piv[label= "piv" shape= plaintext fontcolor="blue"] tail[label= "tail" shape= plaintext fontcolor="red"] beg0[label= "beg[0]" shape= plaintext fontcolor="red"] end0[label= "end[0]" shape= plaintext fontcolor="red"] cur[label= "cur" shape= plaintext fontcolor="red"] 161[label= "161" fontcolor="blue"] { rank= "same"; piv->161; } { rank= "same"; cur->beg0->161; } { rank= "same"; tail->end0->933; } 161->5->421->873->964->93->933; } ``` 2. 從最左方的地方開始向右方比較,找到第一個比 piv 值大的 ```graphviz digraph structs { rankdir=LR; node[shape=box]; tail[label= "tail" shape= plaintext fontcolor="red"] cur[label= "cur" shape= plaintext fontcolor="red"] 421[label= "421" fontcolor="red"] 161[label= "161" fontcolor="blue"] { rank= "same"; cur->421; } { rank= "same"; tail->933; } 161->5->421->873->964->93->933; piv[label= "piv:161" shape= plaintext fontcolor="blue"] } ``` 3. 將比 piovt 大的值移到 `tail` 後方,並把 `cur` 位移一格 ```graphviz digraph structs { rankdir=LR; node[shape=box]; tail[label= "tail" shape= plaintext fontcolor="red"] cur[label= "cur" shape= plaintext fontcolor="red"] 421[label= "421" fontcolor="red"] 161[label= "161" fontcolor="blue"] { rank= "same"; cur->873; } { rank= "same"; tail->933; } 161->5->873->964->93->933->421; piv[label= "piv:161" shape= plaintext fontcolor="blue"] } ``` 4. 重複步驟2與步驟3,直到 `cur` 與 `tail` 重疊 ```graphviz digraph structs { rankdir=LR; node[shape=box]; tail[label= "tail" shape= plaintext fontcolor="red"] cur[label= "cur" shape= plaintext fontcolor="red"] 161[label= "161" fontcolor="blue"] { rank= "same"; cur->tail->933; } 161->5->93->933->421->873->964; piv[label= "piv:161" shape= plaintext fontcolor="blue"] } ``` 5. 將 piv 與 cur 指向的值做比較 - 假設 cur 指向的值比 piv 大,cur 所在的位置會是比較的起始位置 ```graphviz digraph structs { rankdir=LR; node[shape=box]; tail[label= "tail" shape= plaintext fontcolor="red"] cur[label= "cur" shape= plaintext fontcolor="red"] beg1[label= "beg[1]" shape= plaintext fontcolor="green"] end1[label= "end[1]" shape= plaintext fontcolor="green"] beg0[label= "beg[0]" shape= plaintext fontcolor="navy"] end0[label= "end[0]" shape= plaintext fontcolor="navy"] 161[label= "161" fontcolor="blue"] { rank = "same"; beg1->933; } { rank = "same"; end1->964 } { rank = "same"; beg0->161; } { rank = "same"; end0->93 } { rank= "same"; cur->tail->933; } 161->5->93->933->421->873->964; piv[label= "piv:161" shape= plaintext fontcolor="blue"] } ``` - 假設 cur 指向的值比 piv 小,cur 所在的位置會是比較的結束位置 ```graphviz digraph structs { rankdir=LR; node[shape=box]; tail[label= "tail" shape= plaintext fontcolor="red"] cur[label= "cur" shape= plaintext fontcolor="red"] beg1[label= "beg[1]" shape= plaintext fontcolor="green"] end1[label= "end[1]" shape= plaintext fontcolor="green"] beg0[label= "beg[0]" shape= plaintext fontcolor="navy"] end0[label= "end[0]" shape= plaintext fontcolor="navy"] 161[label= "161" fontcolor="blue"] { rank = "same"; beg1->933; } { rank = "same"; end1->964 } { rank = "same"; beg0->161; } { rank = "same"; end0->93 } { rank= "same"; cur->tail->93; } 161->5->93->933->421->873->964; piv[label= "piv:161" shape= plaintext fontcolor="blue"] } ``` ```cpp= void quicksort(node_t *head, int length) { if (!head) return; node_t *beg[length], *end[length], **tmp, **cur, *tail, *temp; node_t *cur_beg, *cur_end; int i=0, piv; /* find the tail of this liked list */ tail = head; while (tail->next) tail = tail->next; beg[0] = head; end[0] = tail; while (i >= 0) { cur_beg = beg[i]; cur_end = end[i]; cur = &cur_beg; tail = cur_end; beg[i+1] = *cur; if( cur_beg != cur_end && cur_beg && cur_end) { piv = cur_beg->value; while (*cur != cur_end) { /** * if the current node value is larger than value of pivot, * move it to the tail. **/ while (*cur != cur_end && (*cur)->value <= piv) { end[i] = *cur; cur = &((*cur)->next); } if (*cur == cur_end) break; temp = tail->next; tail->next = *cur; *cur = (*cur)->next; tail = tail->next; tail->next = temp; } /** * Now we have a sorted linked. * The right side of current node is larger than the pivot, * and the left side of current node is less than the pivot. * However we have not processed current node yet. * There are two condition need to be thought about: * * - If the value of current node is larger than the pivot, * the linked list should be cutted as * beg - the node before current node | current node - tail * * - If the value of current node is less than the pivot, * the linked list should be cutted as * beg - current node | the node after current node - tail */ if ((*cur)->value > piv) { beg[i+1] = *cur; beg[i]->value = end[i]->value; end[i]->value = piv; } if ((*cur)->value <= piv) { if( tail == *cur) { beg[i+1] = *cur; beg[i]->value = tail->value; tail->value = piv; } else { end[i] = *cur; beg[i+1] = (*cur)->next; beg[i]->value = end[i]->value; end[i]->value = piv; } } end[++i] = tail; } else { i--; } } } ``` ### singly-linked list 在上面使用 array 思維但使用 linked list 實做時遇到很多例外要處理,意識到即使可以用 linked list 來實做類似 array 的內容,但是每種不同的資料型態適合的實做並不相同。 在實做我曾經遇到的 bug 為,若是有相同的數字需要排序,那我的程式會陷入無窮迴圈。 後來發現原因是我將 pivot 和與他的數字相同的 node 放在同一邊,根據 quick sort 的運作邏輯,它會將 node 與 pivot 比較並逐次拆解,直到只剩下最後一個 node ,再慢慢依序連接回去,若將 pivot 和與它相同優先序的 node 放在同邊的話,會導致永遠無法拆解至只剩下一個 node。 在這裡我將它改寫為以下程式碼: ```cpp= void quicksort(node_t **list) { if (!*list) return; node_t *sorted = NULL; node_t *stack[MAX_LENGTH] = {NULL}; int stack_count = 0; stack[stack_count] = *list; while (stack_count >= 0) { if (!stack[stack_count]->next) { list_add_node_t(&sorted, stack[stack_count]); stack_count--; continue; } node_t *left = NULL, *right = NULL, *tmp = stack[stack_count]->next; list_add_node_t(&right, stack[stack_count]); while (tmp) { node_t *n = tmp; tmp = tmp->next; list_add_node_t((n->value > stack[stack_count]->value)? &right:&left, n); } --stack_count; if (left) stack[++stack_count] = left; if (right) stack[++stack_count] = right; } *list = sorted; } ``` 最近在看 [並行和多執行緒程式設計系列講座](/@sysprog/concurrency) 的教材內容,當中提到 mutex 與 semaphores 時會特別解釋在不同的意境下,就會有相對合適的實做方式,經過這幾次對 quick sort 的實做後開始強烈的意識到,不同的意境需要搭配上不同實做的重要性。 在最初的實做儲存迭代資訊時,我所儲存的都是 linked list 中的某個位置,就如同儲存陣列的 index。陣列由於連續記憶體空間的特性可以快速存取特定 index 的數值,指標卻需要一個一個的走訪,尤其是在 singly-linked list 需要存取已經走訪過得 node 時,更是顯得不便。 在最後的版本中,利用了 linked-list 可以簡單的連接 node 的特性,將需要比較的數值拆做 left 與 right 兩條 linked-list,比起使用 index 比較的思維,這個版本明顯的清晰許多。 即使相同的功能可以使用不同的實做完成,但如果能夠清晰的了解資料結構與問題本身的特性,選擇了適合的實做,在寫程式碼的過程中可以減少處理例外的狀況,易讀性也會大大提昇。