# 2020q3 Homework1 (quiz1) contributed by < `zzzxxx00019` > ## 作業要求 * 重新回答[第 1 周測驗題](https://hackmd.io/@sysprog/sysprog2020-quiz1),連同附帶的「延伸問題」。 * 解釋程式運作原理時,應提供對應的 Graphviz 圖例。 ## 題目 考慮一個單向 linked list,其結構定義為: ```cpp typedef struct __node { int value; struct __node *next; } node_t; ``` 已知不存在 circular (環狀結構),接下來將對該單向 linked list 進行下列操作: * add_entry: 新增節點,當 linked list 沒有內容時,必須由開發者更新指向開頭的指標。因此實際得到 reference,而非 copy * remove_entry: 移除指定節點,指向開頭的指標可能因此變更,所以需要用到 a pointer to a pointer (指標的指標) * swap_pair: 交換一對相鄰的節點,取自 LeetCode: Swap Nodes in Pairs,給定 1->2->3->4,則該回傳 2->1->4->3 * reverse: 將給定的 linked list 其內節點予以反向,即 1->2->3->4,則該回傳 4->3->2->1 對應的程式碼如下: ```c= #include <stdio.h> #include <stdlib.h> typedef struct __node { int value; struct __node *next; } node_t; 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; AA1; while (*indirect) indirect = &(*indirect)->next; AA2; } node_t *find_entry(node_t *head, int value) { node_t *current = head; for (; current && current->value != value; current = current->next) /* interate */; return current; } void remove_entry(node_t **head, node_t *entry) { node_t **indirect = head; while ((*indirect) != entry) indirect = &(*indirect)->next; *indirect = entry->next; free(entry); } node_t *swap_pair(node_t *head) { for (node_t **node = &head; *node && (*node)->next; BB1) { node_t *tmp = *node; BB2; tmp->next = (*node)->next; (*node)->next = tmp; } return head; } node_t *reverse(node_t *head) { node_t *cursor = NULL; while (head) { node_t *next = head->next; CCC; head = next; } return cursor; } void print_list(node_t *head) { for (node_t *current = head; current; current = current->next) printf("%d ", current->value); printf("\n"); } int main(int argc, char const *argv[]) { node_t *head = NULL; print_list(head); add_entry(&head, 72); add_entry(&head, 101); add_entry(&head, 108); add_entry(&head, 109); add_entry(&head, 110); add_entry(&head, 111); print_list(head); node_t *entry = find_entry(head, 101); remove_entry(&head, entry); entry = find_entry(head, 111); remove_entry(&head, entry); print_list(head); /* swap pair. * See https://leetcode.com/problems/swap-nodes-in-pairs/ */ head = swap_pair(head); print_list(head); head = reverse(head); print_list(head); return 0; } ``` 參考執行輸出: (第 1 行是換行符號) ```c= 72 101 108 109 110 111 72 108 109 110 108 72 110 109 109 110 72 108 ``` 請補完程式碼。 參考解答: * AA1 = assert(new_node) * AA2 = *indirect = new_node * BB1 = node = &(*node)->next->next * BB2 = *node = (*node)->next * CCC = head->next = cursor; cursor = 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; } ``` * 功能:建立一個存放new_value的node,如果head不存在,將新建立的node設為list的head,否則將node插入head的尾端。 * 流程:透過`**indirect`,尋找head的尾端,並將最尾端以new_node取代。 * 詳細執行狀況與說明: * ==測試用程式碼== ```cpp int main(int argc, char const *argv[]) { node_t *head = NULL; print_list(head); add_entry(&head, 72); add_entry(&head, 101); add_entry(&head, 108); add_entry(&head, 109); add_entry(&head, 110); add_entry(&head, 111); print_list(head); return 0; } ``` ```cpp 72 101 108 109 110 111 ``` 程式執行初期,我們建立了一個指標`*head`,並將head指向NULL,即尚未分配記憶體空間。 隨後使用print_list印出linked list內容,因head目前仍為NULL,因此直接執行換行的動作。 接著我們透過add_entry,依序將value以node方式插入head的末端。 下表為建立新的node後,分配給其的記憶體位置: | value | address | | - | - | |72 |0x562d080ad670 | |101 |0x562d080ad690 | |108 |0x562d080ad6b0 | |109 |0x562d080ad6d0 | |110 |0x562d080ad6f0 | |111 |0x562d080ad710 | 而最後鏈結型態則為: | value| address | next | | --- | ------- | ---- | |72 |0x562d080ad670|0x562d080ad690| |101 |0x562d080ad690|0x562d080ad6b0| |108 |0x562d080ad6b0|0x562d080ad6d0| |109 |0x562d080ad6d0|0x562d080ad6f0| |110 |0x562d080ad6f0|0x562d080ad710| |111 |0x562d080ad710|(nil) | 起初執行add_entry後,因main head尚未指向任何linked list結構,因此new_node被設為linked list的head。 ```graphviz digraph add_entry { rankdir=LR ; node [shape = record] head [label="{<name> (add_entry)head }"]; main_head [label="{<name> (main)head}"]; indirect [label="{<name> indirect }"]; head -> main_head ; indirect -> main_head ; } ``` 而後執行add_entry後,indirect將對所有節點依循拜訪,尋找next為NULL的node,並將new_node設為最後一個node->next。 ```graphviz digraph add_new_node { rankdir=LR ; node [shape = record] node_1 [label = "{ <value>72 | <address>0x562d080ad670 | <ref> }"] ; node_2 [label = "{ <value>101 | <address>0x562d080ad690 | <ref> }"] ; node_3 [label = "{ <value>108 | <address>0x562d080ad6b0 | <ref> }"] ; node_4 [label = "{ <value>Empty | <address>NULL }"] ; indirect [label = "{<name>indirect}"] node_1:ref -> node_2 ; node_2:ref -> node_3 ; node_3:ref -> node_4 ; indirect -> node_4 ; } ``` * 功能:尋找存放value的node,並將該node回傳。 * 流程:特過current尋訪各個節點,如果current->value為目標value,則停止迴圈,否則繼續尋訪直到current為NULL,最終回傳NULL。 ### find_entry ```cpp node_t *find_entry(node_t *head, int value) { node_t *current = head; for (; current && current->value != value; current = current->next); return current; } ``` ### remove_entry * 功能:移除linked list中的指定節點。 * 流程:透過find_entry尋找欲刪除的entry,並透過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); } ``` * 詳細執行狀況與說明 * ==測試用程式碼== ```cpp int main(int argc, char const *argv[]) { node_t *head = NULL; add_entry(&head, 72); add_entry(&head, 101); add_entry(&head, 108); add_entry(&head, 109); add_entry(&head, 110); add_entry(&head, 111); print_list(head); node_t *entry = find_entry(head, 101); remove_entry(&head, entry); entry = find_entry(head, 111); remove_entry(&head, entry); print_list(head); return 0; } ``` ```cpp 72 101 108 109 110 111 72 108 109 110 ``` 先透過`find_entry`回傳欲搜尋存放該value的節點指標。 接著在`remove_entry`的部分,透過對照`*indirect`所指向的address是否為entry的值作為對比依據,並透過while迴圈依序尋訪。 找到後讓`*indirect`所存放的內容改為`entry->next`後,刪除目標節點。 ### swap_pair 目的:交換一對相鄰的節點,給定`1->2->3->4`,則回傳`2->1->4->3`。 流程:如果`node`與`node->next`都存在,將兩節點交換,否則結束迴圈。 ```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; } ``` * 詳細執行狀況說明 * ==測試用程式碼== ```cpp int main(int argc, char const *argv[]) { node_t *head = NULL; add_entry(&head, 72); add_entry(&head, 101); add_entry(&head, 108); add_entry(&head, 109); add_entry(&head, 110); add_entry(&head, 111); print_list(head); head = swap_pair(head); print_list(head); return 0; } ``` ```cpp 72 101 108 109 110 111 101 72 109 108 111 110 ``` `進入迴圈初期,符合規則:*node && (*node)->next ;` ```graphviz digraph { rankdir=LR ; node [shape = record] ; Null [shape=plaintext, label = "Null"] ; node_p [shape=plaintext, fontcolor=blue, label = "*node"] ; node_p ->node_1 -> node_2 -> node_3 -> node_4 -> Null ; } ``` `node_t *tmp= *node ; *node= (*node)->next ;` ```graphviz digraph { rankdir=LR ; node [shape = record] ; Null [shape=plaintext, label = "Null"] ; node_p [shape=plaintext, fontcolor=blue, label = "*node"] ; temp [shape=plaintext, fontcolor=red, label = "*temp"] ; node_1 -> node_2 -> node_3 -> node_4 -> Null ; temp -> node_1 ; node_p -> node_2 ; } ``` `tmp->next = (*node)->next ;` ```graphviz digraph { rankdir=LR ; node [shape = record] ; Null [shape=plaintext, label = "Null"] ; node_p [shape=plaintext, fontcolor=blue, label = "*node"] ; temp [shape=plaintext, fontcolor=red, label = "*temp"] ; node_2 -> node_3 -> node_4 -> Null ; temp -> node_1 -> node_3 ; node_p -> node_2 ; } ``` `(*node)->next = tmp` ```graphviz digraph { rankdir=LR ; node [shape = record] ; Null [shape=plaintext, label = "Null"] ; node_p [shape=plaintext, fontcolor=blue, label = "*node"] ; temp [shape=plaintext, fontcolor=red, label = "*temp"] ; node_2 -> node_1 -> node_3 -> node_4 -> Null ; temp -> node_1 ; node_p -> node_2 ; } ``` 再進入下一次迴圈,`*node`指標狀態 ```graphviz digraph { rankdir=LR ; node [shape = record] ; Null [shape=plaintext, label = "Null"] ; node_p [shape=plaintext, fontcolor=blue, label = "*node"] ; node_2 -> node_1 -> node_3 -> node_4 -> Null ; node_p -> node_3 ; } ``` ### reverse * 目的:將linked list反轉。 ```cpp 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; } ``` * 步驟解析: `初始狀態` ```graphviz digraph { rankdir=LR ; node [shape = record] ; head [shape=plaintext, fontcolor=red, label = "*head"] ; cursor [shape=plaintext, fontcolor=blue, label = "*cursor"] ; Nul [shape=plaintext, label = "..."] ; node_1 [label = "{<name> node_1}"] ; node_2 [label = "{<name> node_2}"] ; node_3 [label = "{<name> node_3}"] ; node_1 -> node_2 -> node_3 -> Nul ; head -> node_1 ; cursor -> NULL ; } ``` `node_t *next=head->next` ```graphviz digraph { rankdir=LR ; node [shape = record] head [shape=plaintext, fontcolor=red, label = "*head"] ; cursor [shape=plaintext, fontcolor=blue, label = "*cursor"] ; next [shape=plaintext, label = "*next"] ; Nul [shape=plaintext, label = "..."] ; node_1 [label = "{<name> node_1}"] ; node_2 [label = "{<name> node_2}"] ; node_3 [label = "{<name> node_3}"] ; node_1 -> node_2 -> node_3 -> Nul ; head -> node_1 ; cursor -> NULL ; next -> node_2 ; } ``` `head->next = cursor; cursor = head;` ```graphviz digraph { rankdir=LR ; node [shape = record] head [shape=plaintext, fontcolor=red, label = "*head"] ; cursor [shape=plaintext, fontcolor=blue, label = "*cursor"] ; next [shape=plaintext, label = "*next"] ; Nul [shape=plaintext, label = "..."] ; node_1 [label = "{<name> node_1}"] ; node_2 [label = "{<name> node_2}"] ; node_3 [label = "{<name> node_3}"] ; node_2 -> node_3 -> Nul ; next -> node_2 ; head -> node_1 ; cursor -> node_1 -> NULL ; } ``` `head = next` ```graphviz digraph { rankdir=LR ; node [shape = record] head [shape=plaintext, fontcolor=red, label = "*head"] ; cursor [shape=plaintext, fontcolor=blue, label = "*cursor"] ; next [shape=plaintext, label = "*next"] ; Nul [shape=plaintext, label = "..."] ; node_1 [label = "{<name> node_1}"] ; node_2 [label = "{<name> node_2}"] ; node_3 [label = "{<name> node_3}"] ; next -> node_2 -> node_3 -> Nul ; head -> node_2 ; cursor -> node_1 -> NULL ; } ``` `迴圈執行完最後結果,回傳cursor指標` ```graphviz digraph { rankdir=LR ; node [shape = record] head [shape=plaintext, fontcolor=red, label = "*head"] ; cursor [shape=plaintext, fontcolor=blue, label = "*cursor"] ; next [shape=plaintext, label = "*next"] ; Nul [shape=plaintext, label = "..."] ; node_1 [label = "{<name> node_1}"] ; node_2 [label = "{<name> node_2}"] ; node_3 [label = "{<name> node_3}"] ; cursor -> node_3 -> node_2 -> node_1 -> NULL ; next -> Nul ; head -> Nul ; } ``` ### print_list 目的:將所有節點的value印出。 流程:透過迴圈,如果node存在,印出node->value,透過*current尋訪所有節點。 ```cpp void print_list(node_t *head) { for (node_t *current = head; current; current = current->next) printf("%d ", current->value); printf("\n"); } ``` ## pointer to pointer改寫程式碼 ### pointer與pointer to pointer兩者差異 * 下面將列出function透過`node_t **head`與`node_t *head`的對照圖。 * 透過`node_t *head`圖示: ```graphviz digraph { rankdir=LR ; node [shape = record] ; main_head [shape=plaintext, fontcolor=blue, label = "*main_head"] ; entry_head [shape=plaintext, fontcolor=red, label = "*entry_head"] ; Null [shape=plaintext, label="Null"] ; entry_head -> main_head -> node_1 -> node_2 -> node_3 ->Null ; } ``` * 即對main_head進行取值運算,得到的是node_1的address,透過`node_t **head`圖示了解: ```graphviz digraph { rankdir=LR ; node [shape = record] ; main_head [shape=plaintext, fontcolor=blue, label = "*main_head"] ; entry_head [shape=plaintext, fontcolor=red, label = "**entry_head"] ; Null [shape=plaintext, label="Null"] ; main_head -> node_1 -> node_2 -> node_3 -> Null ; entry_head -> node_1 ; } ``` ### Swap * 透過上圖了解,原本程式碼`node_t **node = &head`即是對main_head進行取值得到node_1的實際address,而透過`node_t **head`,entry_head已對main_head進行取值,因此這邊只要直接將這行程式碼改為`node_t **node = head`即可。 ```cpp void swap_pair_p2p(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 * 透過上圖了解,因entry_head為對main_head取值結果,因此效果相當於main_head,透過main_head尋訪並且逐一翻轉,最後再將main_head接回翻轉後的linked list。 ```cpp void reverse_p2p(node_t **head) { node_t *cursor = NULL ; while(*head) { node_t *next = (*head)->next ; (*head)->next = cursor ; cursor = *head ; *head = next ; } *head = cursor ; } ``` ## 以Recursive改寫Reverse 操作流程:概念與原本的reverse相似,不同的是,將while以recursive的方式進行取代,原本的function執行完後會判斷`*head`是否為空指標,現在則是以判斷式的方式檢查是否為空指標,執行完後再呼叫自己本身(`reverse_rec`),直到結束。 ```cpp void reverse_rec(node_t **head, node_t *cursor) { if(*head) { node_t *next = (*head)->next ; (*head)->next = cursor ; cursor = *head ; *head = next ; reverse_rec(head,cursor) ; } else *head = cursor ; } ``` ## Fisher–Yates shuffle * 簡介:洗牌演算法需 $O(n)$ 的時間複雜度去產生設定長度的亂數,又需要$O(n)$的時間複雜度去執行每次的節點的查訪,因此總時間複雜度為 $O(n^2)$ * 流程: * 透過for迴圈計算linked list總長度 * 透過 `rand()` 取得亂數,範圍為1~length * 透過與 `head` 做 `value` 的交換,降低記憶體使用量 * 每當產生一筆隨機數,`head` 向後推移一格,`length` 減1 * 實作程式碼: ```cpp= void shuffle(node_t **head) { if (!*head) return; int length = 0; node_t **tmp_head = head; for (; *tmp_head ; tmp_head = &(*tmp_head)->next, length += 1); srand(time(NULL)); tmp_head = head ; while (length > 1) { int Roll = rand() % length + 1; node_t **indirect = tmp_head; for(; Roll!=1 ; Roll--, indirect=&(*indirect)->next) ; int tmp_value = (*tmp_head)->value; (*tmp_head)->value = (*indirect)->value; (*indirect)->value = tmp_value; tmp_head = &(*tmp_head)->next; length -= 1; } } ```