# 2020q3 Homework1 (quiz1) contributed by < `jonec76` > ###### tags: `sysprog-2020` > [作業說明](https://hackmd.io/@sysprog/rJ7WDWNVv) ## 開發環境 ```shell $ uname -a Linux jonec76-Aspire-M7720 5.4.0-47-generic #51-Ubuntu SMP GNU/Linux ``` ## 參考解答 AA1 = assert(new_node) AA2 = *indirect = new_node BB1 = node = &(*node)->next->next BB2 = *node = (*node)->next CCC = head->next = cursor; cursor = head ## 作業要求 - 重新回答 [第 1 周測驗題](https://hackmd.io/@sysprog/sysprog2020-quiz1),連同附帶的「延伸問題」。 解釋程式運作原理時,應提供對應的 [Graphviz](https://graphviz.org/) 圖例,可參照 [Linked List 題目 1 + 分析](https://hackmd.io/@sysprog/linked-list-quiz) - 比照 [課前測驗參考解答: Q1](https://hackmd.io/s/ByzoiggIb) : Q1 和 [Linked list 題目分析](https://hackmd.io/s/HyELy5bTz) 的模式來撰寫共筆,需要詳細分析自己的思路、參閱的材料 (以第一手材料為主,包含 C 語言規格書的章節),以及進行相關實驗。 ### Q1 填入 `assert()`,參考 [man page](https://man7.org/linux/man-pages/man3/assert.3.html) 提到,當傳入的值為 false 時,程式會暫停並且印出錯誤訊息。題目中的 assert(new_node) 指出,在配置記憶體給 `new_node` 時,若 `malloc` 失敗的話會回傳 `NULL`,此時表示沒有配置成功,這時透過 `assert` 檢測就能將程式暫停並且印出錯誤訊息。 ### Q2 填入 `*indirect = new_node`,這裡使用到了 pointer to pointer 的方式,使得不用回傳 `node_t*` 也能夠更改 caller 的 list。 - pointer to pointer 參數 callee 函式是如何更改 caller 的 linked-list 呢? 在 callee 的參數是 `node_t **head`,且在 caller 是傳入 `&head`,也就是 head 指標的位址,當 callee 的 head 在做 `*head`,也就是對這個 pointer to pointer 做 dereference 時,所得到的就是 caller 的記憶體位置,可從下方程式碼清楚看出 ```c=1 void add_entry(node_t **head, int new_value){ printf("callee: %p\n", &*head); ... } int main(){ node_t *head = NULL; printf("caller: %p\n", &head); ... } ``` 這樣的好處是,在 callee 透過 dereference 之後所操作的指標位址,跟外部的 caller 的指標位址是相同的,是同一個指標。 然而,若不使用 pointer to pointer 的方式,而改成傳入 `node_t* head`,此時 caller 跟 callee 的指標是不同的,參考 [Passing Reference to a Pointer in C++](https://www.geeksforgeeks.org/passing-reference-to-a-pointer-in-c/) 裡頭說到 > “pass by pointer” is passing a pointer by value. 也就是將值複製給 caller 指標,但是 caller 的變動無法改變 callee 的指標。 在 `add_entry` 裡頭,也透過 pointer to pointer `indirect` 指標,指到該 list 的最後一個位置(`NULL`),並且更新該位置為新配置的節點 `new_node`,圖示如下 ```graphviz digraph linked_list{ rankdir=LR node [shape=record]; 1 [label="{ <data> 1 | <ref> }"]; 3 [label="{ <data> 3 | <ref> }"]; 5 [label="{ <data> 5 | <ref> }"]; 7 [label="{ <data> 7 | <ref> }"]; h1[label = "{<data> head}"]; new[label = "{<data> new node| <ref>}"]; ind[label = "{<data> indirect| <ref> }"]; h1->1 1:ref:c -> 3:data [arrowtail=dot, dir=both, tailclip=false]; 3:ref:c -> 5:data [arrowtail=dot, dir=both, tailclip=false]; 5:ref:c -> 7:data [arrowtail=dot, dir=both, tailclip=false]; 7:ref:c ->new [arrowtail=dot, dir=both, tailclip=false] ind:ref:c -> new[arrowtail=dot, dir=both, tailclip=false]; new:ref:c -> NULL } ``` ### Q3 這部分要填入的是讓該指標能往 `next` 移動的選項,自己選了 (b)`*node = (*node)->next->next` 選項,但其實跟 (c) 是一樣的都是錯,錯在這樣寫會更改到 head 指標,使得最後 head 等於 `NULL` 並且回傳 `head`。正確寫法應該是使用 pointer to pointer 的指標去遍歷整條 list,如此才能不更改到 head 的位置,且對 node 做兩兩交換 ### Q4 這裡希望達到兩個 node 互相交換,步驟示意圖如下: 首先,先讓 `*node` 到 `*node->next`的位址 ```graphviz digraph linked_list{ rankdir=LR node [shape=record]; n1 [ fillcolor=gray style=filled label="{ <data> 4 | <ref> }"]; head [ label="{ <data> head}"]; n2 [fillcolor=gray style=filled label="{ <data> 12 | <ref> }"]; null [fillcolor=gray style=filled label="{ <data> NULL }"]; h1[ label = "{<data> *node}"]; pp[label = "{<data> node}"]; tmp[label = "{<data> tmp}"]; n1:ref:c -> n2:data [arrowtail=dot, dir=both, tailclip=false]; n2:ref:c -> null; h1:c -> n2:n tmp:ref:c -> n1:n pp:c -> h1:n [label="dereference"] head->n2:n } ``` 接著,更改這兩個 node 的 next pointer: ```graphviz digraph linked_list{ rankdir=LR node [shape=record]; head [ label="{ <data> head | <ref> }"]; n1 [ fillcolor=gray style=filled label="{ <data> 4 | <ref> }"]; n2 [fillcolor=gray style=filled label="{ <data> 12 | <ref> }"]; null [fillcolor=gray style=filled label="{ <data> NULL }"]; h1[ label = "{<data> *node}"]; pp[label = "{<data> node}"]; tmp[label = "{<data> tmp}"]; n1:ref:c -> null; h1:c -> n2:n n2:ref:c -> n1; tmp:ref:c -> n1:n pp:c -> h1:n [label="dereference"] head->n2:n } ``` 接著,`node` 往下兩步(參考第 (3) 題) ```graphviz digraph linked_list{ rankdir=LR node [shape=record]; n1 [ fillcolor=gray style=filled label="{ <data> 4 | <ref> }"]; n2 [fillcolor=gray style=filled label="{ <data> 12 | <ref> }"]; null [fillcolor=gray style=filled label="{ <data> NULL }"]; h1[ label = "{<data> *node}"]; pp[label = "{<data> node}"]; tmp[label = "{<data> tmp}"]; n1:ref:e -> null:w; n2:ref:e -> n1:w; h1:c -> null:n; tmp:e -> null:n head->n2:w pp:c -> h1:n [label="dereference"] } ``` ### Q5 這題考 reverse list,動畫示意圖參考 [reverse a linked list](https://www.geeksforgeeks.org/reverse-a-linked-list/) ![](https://i.imgur.com/sg9cbr8.gif =400x400) 圖中的 prev 指標對應到題目的 cursor,curr 指標對應到 head。因為 current->next 會指向前一個 node 的位址,於是需要 next 指標先去紀錄 current->next 的位址,接著才能改動 current->next 指到前一個 node,前一個 node 由 prev(cursor) 紀錄著。 ## 延伸問題 - [x] 解釋上述程式運作原理,搭配 Graphviz,比照 Linked List 練習題 在 HackMD 筆記上視覺化展現; - [x] 函式 swap_pair 和 reverse 對於指標的操作方式顯然異於 add_entry 及 remove_entry,需要額外做 head = ... 的更新,請用指標的指標來改寫,並避免回傳指標; - 要避免回傳指標,解法就是讓 pointer to pointer 指向傳進來的參數的位址,如此一來就能透過 dereference 直接對 caller 的那指標做操作,改後程式碼如下 ```c=1 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; } } void reverse(node_t **head){ node_t *cursor = NULL; node_t *next = NULL; while (*head) { next = (*head)->next; (*head)->next = cursor; cursor = *head; *head = next; } *head = cursor; } ``` - [x] 以遞迴改寫上述的 reverse,注意,你可能因此需要建立新的函式,如 rev_recursive,隨後在 reverse 函式中呼叫 rev_recursive; 想法是先遞迴到 list 的最後一個 node(在 `NULL` 的前一個)。而這個最後一個 node 將會是新的 head 位置,所以使用了 `rev_h` 去紀錄 > 這邊不確定是否有更好的寫法,譬如說不需要多一個參數 `rev_h`,而是只用一個參數即可 接著在遞迴的 backtracking 階段,所拿到的 node 都是前一個遞迴回傳的 node,該回傳 node 取做 `prev`,表示他是前一個 node 要來指向我現在的 node。 這邊值得注意的是 `curr->next = NULL;`,沒加這行之前執行 `print_list` 會是無限迴圈,這是因為假設下方是原始 list ```graphviz digraph linked_list{ rankdir=LR node [shape=record]; head [ label="{ <data> head }"]; n1 [ fillcolor=gray style=filled label="{ <data> 4 | <ref> }"]; n2 [fillcolor=gray style=filled label="{ <data> 12 | <ref> }"]; n3 [fillcolor=gray style=filled label="{ <data> 17 | <ref> }"]; n4 [fillcolor=gray style=filled label="{ <data> 21 | <ref> }"]; head->n1:n n1:ref:c->n2[arrowtail=dot, dir=both, tailclip=false]; n2:ref:c->n3[arrowtail=dot, dir=both, tailclip=false]; n3:ref:c->n4[arrowtail=dot, dir=both, tailclip=false]; n4->NULL } ``` 接著若沒有處理 `curr->next = NULL;`,就會得到下方的圖。接著若再跑 `print_list(newh)` 的話,就會產生無限迴圈 ```graphviz digraph linked_list{ rankdir=LR node [shape=record]; head [ label="{ <data> newh }"]; n1 [ fillcolor=gray style=filled label="{ <data> 4 | <ref> }"]; n2 [fillcolor=gray style=filled label="{ <data> 12 | <ref> }"]; n3 [fillcolor=gray style=filled label="{ <data> 17 | <ref> }"]; n4 [fillcolor=gray style=filled label="{ <data> 21 | <ref> }"]; head->n4:n n4:ref:c->n3[arrowtail=dot, dir=both, tailclip=false]; n3:ref:c->n2[arrowtail=dot, dir=both, tailclip=false]; n2:ref:c->n1[arrowtail=dot, dir=both, tailclip=false]; n1:ref:n->n2:n[arrowtail=dot, dir=both, tailclip=false]; } ``` 以下附上程式碼 ```c=1 node_t* rev_recursive(node_t *curr, node_t **rev_h){ if(!(curr)->next){ *rev_h = curr; return curr; } node_t* prev = rev_recursive(curr->next, rev_h); prev->next = curr; curr->next = NULL; return curr; } void reverse(node_t **head) { node_t* rev_h; rev_recursive(*head, &rev_h); *head = rev_h; } ``` - [x] 針對 singly-linked list 的節點,實作 Fisher–Yates shuffle,你應該儘量降低記憶體的使用量; ```c=1 void fy_shuffle(node_t** head){ node_t* tail = NULL, *rand_ptr = NULL; node_t** tmp; time_t t; int len, tmp_value, rand_idx; while(tail != *head){ tmp = head; len = 1; // find the tail of the list while((*tmp)->next != tail){ len++; tmp = &((*tmp)->next); } tail = *tmp; // find the random index pointer srand((unsigned) time(&t)); rand_idx = (rand() % len); rand_ptr = *head; for(int i=0;i<rand_idx;i++){ rand_ptr = rand_ptr->next; } // swap the value of two nodes tmp_value = rand_ptr->value; rand_ptr->value = tail->value; tail->value = tmp_value; } } ``` 用最直觀的方法將演算法實作出來,因為不能用多餘的 array 儲存每個值,所以時間複雜度是 $o(n^2)$。下面附上檢測時間複雜度的實驗結果,讓 `fy_shuffle` 執行從長度為 1 到 15000 的 list (以 50 為單位向上增加) ![](https://i.imgur.com/rAQAKjE.png =450x400)