--- tags: 進階電腦系統理論與實作 --- # 2020q3 Homework1 (quiz1) contributed by < `RinHizakura` > [2020q3 第 1 週測驗題](https://hackmd.io/@sysprog/sysprog2020-quiz1) [GitHub](https://github.com/RinHizakura/linked-list) ## 程式運作原理 :::warning 注意圖中的所有 `head` 是以 main(caller) 裡的 `head` pointer 的角度,而不是做為參數的 pointer to pointer 的 `head` ::: ### `add_entry` ```cpp void add_entry(node_t **head, int new_value) { node_t **indirect = head; node_t *new_node = malloc(sizeof(node_t)); new_node->value = new_value; new_node->next = NULL; assert(new_node); while (*indirect) indirect = &(*indirect)->next; *indirect = new_node; } ``` 把一個 node 加到 linked list 的尾端: * 這裡使用到了 pointer to pointer 的技巧,indirect 是指向 head pointer 的 pointer * 首先,透過 malloc 建立一個新的 node 空間,填入 value 欄位後將其指向 NULL(因為 new_node 會放在尾端) ```graphviz digraph graphname{ node[shape=box] head[shape=plaintext,fontcolor=red] indirect[shape=plaintext,fontcolor=blue] rankdir=LR { rankdir = LR A[label=A] B[label=B] C[label=C] D[label=new_node] NULL1[label=NULL,shape=plaintext] NULL2[label=NULL,shape=plaintext] } { rank="same"; indirect[shape=plaintext,fontcolor=blue] indirect->head->A } A->B->C->NULL1 D->NULL2 } ``` * 透過 assert 去檢查 new_node 是否為 NULL(malloc 是否失敗),因此 `AA1` = `assert(new_node)` * 我認為這個 `assert` 的位置需要調整!詳細請閱讀往下的 **Issue 1: where to use assert** * indirect 如果指向的位置上不是 NULL,就往指向位置存放的 node 的下一個 node 指去,直到指到的位置上是放 NULL ```graphviz digraph graphname{ node[shape=box] head[shape=plaintext,fontcolor=red] indirect[shape=plaintext,fontcolor=blue]; n[label="(addr of C's next)",shape=plaintext] rankdir=LR { rankdir = LR A[label=A] B[label=B] C[label=C] D[label=new_node] NULL1[label=NULL,shape=plaintext] NULL2[label=NULL,shape=plaintext] } { rank="same"; head->A } { rank="same"; indirect->n->NULL1 } A->B->C->NULL1 D->NULL2 } ``` * 這個位置就是 new_node 要放的地方,所以 `AA2` = `*indirect = new_node` ```graphviz digraph graphname{ node[shape=box] head[shape=plaintext,fontcolor=red] indirect[shape=plaintext,fontcolor=blue]; n[label="(addr of C's next)",shape=plaintext] rankdir=LR { rankdir = LR A[label=A] B[label=B] C[label=C] D[label=new_node] NULL1[label=NULL,shape=plaintext] } { rank="same"; head->A } { rank="same"; indirect->n->D } A->B->C->D->NULL1 } ``` #### Issue 1: where to use assert 為了模擬 malloc 出 NULL 的情境,設計了一個總是回傳 NULL 的 MALLOC,假裝配置記憶體失敗。 ```cpp= static inline node_t *MALLOC(size_t size){ return NULL; } void add_entry(node_t **head, int new_value) { node_t **indirect = head; node_t *new_node = MALLOC(sizeof(node_t)); new_node->value = new_value; new_node->next = NULL; assert(new_node); while (*indirect) indirect = &(*indirect)->next; *indirect = new_node; } ``` 意料之中,上面 `add_entry` 的寫法在 `assert` 前就有透過 `new_node->value = new_value` 去存取 new_node,因此會產生 Segmentation fault。 > Segmentation fault (core dumped) 從 valgrind 偵測的結果來看,問題也確實是如此 (linked_list.c:16 等同上面程式的第 11 行)。 ``` ==108297== Invalid write of size 4 ==108297== at 0x1092C4: add_entry (linked_list.c:16) ==108297== by 0x1090F7: main (main.c:10) ==108297== Address 0x0 is not stack'd, malloc'd or (recently) free'd ``` 至於如果改成把 `assert` 接續在 `malloc` 之後,測試後會得到以下結果。 ``` exec: linked_list.c:16: add_entry: Assertion `new_node' failed. Aborted (core dumped) ``` 從兩個結果的差異來思考。首先,如果我們的目的只是希望整個 linked list 的操作符合預期,並且在發生錯誤時 shut down,那麼是不是放不放 `assert` 都沒關係呢?(反正 dereference pointer 會發生 exception?) 為此我們需要了解 NULL pointer 的一些細節,在 C 語言規格書中對 NULL pointer 的定義是: > **6.3.2.3 Pointers:** > An integer constant expression with the value 0, or such an expression cast to type void *, is called a null pointer constant. If a null pointer constant is converted to a pointer type, the resulting pointer, called a null pointer, is guaranteed to compare unequal to a pointer to any object or function. 因此,`int* ptr = (int *)0;` 或者 `int* ptr = (int *)(void *)0` 都同等於是 NULL pointer。而規格書裡也有提到: > Conversion of a null pointer to another pointer type yields a null pointer of that type. Any two null pointers shall compare equal. 所以下面的程式會印出 `equal!!`。 ```c= int main() { int* ptr = (int *)0; int* ptr2 = (int *)(void *)0; if(ptr == ptr2) printf("equal!!\n"); return 0; } ``` 而 dereference 的操作,在規格書裡如是說: > **6.5.3.2 Address and indirection operators** > If an invalid value has been assigned to the pointer, the behavior of the unary * operator is undefined Deference NULL pointer 是 undefined behavior。在 [Null pointer](https://en.wikipedia.org/wiki/Null_pointer#cite_note-2) 中也提到,在某些系統上 dereference NULL pointer 是可能會繼續執行然而導致非預期的結果的。所以如果想要即時的掌握錯誤,`assert` 應該接續在 malloc 之後,才能保證即時的檢查到錯誤。 #### Issue 2: when to use assert? 我們在 [lab0](https://hackmd.io/SIIKhe9FT-S1fFVV_7Kg5g) 的時候,以類似的插入節點的 function `q_insert_head` 為例,對 malloc 的檢查是用 `if` 分支,雖然也對 malloc 的失敗做了處理,但與 `assert` 程式會保持 linked list 的原始模樣但程式不會直接 crash。 ```c= newh = malloc(sizeof(list_ele_t)); if (newh == NULL) return false; ``` 那麼在真實的應用上,在甚麼情境下使用哪個方法可能會比較好呢? assert 的中文是**斷言**,因此,程式碼的 assert 就像是在對閱讀程式碼的人說 「這一行 statement 必定是這種情況」,當出現程式撰寫者預期外的情況,則必須要回頭來檢查錯誤。因此,assert 基本就是一個比較粗糙、但方便的 debug 工具,我們可以在編譯中加上 `-DNDEBUG` 將其變成 no operation。 使用 if 檢查的情況,則比較像是我們預期使用程式的人可能誤用(錯誤的參數等),程式可以容忍使用者犯傻,告訴他 「這個操作不合法喔」,然後維持正確的狀態讓程式往下執行。 (議題細節待詳細補充) #### My conclusion 因此,結論來說,以上面的程式而言,如果我們的目的只是希望在發生錯誤時 shut down,考慮到 dereference 也有可能不會發生 exception,所以 `assert` 的存在仍然是必要的,至於 `assert` 該放在原本的位置或者接續在 `malloc` 之後,以此為目的的話都可以。但是如果我們想要在出錯時可以直接抓到錯誤之處,因為 Segmentation fault 並不會直接反映出錯的地方,或者考慮到使用 `assert` 的邏輯的話,我會認為接續在 `malloc` 之後才是正確的。 ### `find_entry` ```cpp node_t *find_entry(node_t *head, int value) { node_t *current = head; for (; current && current->value != value; current = current->next) /* interate */; return current; } ``` 從 head 開始,逐步尋找符合儲存的值為 `value` 的 node。 ### `remove_entry` ```cpp void remove_entry(node_t **head, node_t *entry) { node_t **indirect = head; while ((*indirect) != entry) indirect = &(*indirect)->next; *indirect = entry->next; free(entry); } ``` 移除目標的 entry。pointer to pointer 的使用手法類似前面,假設初始為下圖,而 `head` 是 A,`entry` 是 C。 ```graphviz digraph graphname{ node[shape=box] head[shape=plaintext,fontcolor=red] indirect[shape=plaintext,fontcolor=blue]; n[shape=plaintext,label="entry"] m[shape=plaintext,label="(addr of B's next)"] rankdir=LR { rankdir = LR A[label=A] B[label=B] C[label=C] D[label=D] NULL1[label=NULL,shape=plaintext] } { rank="same"; indirect->head->A } { rank="same"; m->C } n->C A->B->C->D->NULL1 } ``` * 一直前進,直到找到 `entry` ```graphviz digraph graphname{ node[shape=box] head[shape=plaintext,fontcolor=red] indirect[shape=plaintext,fontcolor=blue]; n[shape=plaintext,label="entry"] m[shape=plaintext,label="(addr of B's next)"] rankdir=LR { rankdir = LR A[label=A] B[label=B] C[label=C] D[label=D] NULL1[label=NULL,shape=plaintext] } { rank="same"; head->A } { rank="same"; indirect->m->C } n->C A->B->C->D->NULL1 } ``` * 把 `B->next` 下的節點換成 D,於是就變成了 ```graphviz digraph graphname{ node[shape=box] head[shape=plaintext,fontcolor=red] indirect[shape=plaintext,fontcolor=blue]; rankdir=LR { rankdir = LR A[label=A] B[label=B] C[label=C] D[label=D] NULL1[label=NULL,shape=plaintext] } { rank="same"; head->A } { rank="same"; n[shape=plaintext,label="entry"] m[shape=plaintext,label="(addr of B's next)"] n->C indirect->m->D } A->B->D->NULL1 } ``` * 最後再把 `entry` 指向的節點 C 釋放掉,大功告成 ### `swap_pair` ```cpp node_t *swap_pair(node_t *head) { for (node_t **node = &head; *node && (*node)->next; node = &((*node)->next->next)) { node_t *tmp = *node; *node = (*node)->next; tmp->next = (*node)->next; (*node)->next = tmp; } return head; } ``` 將節點兩兩交換(第 0 個跟第 1 個交換、第 2 個跟第 3 個......)。下面以前兩個 node 的操作來解釋 swap。 ```graphviz digraph graphname{ node[shape=box] head[shape=plaintext,fontcolor=red] rankdir=LR { rankdir = LR A[label=A] B[label=B] NULL1[label="...",shape=plaintext] } { rank="same"; n[shape=plaintext,label="node"] n->head->A } A->B->C->NULL1 } ``` * 首先 `node_t *tmp = *node` ```graphviz digraph graphname{ node[shape=box] head[shape=plaintext,fontcolor=red,label="head(tmp)"] rankdir=LR { rankdir = LR A[label=A] B[label=B] NULL1[label="...",shape=plaintext] } { rank="same"; n[shape=plaintext,label="node"] n->head->A } A->B->C->NULL1 } ``` * 然後,把 node 指到的地址換成下一個節點的地址。因此 `BB2` 會是 `*node = (*node)->next;` 。注意到因為交換的是節點的位址,因此第一個 iteration 的這一步會把 head 的位址變成 B 的位址,也就是讓 head 指向變成開頭的 B。 ```graphviz digraph graphname{ node[shape=box] tmp[shape=plaintext] n[shape=plaintext,label="node"] m[shape=plaintext,label="head\n(original addr of A's next)",fontcolor=red] rankdir=LR { rankdir = LR A[label=A] B[label=B] NULL1[label="...",shape=plaintext] } { rank="same"; tmp->A } { rank="same"; n->m->B } A->B->C->NULL1 } ``` * 再來是 `tmp->next = (*node)->next` ```graphviz digraph graphname{ node[shape=box] tmp[shape=plaintext,label="tmp"] n[shape=plaintext,label="node"] m[shape=plaintext,label="head\n(original addr of A's next)",fontcolor=red] rankdir=LR { rankdir = LR A[label=A] B[label=B] NULL1[label="...",shape=plaintext] } { rank="same"; tmp->A } { rank="same"; n->m->B } B->C A->C->NULL1 } ``` * 最後是 `(*node)->next = tmp`。 ```graphviz digraph graphname{ node[shape=box] tmp[shape=plaintext,label="tmp"] n[shape=plaintext,label="node"] m[shape=plaintext,label="head\n(original addr of A's next)",fontcolor=red] rankdir=LR { rankdir = LR A[label=A] B[label=B] NULL1[label="...",shape=plaintext] } { rank="same"; tmp->A } { rank="same"; n->m->B } B->A->C->NULL1 } ``` * 把 A、B 處理完後,要把 node 指向的位址換到 C 去。所以 `BB1` 的答案是 `node = &((*node)->next->next)`,選項裡好像沒有正確答案? ```graphviz digraph graphname{ node[shape=box] n[shape=plaintext,label="node"] m[shape=plaintext,label="head",fontcolor=red] rankdir=LR { rankdir = LR A[label=A] B[label=B] NULL1[label="...",shape=plaintext] } { rank="same"; m->B } { rank="same"; n->C } B->A->C->NULL1 } ``` :::warning 原有選項 `node = &(*node)->next->next` 不能執行嗎? :notes: jserv ::: > 可以! 當初可能弄錯了以為沒有正確答案 XD > > 事實上根據 C 語言規格書 > `&` (address of) 是屬於 6.5.3 Unary operators > `->` 則是屬於 6.5.2 Postfix operators > > 又在規格書第 76 頁中有提到: > The syntax specifies the precedence of operators in the evaluation of an expression, which is the same as the order of the major subclauses of this subclause, highest precedence first. > > The exceptions are cast expressions (6.5.4) as operands of unary operators > (6.5.3), and an operand contained between any of the following pairs of operators: grouping parentheses () (6.5.1), subscripting brackets [] (6.5.2.1), function-call parentheses () (6.5.2.2), and the conditional operator ? : (6.5.15). > > 因此在章節比較前面的 `->` 優先權會大於 `&`,所以 `&((*node)->next->next)` 和 `node = &(*node)->next->next` 是等價的,所以答案是選項 ( e ) ! > > 也可以透過下面的程式來驗證這點。 ```c= void test(node_t *head) { node_t **node = &head; node = &(*node)->next->next; printf("test: %ld, value: %d\n", (unsigned long)node, (*node)->value); } void test_2(node_t *head) { node_t **node = &head; node = &((*node)->next->next); printf("test_2: %ld, value: %d\n", (unsigned long)node, (*node)->value); } int main () { node_t *head = NULL; add_entry(&head, 1); add_entry(&head, 2); add_entry(&head, 3); test(head); test_2(head); } ``` > 得到結果: ``` test: 94834115597000, value: 3 test_2: 94834115597000, value: 3 ``` ### `reverse` ```c= node_t *reverse(node_t *head) { node_t *cursor = NULL; while (head) { node_t *next = head->next; head->next = cursor; cursor = head; head = next; } return cursor; } ``` 將 linked list 倒置。一開始 linked list 大概如下圖: ```graphviz digraph graphname{ node[shape=box] head[shape=plaintext,fontcolor=red] NULL[shape=plaintext] cursor[shape=plaintext,label="cursor"] rankdir=LR { rankdir = LR A[label=A] B[label=B] NULL1[label="...",shape=plaintext] } { rank="same"; head->A } cursor->NULL A->B->C->NULL1 } ``` * 首先 ` node_t *next = head->next;` ```graphviz digraph graphname{ node[shape=box] head[shape=plaintext,fontcolor=red] cursor[shape=plaintext,label="cursor"] NULL[shape=plaintext] rankdir=LR { rankdir = LR A[label=A] B[label=B] NULL1[label="...",shape=plaintext] } { rank="same"; head->A } { rank="same"; next[shape=plaintext] next->B } cursor->NULL A->B->C->NULL1 } ``` * cursor 就是回頭的節點,因此 `CCC` 應該為 `head->next = cursor; cursor = head` ```graphviz digraph graphname{ node[shape=box] cursor[shape=plaintext,label="cursor(head)",fontcolor=red] NULL[shape=plaintext] rankdir=LR { rankdir = LR A[label=A] B[label=B] NULL1[label="...",shape=plaintext] } { rank="same"; cursor->A } { rank="same"; next[shape=plaintext] next->B } A->NULL A->B->C->NULL1 } ``` * 最後是 `head = next;`。後面就重覆前面的步驟,把 linked list 所指的方向逆轉。最後 cursor 會指向原本 linked list 的最尾端,也就是新的 `head`,return 之。 ```graphviz digraph graphname{ node[shape=box] cursor[shape=plaintext,label="cursor"] NULL[shape=plaintext] rankdir=LR { rankdir = LR A[label=A] B[label=B] NULL1[label="...",shape=plaintext] } { rank="same"; cursor->A } { rank="same"; next[shape=plaintext,label="head(next)",fontcolor=red] next->B } A->NULL A->B->C->NULL1 } ``` ## 改寫為 pointer to pointer ### Bonus: `delete_list` 為了方便做實驗時更新 linked list 且避免 memory leak,實作一個釋放 linked list 上所有節點的 function。 ```cpp void delete_list(node_t **head) { while (*head) { node_t *next = (*head)->next; free(*head); *head = next; } } ``` ### `swap_pair` 由於原本的 `node` 已經是在對 pointer to pointer 做操作,所以可以簡單的改寫 `swap_pair` 成: ```cpp void swap_pair(node_t **head) { for (; *head && (*head)->next; head = &((*head)->next->next)) { node_t *tmp = *head; *head = (*head)->next; tmp->next = (*head)->next; (*head)->next = tmp; } } ``` 考慮到我們的 linked list 結構比較簡單,node 裡只有下一個的位置和儲存的數值,這裡有另外一個也可以做到結果相同的 swap 方法。我們可以只是把節點中的 `value` 做交換。 ```cpp #define XORSWAP_UNSAFE(a, b) \ ((a) ^= (b), (b) ^= (a), \ (a) ^= (b)) /* Doesn't work when a and b are the same object - assigns zero \ (0) to the object in that case */ #define XORSWAP(a, b) \ ((&(a) == &(b)) ? (a) /* Check for distinct addresses */ \ : XORSWAP_UNSAFE(a, b)) void swap_pair_by_value(node_t **head) { for (; *head && (*head)->next; head = &((*head)->next->next)) { XORSWAP((*head)->value, (*head)->next->value); } } ``` #### Bonus: 兩個方法的執行時間差異 我想要知道哪個方法可能運行的比較快,所以設計了一段測試程式。考慮到 cache 可能的影響,所以每次 swap 的都是一個新的 linked list,並且透過 `taskset` 指令僅在單核上運作。 ```c= for (int num = 10; num < 2000; num++){ for (int i=0; i<num; i++){ add_entry(&head, (rand() % 1000)); } struct timespec tt1, tt2; clock_gettime(CLOCK_MONOTONIC, &tt1); swap_pair(&head); clock_gettime(CLOCK_MONOTONIC, &tt2); long long time = (long long) (tt2.tv_sec * 1e9 + tt2.tv_nsec) - (long long) (tt1.tv_sec * 1e9 + tt1.tv_nsec); delete_list(&head); for (int i=0; i<num; i++){ add_entry(&head, (rand() % 1000)); } clock_gettime(CLOCK_MONOTONIC, &tt1); swap_pair_by_value(&head); clock_gettime(CLOCK_MONOTONIC, &tt2); long long time2 = (long long) (tt2.tv_sec * 1e9 + tt2.tv_nsec) - (long long) (tt1.tv_sec * 1e9 + tt1.tv_nsec); printf("%d, %lld, %lld\n", num, time, time2); delete_list(&head); } ``` 考慮到可能的誤差,因此我通過簡單的統計方法 [standard deviation around the mean](https://en.wikipedia.org/wiki/Standard_deviation) 來分析運行時間(詳細參閱 [GitHub](https://github.com/RinHizakura/linked-list/blob/master/scripts/perf.py))。得到如下的圖: ![](https://i.imgur.com/Jf6Zb1X.png) 原本我預期交換數值的方法會比較快,畢竟不用調整 next 指去哪,又可以通過 bitwise 運算提升交換的效率。然而在圖中驚訝的發現,兩個方法的快慢其實沒有顯著的差異。 初步猜測是 XOR 裡的 branch 判斷導致。因為可以確認自己對 XOR 的使用是對於兩個相異的 object(粗淺的說,不同的變數),因此我們把程式調整為沒有分支的 `XORSWAP_UNSAFE`,然後得到的結果是: ![](https://i.imgur.com/jvOEBB8.png) 感覺好像真的是 branch 產生影響?於是我試圖用 perf 去觀察使用 `XORSWAP_UNSAFE` 和 `XOR` 的在 node 數量 2000 時的 branch misses, * `XORSWAP` ``` sudo perf stat -e branch-misses:u,branches:u ./exec Performance counter stats for './exec': 5,668 branch-misses:u # 0.26% of all branches 2,214,808 branches:u 0.005515600 seconds time elapsed 0.005551000 seconds user 0.000000000 seconds sys ``` * `XORSWAP_UNSAFE` ``` sudo perf stat -e branch-misses:u,branches:u ./exec Performance counter stats for './exec': 5,656 branch-misses:u # 0.26% of all branches 2,213,809 branches:u 0.007371779 seconds time elapsed 0.007382000 seconds user 0.000000000 seconds sys ``` 從結果來看,`XORSWAP_UNSAFE` 的 branch miss 雖然看似少一些,但兩者的差異並不大。雖然也有可能是因為圖中的時間是用 ns 等級,所以看起來會差距比較明顯,但是如此接近的 branch miss 不該是導致如此差距的原因。為了可以得知更多的細節,我嘗試使用 [Cachegrind](https://www.valgrind.org/docs/manual/cg-manual.html) 檢查記憶體的實際情況: ``` $ valgrind --tool=cachegrind ./exec $ cg_annotate <file> $ cg_diff <file1> <file2> ``` * `XORSWAP` ``` -------------------------------------------------------------------------------- Ir I1mr ILmr Dr D1mr DLmr Dw D1mw DLmw -------------------------------------------------------------------------------- 17,116,037 1,050 1,029 8,303,820 755,543 2,081 2,131,776 4,773 1,596 PROGRAM TOTALS -------------------------------------------------------------------------------- Ir I1mr ILmr Dr D1mr DLmr Dw D1mw DLmw file:function -------------------------------------------------------------------------------- 55,014 4 4 41,005 1,002 0 4,003 0 0 /home/rin/Github/linked-list/linked_list.c:swap_pair_by_value ``` * `XORSWAP_UNSAFE` ``` -------------------------------------------------------------------------------- Ir I1mr ILmr Dr D1mr DLmr Dw D1mw DLmw -------------------------------------------------------------------------------- 17,109,037 1,047 1,027 8,298,820 755,543 2,081 2,131,776 4,773 1,596 PROGRAM TOTALS -------------------------------------------------------------------------------- Ir I1mr ILmr Dr D1mr DLmr Dw D1mw DLmw file:function -------------------------------------------------------------------------------- 48,014 3 3 36,005 1,002 0 4,003 0 0 /home/rin/Github/linked-list/linked_list.c:swap_pair_by_value ``` 觀察一下兩者的差異: * Ir(執行的總指令數)總體的差距為 `17,116,037 - 17,109,037 = 7000` 與 `swap_pair_by_value` 的 `55,014-48014 = 7000` 相等 * Dr(memory 的讀取次數)總體的差距為 `8,303,820 - 8,298,820 = 5000` 與 `swap_pair_by_value` 的 `41,005 - 36,005 = 5000` 相等 仔細一想,就發現其實自己把問題複雜化了,因為 `XORSWAP_UNSAFE` 的指令會比 `XORSWAP` 少,不需要讀取記憶體位址檢查是否相同,所以可以執行的快一些原本就十分合理。而當節點變多,指令的差異量就會表現在時間上。 :::warning 設計實驗時,應考慮 linked list 節點結構中的資料可能很多項,成員可能還指向其他記憶體空間。 :notes: jserv ::: ### `reverse` 概念和原本的 `reverse` 並無太大差異,只是為了確保 `head` 最後會是指到原本 linked list 的尾端,把 while 迴圈中的判斷條件條件調整成判斷下一個是否為 NULL。 ```c= void reverse(node_t **head) { node_t *prev = NULL; node_t *next = (*head)->next; (*head)->next = prev; while(next){ prev = (*head); *head = next; next = next->next; (*head)->next = prev; } } ``` ## 遞迴版本的 `reverse` 整體的概念和前面的 reverse 相同: 首先改變現在的 node 的指向,把它往前指。如果下一個要被調整的 node 為 NULL,表示已經找到原本 linked list 的尾端,因此要把 head 更新到該位置上並且 return。否則就繼續往下 recursive。 ```c= void recursive_rev(node_t **head) { if (!head) return; recursive_rev_step(*head, NULL, head); } void recursive_rev_step(node_t *curr, node_t *prev, node_t **head) { node_t *next = curr->next; curr->next = prev; if (!next) { *head = curr; return; } recursive_rev_step(next, curr, head); } ``` ## 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 個數字」這個過程涉及對原始 array 的額外調整,所以時間複雜度會是 $O(n^2)$,這裡的時間複雜度則是 $O(n)$。此外,不需另外產生一個儲存的序列(inplace),而是透過 swap 的方式達到同等的效果。 :::warning 這裡的時間複雜度當是以 array 儲存的前題下,考慮到資料結構的差異可能也會有複雜度的差異,必須思考自己使用的資料結構給出最恰當的演算法。 ::: * 假設初始的序列如下: | 隨機數範圍 | 選擇隨機數 | 原始序列 | | ---------- | ---------- |:--------- | | | | 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 shffling 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 上沒有 node (`len == 0`) :::warning 延伸問題: * 檢驗洗牌後的結果是否「足夠亂」,確認演算法的實作正確性 * 思考現在的實作是否可以改善?或者是否有對於 singlely-linked list 更有效率的 shuffle 方式?(考慮到 $O(n)$ 的 access) :::