# 2020q3 Homework1 (quiz1)
contributed by < `dalaoqi` >
## Outline
[TOC]
考慮一個單向 linked list,其結構定義為:
```cpp
typedef struct __node {
int value;
struct __node *next;
} node_t;
```
## 程式運作原理
### 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;
}
```
新增一個節點到 linked list 的尾端:
```graphviz
digraph G {
graph[pencolor=transparent];
rankdir=LR
indirect[shape=none];
head[shape=none];
node_0[shape=box];
node_1[shape=box];
node_2[shape=box];
NULL_1[shape=none,label="NULL"];
NULL_2[shape=none,label="NULL"];
new_node[shape=box];
node_0->node_1->node_2->NULL_2;
new_node->NULL_1;
{rank=same;indirect->head->node_0;}
}
```
在這函式裡,首先建立了一個 `indirect` 指標指向 `head` 指標, `head` 則指向 linked list 的起點。
接著透過 malloc 新增一個節點的記憶體空間,這個節點會指向 NULL,經過 while 迴圈 , `indirect` 會指向 linked list 最尾端NULL,此時將新節點指派給 `indirect` 完成新增節點。
```graphviz
digraph G {
graph[pencolor=transparent];
rankdir=LR
indirect[shape=none];
head[shape=none];
node_0[shape=box];
node_1[shape=box];
node_2[shape=box];
NULL_1[shape=none,label="NULL"];
NULL_2[shape=none,label="NULL"];
new_node[shape=box];
node_0->node_1->node_2->NULL_2;
new_node->NULL_1;
{rank=same;head->node_0;}
{rank=same;indirect->NULL_2;}
}
```
```graphviz
digraph G {
graph[pencolor=transparent];
indirect[shape=none];
head[shape=none];
node_0[shape=box];
node_1[shape=box];
node_2[shape=box];
NULL_1[shape=none,label="NULL"];
new_node[shape=box];
{rank=same;
node_0->node_1->node_2->new_node->NULL_1;}
head->node_0;
indirect->new_node;
}
```
* 此函式中的 `assert(new_node);` 目的是為了要確保新的節點有成功 malloc (NULL 失敗),因此 `AA1` 為 `assert(new_node);`
* `AA2` 答案為 `*indirect = new_node` 是因為需要得知 linked list 最尾端的位置在何處。
### 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;
}
```
從 linked list 的頭開始遍歷,直到找到符合的節點後回傳該節點。
### 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),類似 `add_entry` 的做法,詳細如下:
```graphviz
digraph G {
graph[pencolor=transparent];
rankdir=LR
indirect[shape=none];
head[shape=none];
node_0[shape=box];
node_1[shape=box];
node_2[shape=box];
node_3[shape=box];
NULL_1[shape=none,label="NULL"];
delete[shape=none,label="want_to_delete"];
node_0->node_1->node_2->node_3->NULL_1;
{rank=same;indirect->head->node_0;}
{rank=same; delete->node_2}
}
```
經由 while 迴圈找到該 entry
```graphviz
digraph G {
graph[pencolor=transparent];
rankdir=LR
indirect[shape=none];
head[shape=none];
node_0[shape=box];
node_1[shape=box];
node_2[shape=box];
node_3[shape=box];
NULL_1[shape=none,label="NULL"];
delete[shape=none,label="want_to_delete_entry"];
node_0->node_1->node_2->node_3->NULL_1;
{rank=same;head->node_0;}
{rank=same; indirect->delete->node_2}
}
```
更換 indirect 指向的記憶體空間,再 free entry。
```graphviz
digraph G {
graph[pencolor=transparent];
rankdir=LR
indirect[shape=none];
head[shape=none];
node_0[shape=box];
node_1[shape=box];
node_2[shape=box];
node_3[shape=box];
NULL_1[shape=none,label="NULL"];
delete[shape=none,label="want_to_delete_entry"];
node_0->node_1->node_3->NULL_1;
{rank=same;head->node_0;}
{rank=same; delete->node_2}
{rank=same; indirect->node_3}
}
```
### 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;
}
```
`node_0` 和 `node_1` 交換, `node_2` 和 `node_3` 交換,以次類推。
這裡的做法也是使用指標的指標來做操作。
一開始進入迴圈時長這樣, node 指標指向 head 指標。
```graphviz
digraph G {
graph[pencolor=transparent];
rankdir=LR
head[shape=none];
n[shape=none,label="node"];
node_0[shape=box];
node_1[shape=box];
node_2[shape=box];
node_3[shape=box];
NULL_1[shape=none,label="NULL"];
node_0->node_1->node_2->node_3->NULL_1;
{rank=same;n->head->node_0;}
}
```
宣告 tmp 指向 *node(head 指標)。
```graphviz
digraph G {
graph[pencolor=transparent];
rankdir=LR
head[shape=none];
n[shape=none,label="node"];
node_0[shape=box];
tmp[shape=none]
node_1[shape=box];
node_2[shape=box];
node_3[shape=box];
NULL_1[shape=none,label="NULL"];
node_0->node_1->node_2->node_3->NULL_1;
{rank=same;n->head->node_0;}
tmp->node_0;
}
```
`*node = (*node)->next;`
node 新位址指向 node 的下個節點的位址。因此 `BB2` 答案為 `*node = (*node)->next;`
```graphviz
digraph G {
graph[pencolor=transparent];
rankdir=LR
head[shape=none];
n[shape=none,label="node"];
node_0[shape=box];
tmp[shape=none]
node_1[shape=box];
node_2[shape=box];
node_3[shape=box];
NULL_1[shape=none,label="NULL"];
node_0->node_1->node_2->node_3->NULL_1;
{rank=same;n->head->node_1}
{rank=same;tmp->node_0;}
}
```
`tmp->next = (*node)->next;`
tmp 新位址指向 node 下一個的位址。
```graphviz
digraph G {
graph[pencolor=transparent];
rankdir=LR
head[shape=none];
n[shape=none,label="node"];
node_0[shape=box];
tmp[shape=none]
node_1[shape=box];
node_2[shape=box];
node_3[shape=box];
NULL_1[shape=none,label="NULL"];
node_1->node_2->node_3->NULL_1;
{rank=same;n->head->node_1}
{rank=same;tmp->node_0->node_2;}
}
```
node 指向 tmp 完成交換。
```graphviz
digraph G {
graph[pencolor=transparent];
rankdir=LR
head[shape=none];
n[shape=none,label="node"];
node_0[shape=box];
tmp[shape=none]
node_1[shape=box];
node_2[shape=box];
node_3[shape=box];
NULL_1[shape=none,label="NULL"];
node_1->node_0->node_2->node_3->NULL_1;
{rank=same;n->head->node_1}
{rank=same;tmp->node_0}
}
```
因為是兩倆交換,因此 node 在每次 iteration 完都要 往前兩格,所以 `BB1` 為 `node = &(*node)->next->next`
```graphviz
digraph G {
graph[pencolor=transparent];
rankdir=LR
head[shape=none];
n[shape=none,label="node"];
node_0[shape=box];
tmp[shape=none]
node_1[shape=box];
node_2[shape=box];
node_3[shape=box];
NULL_1[shape=none,label="NULL"];
node_1->node_0->node_2->node_3->NULL_1;
{rank=same;head->node_1}
{rank=same;tmp->node_0}
{rank=same;n->node_2}
}
```
### reverse
```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;
}
```
做 linked list 反轉,一開始大概是長這樣。
```graphviz
digraph G {
graph[pencolor=transparent];
rankdir=LR
head[shape=none];
node_0[shape=box];
cursor[shape=none];
node_1[shape=box];
node_2[shape=box];
node_3[shape=box];
NULL_1[shape=none,label="NULL"];
NULL_2[shape=none,label="NULL"];
node_0->node_1->node_2->node_3->NULL_1;
{rank=same;head->node_0}
rank=same;cursor->NULL_2
}
```
`node_t *next = head->next;` , next 指標指向 head 的 next。
```graphviz
digraph G {
graph[pencolor=transparent];
rankdir=LR
head[shape=none];
next[shape=none];
node_0[shape=box];
cursor[shape=none];
node_1[shape=box];
node_2[shape=box];
node_3[shape=box];
NULL_1[shape=none,label="NULL"];
NULL_2[shape=none,label="NULL"];
node_0->node_1->node_2->node_3->NULL_1;
{rank=same;head->node_0}
{rank=same;next->node_1}
cursor->NULL_2
}
```
`CCC` 是 `head->next = cursor; cursor = head;` ,其中 cursor 是用來記住 head 前一個節點使得之後可以被指向,達成反轉的效果。
```graphviz
digraph G {
graph[pencolor=transparent];
rankdir=LR
head[shape=none];
next[shape=none];
node_0[shape=box];
cursor[shape=none];
node_1[shape=box];
node_2[shape=box];
node_3[shape=box];
NULL_1[shape=none,label="NULL"];
NULL_2[shape=none,label="NULL"];
node_0->node_1->node_2->node_3->NULL_1;
{rank=same;head->node_0}
{rank=same;next->node_1}
{rank=same;cursor->node_0;}
rank=same;node_0->NULL_2;
}
```
`head = next;` ,遍歷下去後直到 head = NULL,回傳 cursor 做為新的 head。
```graphviz
digraph G {
graph[pencolor=transparent];
rankdir=LR
next[shape=none,label="next(head)"];
node_0[shape=box];
cursor[shape=none];
node_1[shape=box];
node_2[shape=box];
node_3[shape=box];
NULL_1[shape=none,label="NULL"];
NULL_2[shape=none,label="NULL"];
node_0->node_1->node_2->node_3->NULL_1;
{rank=same;next->node_1}
{rank=same;cursor->node_0;}
rank=same;node_0->NULL_2;
}
```
## 延伸問題
避免函式回傳指標。
### swap_pair
```cpp
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;
}
}
```
其實原本的寫法就可以說是指標的指標的操作,只是還需要回傳 head ,因此稍微改一下,將傳入的參數改成是傳位址,用 node 去接這個傳入的參數,變相利用 node 當作 head 進行操作。
### reverse
```cpp
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;
}
```
一樣也是傳 head 的位址參數到函式中,概念就和原先的方式一樣,比較要注意的地方是在最後要把 cursor 指標 指派給 *head 。
### 遞迴版 reverse
```cpp
void reverse(node_t **head)
{
rev_recurive(*head, head);
}
void rev_recurive(node_t *head, node_t **headRef)
{
if (head == NULL) {
return;
}
node_t *first = head;
node_t *rest = head->next;
if (rest == NULL) {
*headRef = first;
return;
}
rev_recurive(rest, headRef);
rest->next = first;
first->next = NULL;
}
```
遞迴的概念是將 linked list 分成 第一個節點 ( `first` ) 和剩下其他的節點們 ( `rest` ) 遞迴拆解下去。遞迴函式傳入的會是 `head` 指標和 `head` 指標的位址,此方式是為了避免回傳指標。
從 `reverse` call `rev_recurive` 函式後,在進入 `rev_recurive` 函式之前的狀態會長這樣。
```graphviz
digraph G {
graph[pencolor=transparent];
rankdir=LR
rest[shape=none];
node_0[shape=box];
head[shape=none,label="first"];
node_1[shape=box];
node_2[shape=box];
NULL_1[shape=none, label="NULL"];
node_0->node_1->node_2->NULL_1;
{rank=same;head->node_0}
{rank=same;rest->node_1}
}
```
進入 `rev_recurive` 後, `first` 和 `rest` 會更新成下面這樣。
```graphviz
digraph G {
graph[pencolor=transparent];
rankdir=LR
rest[shape=none];
node_0[shape=box];
head[shape=none,label="first"];
node_1[shape=box];
node_2[shape=box];
NULL_1[shape=none, label="NULL"];
node_0->node_1->node_2->NULL_1;
{rank=same;head->node_1}
{rank=same;rest->node_2}
}
```
一直遞迴下去,直到 `rest` 碰到 `NULL` 進入 if 判斷式。
```graphviz
digraph G {
graph[pencolor=transparent];
rankdir=LR
rest[shape=none];
node_0[shape=box];
head[shape=none,label="first"];
node_1[shape=box];
node_2[shape=box];
NULL_1[shape=none, label="NULL"];
node_0->node_1->node_2->NULL_1;
{rank=same;head->node_2}
{rank=same;rest->NULL_1}
}
```
在 if 在判斷式中將目前 `first` 節點的位址指派給 `headRef` 目的是為了將 linked list 最後面節點的位址指派給原先傳進來的 head 位址。之後return 。
```graphviz
digraph G {
graph[pencolor=transparent];
rankdir=LR
rest[shape=none];
node_0[shape=box];
head[shape=none,label="first"];
headRef[shape=none];
node_1[shape=box];
node_2[shape=box];
NULL_1[shape=none, label="NULL"];
node_0->node_1->node_2->NULL_1;
{rank=same;headRef->head->node_2}
{rank=same;rest->NULL_1}
}
```
return 之後,繼續往下做還沒做完的事情,因為 return 回到遞迴的前一層,所以 `rest` 跑回 `first` 的位置, `first` 跑回指向前一個 node(回到進到函式前的狀態)。
```graphviz
digraph G {
graph[pencolor=transparent];
rankdir=LR
rest[shape=none];
node_0[shape=box];
head[shape=none,label="first"];
headRef[shape=none];
node_1[shape=box];
node_2[shape=box];
NULL_1[shape=none, label="NULL"];
node_0->node_1->node_2->NULL_1;
{rank=same;headRef->rest->node_2}
{rank=same;head->node_1}
}
```
繼續完成沒做完的事, `rest` 指向 `first` 指向的節點, `first` 指向的節點指向 `NULL`。此次的函式執行完畢,返回上一層遞迴。
```graphviz
digraph G {
graph[pencolor=transparent];
rankdir=LR;
rest[shape=none];
node_0[shape=box];
NULL_1[shape=none, label="NULL"];
head[shape=none,label="first"];
headRef[shape=none];
node_1[shape=box];
node_2[shape=box];
node_0->node_1;
{rank=same;headRef->rest->node_2}
{rank=same;head->node_1}
node_2->node_1->NULL_1
}
```
一樣 `rest` 跑回 `first` 的位置, `first` 跑回指向前一個 node。
```graphviz
digraph G {
graph[pencolor=transparent];
rankdir=LR;
rest[shape=none];
node_0[shape=box];
NULL_1[shape=none, label="NULL"];
head[shape=none,label="first"];
headRef[shape=none];
node_1[shape=box];
node_2[shape=box];
node_0->node_1;
{rank=same;headRef->node_2};
{rank=same;rest->node_1}
{rank=same;head->node_0}
node_2->node_1->NULL_1
}
```
和前一次的遞迴一樣做完剩下的工作。
```graphviz
digraph G {
graph[pencolor=transparent];
rankdir=LR;
rest[shape=none];
node_0[shape=box];
NULL_1[shape=none, label="NULL"];
head[shape=none,label="first"];
headRef[shape=none];
node_1[shape=box];
node_2[shape=box];
node_1->node_0->NULL_1;
{rank=same;headRef->node_2};
{rank=same;rest->node_1}
{rank=same;head->node_0}
node_2->node_1
}
```
### Fisher–Yates shuffle
參考 [wiki, Fisher–Yates shuffle](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle)進行實作。
```cpp
void shuffle(node_t **head)
{
int len = 0;
srand(time(NULL));
node_t **cursor = head;
while(*cursor) {
len++;
cursor = &(*cursor)->next;
}
node_t *first = NULL;
for (int i = len; i > 0; i--) {
int random = rand() % i;
cursor = head;
while (random--) {
cursor = &(*cursor)->next;
}
node_t *new_node = *cursor;
*cursor = (*cursor)->next;
new_node->next = NULL;
if (first) {
new_node->next = first;
first = new_node;
} else {
first = new_node;
}
}
*head = first;
}
```
* 一開始會去計算整個 linked list 的長度有多長。知道長度後就開始進入該演算法的實作。
* 一開始會宣告 `first` ,這是待會要指派給 head 的新的 linked list 的頭。
* 隨後 `cursor` 會去找被挑選中的 node 。
* 找到後會有一個 `new_node` 指標去指向這個被找到的 node 。而 `cursor` 則將此 node 的前一個和後一個的 node 連接。
* `new_node` append 到剛剛的 `first` 。直到迴圈結束。
* 迴圈結束後,將原先的 `head` 的更換為 `first` 。
:::warning
延伸問題 :fire: :fire: :fire:
* 思考可能的更好的寫法,能否降低時間複雜度。
* 或是其他更好的洗牌演算法。
:::