# 2020q3 Homework1 (quiz1) contributed by < `Holychung` > [2020q3 第 1 週測驗題](https://hackmd.io/@sysprog/sysprog2020-quiz1) ## Outline [TOC] ## 題目 考慮一個單向 linked list,其結構定義為: ```c typedef struct __node { int value; struct __node *next; } node_t; ``` 已知不存在 circular (環狀結構),接下來將對該單向 linked list 進行下列操作: - add_entry - remove_entry - swap_pair - reverse ## 程式運作原理 ### add_entry 新增節點,當 linked list 沒有內容時,必須由開發者更新指向開頭的指標。因此實際得到 reference,而非 copy ```c= 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; } ``` head 是指標的指標,指向 linked list 起點的指標位置。 首先建立了一個指標的指標 indirect,指向 head 所指向的位置。 ```graphviz digraph G { rankdir=LR node [shape="block"] node0 [label="node 0"] node1 [label="node 1"] null0 [shape=none, label="NULL"] head [shape=none, label="head"] ptr [shape=none, label="ptr"] head->ptr ptr->node0 node0->node1 node1->null0 {rank=same;node0 ptr head} } ``` 接著 malloc 新增一個節點 new_node 的記憶體空間,這個節點 next 指向 NULL。 ```graphviz digraph G { rankdir=LR node [shape="block"] node0 [label="node 0"] node1 [label="node 1"] null0 [shape=none, label="NULL"] null1 [shape=none, label="NULL"] head [shape=none, label="head"] ptr [shape=none, label="ptr"] new_node->null1 head->ptr ptr->node0 node0->node1 node1->null0 {rank=same;node0 ptr head} } ``` 經過 while 迴圈走訪 linked list,indirect 會指向最尾端 NULL,此時將最尾端指向 new_node,完成新增節點。 ```graphviz digraph G { rankdir=LR node [shape="block"] node0 [label="node 0"] node1 [label="node 1"] new_node [label="new_node"] null0 [shape=none, label="NULL"] node0->node1 node1->new_node new_node->null0 {rank=same;null0 } } ``` ### remove_entry 移除指定節點,指向開頭的指標可能因此變更,所以需要用到 a pointer to a pointer (指標的指標) ```c= void remove_entry(node_t **head, node_t *entry) { node_t **indirect = head; while ((*indirect) != entry) indirect = &(*indirect)->next; *indirect = entry->next; free(entry); } ``` ### swap_pair 交換一對相鄰的節點,取自 [LeetCode: Swap Nodes in Pairs](https://leetcode.com/problems/swap-nodes-in-pairs/),給定 `1->2->3->4`,則該回傳 `2->1->4->3` 透過指標的指標 node 指向 linked list 開頭的位址,每一次交換相鄰的兩個節點,完成之後 node 指向下下一個節點的位置,直到 最尾端或是只剩一個節點的時候停止,並且把參數 head 指標傳回去更新新的起點位置。 ```c= 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; } ``` 一開始用指標的指標 node 指向起始的位址。 ```graphviz digraph G { rankdir=LR node [shape="block"] null0 [shape=none, label="NULL"] node0 [label="node", shape=none] head [shape=none, label="head"] node0->head->node1 node1->node2->null0 {rank=same;node0 head node1} } ``` 然後用指標 tmp 指向 \*node 的位址。 ```graphviz digraph G { rankdir=TB node [shape="block"] null0 [shape=none, label="NULL"] node0 [label="node", shape=none] head [shape=none, label="head"] tmp [shape=none] tmp->node1 node0->head->node1 node1->node2->null0 {rank=same;node2 null0 node1} } ``` \*node 移動指向下一個節點。 ```graphviz digraph G { rankdir=TB node [shape="block"] null0 [shape=none, label="NULL"] node0 [label="node", shape=none] head [shape=none, label="head"] tmp [shape=none] tmp->node1 node0->head->node2 node1->node2->null0 {rank=same;node2 null0 node1} } ``` 把第一個節點的 next 指向第二個節點 next,也就是把 tmp 指向的節點的 next 改成 node 節點指向的 next。 ```graphviz digraph G { rankdir=TB node [shape="block"] null0 [shape=none, label="NULL"] node0 [label="node", shape=none] head [shape=none, label="head"] tmp [shape=none] tmp->node1 node0->head->node2 node1:s->null0 node2->null0 {rank=same;node2 null0 node1} } ``` 再把第二個節點的 next 指向第一個節點,也就是把 node 指向的節點的 next 改成 tmp 指向的位址,完成一對節點的交換。 ```graphviz digraph G { rankdir=TB node [shape="block"] null0 [shape=none, label="NULL"] node0 [label="node", shape=none] head [shape=none, label="head"] tmp [shape=none] tmp->node1 node0->head->node2 node1->null0 node2->node1 {rank=same;node2 null0 node1} } ``` 原本 node 是指向 head 這個指標,在做完一次 swap 後,head 會更新指到新的起點位置,因此要再把這個指標回傳回去更新開頭位置。 然後 node 就繼續指向下下一個節點,直接把 node 指向下一個節點的指標 next,一直做到最後結束。 ```graphviz digraph G { rankdir=TB node [shape="block"] null0 [shape=none, label="NULL"] node0 [label="node", shape=none] head [shape=none, label="head"] tmp [shape=none] tmp->node1 node0->head->null0 node1->null0 node2->node1 {rank=same;node2 null0 node1} } ``` ### reverse 將給定的 linked list 其內節點予以反向,即 `1->2->3->4`,則該回傳 `4->3->2->1` 用指標 cursor 紀錄下上一個節點的位置,初始值為 null,用一開始傳進來的起點位置 head 開始對 linked list 每一個節點做反轉,每一次 loop 都把當前的節點 next 指向前一個節點位址,直到最後,再回傳最後一個節點位址,也就是反轉後的開頭位址。 ```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; } ``` 先用指標 next 紀錄下一個節點的位置,也就是當前 head->next 指向的位置。 ```graphviz digraph G { rankdir=TB node [shape="block"] cursor [shape=none, label="cursor"] null0 [shape=none, label="NULL"] null1 [shape=none, label="NULL"] head [shape=none, label="head"] next [shape=none, label="next"] cursor->node1 head->node2 next->node3 node2->node3->null0 node1->null1 {rank=same;node2 null0 node1 node3 null1} } ``` 然後 head 指向的節點的 next 就可以指向上一個節點,達到反轉。 ```graphviz digraph G { rankdir=TB node [shape="block"] node1 node2 cursor [shape=none, label="cursor"] null0 [shape=none, label="NULL"] null1 [shape=none, label="NULL"] head [shape=none, label="head"] next [shape=none, label="next"] cursor->node1 head->node2 next->node3 node3->null0 node1->null1 node2:e->node1:w [label=" "] null1->node2 [style="invis"] {rank=same;node2 null0 node1 node3 null1} } ``` 上一個節點 cursor 就可以更新程現在這個節點 head,再把 head 指向剛剛 next 保存下來的下一個節點位置,完成一個節點的反轉。 ```graphviz digraph G { rankdir=TB node [shape="block"] node1 node2 cursor [shape=none, label="cursor"] null0 [shape=none, label="NULL"] null1 [shape=none, label="NULL"] head [shape=none, label="head"] next [shape=none, label="next"] cursor->node2 head->node3 next->null0 node3->null0 node1->null1 node2->node1 {rank=same;node2 null0 node1 node3 null1} } ``` ## 延伸問題 用指標的指標來改寫 `swap_pair` `reverse`,避免回傳指標。 並且以遞迴改寫上述的 `reverse`。 ### swap_pair 原本的寫法是傳入指標,所以在函式結束的時候並不會更新到原本的指標 head,所以需要回傳開頭的位置。修改成指標的指標進行操作,就會更新到原本的 head,就不用回傳指標。 ```c= void 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; } } ``` ### reverse 修改成指標的指標進行操作,就不用回傳指標,只是要注意最後要把 head 指向前一個的 cursor,才是開頭的位置。 ```c= void reverse(node_t **head) { node_t *cursor = NULL; while (*head) { node_t *next = (*head)->next; (*head)->next = cursor; cursor = *head; *head = next; } *head = cursor; } ``` ### 遞迴版 reverse 把 reverse 整個 linked list 的工作拆成一次只 reverse 當前的節點,然後把剩下的 linked list 丟下去遞迴。 所以遞迴函式`rev_recurive` 需要吃兩個參數,第一個 head 是指向剩下的 linked list 起點位置,第二個 cursor 則是指向上一個節點的位置。然後每次遞迴都更新並傳遞這兩個參數,直到最後指向尾端 NULL,再把 head 指向 cursor 才是開頭的位置。 ```c= void rev_recurive(node_t **head, node_t *cursor) { if (*head == NULL) { *head = cursor; return; } node_t *next = (*head)->next; (*head)->next = cursor; cursor = *head; *head = next; rev_recurive(head, cursor); } void reverse(node_t **head) { rev_recurive(head, NULL); } ``` 並且有實現老師[上課講解](https://hackmd.io/@sysprog/c-recursion?type=view#Tail-recursion)的[尾端遞迴 (Tail Recursion)](https://en.wikipedia.org/wiki/Tail_call)。 :::info Tail recursion 是遞迴的一種特殊形式,副程式只有在最後一個動作才呼叫自己。以演算法的角度來說,recursion tree 全部是一脈單傳,所以間複雜度是線性個該副程式的時間。不過遞迴是需要系統使用 stack 來儲存某些資料,於是還是會有 stack overflow 的問題。但是從編譯器的角度來說,因為最後一個動作才呼叫遞迴,所以很多 stack 資料是沒用的,所以他是可以被優化的。基本上,優化以後就不會有時間與空間效率的問題。 ::: ### Fisher–Yates shuffle 針對 singly-linked list 的節點,實作 Fisher–Yates shuffle,你應該儘量降低記憶體的使用量。 參考 [Fisher–Yates shuffle](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle) 的方法和 [dalaoqi ](https://hackmd.io/C8fRRRctRdGKqr4ezOzDoA?view) 的做法,先計算出 singly-linked list 的長度,然後開始進入迴圈實作。每次挑選一個隨機的節點 `target`,然後把 target 前後的節點接起來,再把 `target` 接到新的 list 開頭的位置,直到迴圈結束。最後再把新的 list 的開頭位址 `new_head` 更新到原本開頭的 head。 ```c= void shuffle(node_t **head) { if (*head == NULL) return; srand(time(NULL)); int len = 0; node_t **indirect = head; while (*indirect) { len++; indirect = &(*indirect)->next; } node_t *new_head = NULL; for (int i = len; i > 0; i--) { int random = rand() % i; indirect = head; while (random--) indirect = &(*indirect)->next; node_t *target = *indirect; *indirect = (*indirect)->next; target->next = NULL; if (new_head) { target->next = new_head; new_head = target; } else { new_head = target; } } *head = new_head; } ``` 首先,indirect 先指向開頭 head,並且透過迴圈計算出 list 長度。 ```graphviz digraph G { rankdir=TB node [shape="block"] node1 node2 indirect [shape=none, label="indirect"] null0 [shape=none, label="NULL"] head [shape=none, label="head"] ptr [shape=none, label="ptr"] indirect->ptr head->ptr->node1 node3->null0 node1->node2->node3 {rank=same;node2 null0 node1 node3} } ``` 接下來進入 for 迴圈,每一次都隨機選取出一個節點,並且用 indirect 去找到該節點,並且用一個指標 target 指向該節點。 ```graphviz digraph G { rankdir=TB node [shape="block"] node1 node2 indirect [shape=none, label="*indirect"] target [shape=none, label="target"] null0 [shape=none, label="NULL"] head [shape=none, label="head"] ptr [shape=none, label="ptr"] two [shape=none, label="隨機 2"] target->node2 indirect->node2 head->ptr->node1 node3->null0 node1->node2->node3 {rank=same;node2 null0 node1 node3} } ``` 透過 indirect 把 target 前一個節點接到後一個節點上。 ```graphviz digraph G { rankdir=TB node [shape="block"] node1 node2 indirect [shape=none, label="*indirect"] target [shape=none, label="target"] null0 [shape=none, label="NULL"] head [shape=none, label="head"] ptr [shape=none, label="ptr"] target->node2 indirect->node2 head->ptr->node1 node3->null0 node1->node2 [style="invis"] node2->node3 [style="invis"] node1:s->node3:s {rank=same;node2 null0 node1 node3} } ``` 然後把 target 接到新的 list 的開頭。 ```graphviz digraph G { rankdir=TB node [shape="block"] node1 node2 target [shape=none, label="target"] null0 [shape=none, label="NULL"] null1 [shape=none, label="NULL"] head [shape=none, label="head"] ptr [shape=none, label="ptr"] new_head [shape=none, label="new_head"] new_head->node2 target->node2 head->ptr->node1 node3->null0 node1->node3 node2->null1 {rank=same;node2 null0 node1 node3 null1} } ``` 直到迴圈結束,把指標的指標 head 指向新的開頭 new_head 就結束了。 ```graphviz digraph G { rankdir=TB node [shape="block"] node1 node2 null0 [shape=none, label="NULL"] head [shape=none, label="head"] new_head [shape=none, label="new_head"] head->new_head new_head->node3 node2->null0 node3->node1->node2 {rank=same;node2 null0 node1 node3} } ``` ## 延伸閱讀 - [graphviz tutorial](https://www.tonyballantyne.com/graphs.html?fbclid=IwAR3h9wdAUSMxyU8YK_Gl2CEgcHUyCTpfknO-4XpTcLuI3jMY7_ulyVC2MQo) - [graphviz documentation](http://www.graphviz.org/documentation/) - [你所不知道的C語言:遞迴呼叫篇](https://hackmd.io/@sysprog/c-recursion?type=view#Tail-recursion) - [尾端遞迴 (Tail Recursion)](https://en.wikipedia.org/wiki/Tail_call) - [Fisher–Yates shuffle](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle)