# 2020q3 Homework1 (quiz1)
contributed by< `JimmyLiu0530` >
###### tags: `進階電腦系統理論與實作`
- [Linked list 測驗題目](https://hackmd.io/@sysprog/sysprog2020-quiz1)
## 測驗1: singly linked list
題目給定 linked list 結構定義為:
```c=
typedef struct __node {
int value;
struct __node *next;
} node_t;
```
且已知不存在 circular (環狀結構) 情況下,對單向 linked list 進行操作
圖例:
```graphviz
digraph foo{
rankdir=LR;
node [shape=record];
a [label="{ <data> 12 | <ref> }"]
b [label="{ <data> 54 | <ref> }"];
c [label="{ <data> 37 | <ref> }"];
a:ref:c -> b:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false];
b:ref:c -> c:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false];
c:ref:c -> NULL [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false];
}
```
### 原始測驗題的思考及理解
- `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;
AA1;
while (*indirect)
indirect = &(*indirect)->next;
AA2;
}
```
#### 核心觀念: 找到整串 linked list 的尾巴,將它指向新增的節點
運作原理如下:
1. **第3行** 建立一個指向 head 的指標 indirect ( pointer to pointer )
```mermaid
graph LR
subgraph linked list
head==>node1==>node2
end
subgraph pointer to pointer
indirect==>head
end
```
2. **第5-7行** 配置一個 node_t 的空間,並初始化
3. **第9行** 應選 `assert(new_node)`,用來檢查new_node是否合法
4. **第10-11行** 用來找到 linked list 的最後一個 node。下圖展示第一圈 while 後的結果,以此類推
```mermaid
graph LR
subgraph linked list
head==>node1==>node2
end
subgraph pointer to pointer
indirect==>node1
end
```
5. **第12行** 應選***indirect = new_node**,把最後一個 node 的 next 指向 new_node,增加節點完成
:::warning
**Note**
- `->` 的執行順位大於 `&` ,所以要先進行 `->` 運算再運算 `&`
:::
- `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);
}
```
#### 核心觀念: 繞過欲刪除的節點
運作原理如下:
1. **第3行** 建立一個指向 head 的指標 indirect ( pointer to pointer )
```mermaid
graph LR
subgraph linked list
head==>node1==>entry==>node3
end
subgraph pointer to pointer
indirect==>head
end
```
2. **第5-6行** 找到欲刪除的節點 entry
```mermaid
graph LR
subgraph linked list
head==>node1==>entry==>node3
end
subgraph pointer to pointer
indirect==>node1
end
```
3. **第8行** 繞過 entry,也就是說把 entry 的上一個節點改指向 entry 的下一個節點
```mermaid
graph LR
subgraph linked list
head==>node1==>node3
entry==>node3
end
subgraph pointer to pointer
indirect==>node1
end
```
4. **第9行** 釋放 entry 的記憶體資源
```mermaid
graph LR
subgraph linked list
head==>node1==>node3
end
subgraph pointer to pointer
indirect==>node1
end
```
- `swap_pair`
> 交換一對相鄰的節點,取自 [LeetCode: Swap Nodes in Pairs](https://leetcode.com/problems/swap-nodes-in-pairs/),給定 `1->2->3->4`,則該回傳 `2->1->4->3`
程式碼如下:
```c=
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;
}
```
#### 核心觀念: 倆倆一組,相關指標做改變
運作原理如下:
1. **第3行 for 條件** 應選 `node = &(*node)->next->next`,因為此 function 為兩兩交換,因此下一輪 node pointer 應指向目前這對的尾,如下所示:
```mermaid
graph LR
subgraph linked list
head==>node1==>node2==>node3==>node4
end
subgraph pointer to pointer
node==>node2
end
```
2. **第4行** 建立一個 tmp 指向該對的頭,如下所示:
```mermaid
graph LR
subgraph linked list
head==>node1==>node2==>node3==>node4
end
subgraph pointer to pointer
node==>head
end
subgraph temp
tmp==>node1
end
```
3. **第5行** 應選 `*node = (*node)->next`,使 head 改指向該對的尾(e.g. node2),如下所示:
```mermaid
graph LR
subgraph linked list
head==>node2
node1==>node2==>node3==>node4
end
subgraph pointer to pointer
node==>head
end
subgraph temp
tmp==>node1
end
```
4. **第6行** 該對的頭 (e.g. node1) 的 next 改指向下一對的頭 (e.g. node3),如下所示:
```mermaid
graph LR
subgraph linked list
head==>node2
node1==>node3
node2==>node3==>node4
end
subgraph pointer to pointer
node==>head
end
subgraph temp
tmp==>node1
end
```
5. **第7行** 該對的尾 (e.g. node2) 的 next 改指向頭 (e.g. node1),如下所示:
```mermaid
graph LR
subgraph linked list
head==>node2
node1==>node3
node2==>node1
node3==>node4
end
subgraph pointer to pointer
node==>head
end
subgraph temp
tmp==>node1
end
```
6. 之後依此類推...直到交換結束,或是只剩沒成對的node。最後 return 交換後整個 linked list 的頭
- `reverse`
> 將給定的 linked list 其內節點予以反向,即 `1->2->3->4`,則該回傳 `4->3->2->1`
程式碼如下:
```c=
node_t *reverse(node_t *head)
{
node_t *cursor = NULL;
while (head) {
node_t *next = head->next;
CCC;
head = next;
}
return cursor;
}
```
#### 核心觀念: 將 cursor 視為指向 head 的前一個節點,next 視為指向 head 的後一個節點。不斷更新(往後挪)三個指標 cursor、head、next直到 linked list 的尾巴
運作原理如下:
1. **第5行** 創建一個指標 next 指向目前 head 所指的節點的下一個節點。如圖 next 指向 node2
```mermaid
graph LR
subgraph linked list
head==>node1==>node2==>node3==>node4
next==>node2
cursor==>NULL
end
```
2. **第6行** 應選 `head->next = cursor; cursor = head` ,如圖先將 head->next (i.e. node1) 指向 cursor 指的 NULL,再將 cursor 指向 head
```mermaid
graph LR
subgraph linked list
head==>node1
cursor==>node1
next==>node2==>node3==>node4
node1==>NULL
end
```
3. **第7行** 將 head 指標往後挪一個節點,如圖 head 從指向 node1 變成 node2
```mermaid
graph LR
subgraph linked list
head==>node2
cursor==>node1
next==>node2==>node3==>node4
node1==>NULL
end
```
### 延伸問題
#### 1.避免回傳指標的函式 `swap_pair_revised` & `reverse_revised`
說明: 利用指標的指標對 `swap_pair` 及 `reverse` 兩函式做改寫,來達到不需回傳整串 linked list 的 head
- `swap_pair_revised`
程式碼如下:
```c=
void swap_pair_revised(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;
}
}
```
主程式呼叫:
`swap_pair_revised(&head);`
運作原理如下:
1. 與函式 `swap_pair` 只有傳入 parameter 的方式不同,差別在於 `swap_pair` 採 **pass by value** 的方法;而 `swap_pair_revised` 則是採 **pass by reference** 的方法。有關 pass by value 跟 reference 的詳細解說,可到 [Pass by value vs Pass by reference](https://medium.com/@racktar7743/c-c-%E6%8C%87%E6%A8%99%E6%95%99%E5%AD%B8-%E5%9B%9B-pass-by-value-vs-pass-by-reference-ed5882802789) 觀看
- `reverse_revised`
程式碼如下:
```c=
void reverse_revised(node_t **indirect)
{
node_t *cursor = NULL;
while (*indirect) {
node_t *next = (*indirect)->next;
(*indirect)->next = cursor;
cursor = *indirect;
*indirect = next;
}
*indirect = cursor;
}
```
主程式呼叫:
`reverse_revised(&head);`
運作原理如下:
1. 與原函式概念一樣,只是改採用 pass by reference 的方法,因此一些相對應的變數(e.g. while 的條件從 `head` 改成 `*indirect`)也須做修改
2. **第10行** 最後再把新的 linked list 頭的位置 assign 到 head (i.e. *indirect) 去
#### 2. 以遞迴改寫 `reverse` 函式
首先,建立**遞迴**演算法有幾個重點: (a) **終止條件** (b) **將原問題變成與其相似且規模較小的問題**
程式碼如下:
```c=
node_t *rev_recursive(node_t *p)
{
if(p->next == NULL)
{
return p;
}
node_t *head = rev_recursive(p->next);
node_t *q = p->next;
q->next = p;
p->next = NULL;
return head;
}
```
主程式呼叫:
`head = rev_recursive(head);`
運作原理如下:
1. **第3-6行** 即為終止條件。if 條件若成立,代表 p 為此 linked list 的最後一個節點,也就是新的 head,因此回傳該位址
2. **第7行** 遞迴呼叫 rev_recursive(),並將新的 head 記錄下來
3. **第8-10行** 舉例來說,假設 p 指向 node2,則 q 指向 node3。經過這幾行的處理後,第一張圖會變成第二張圖,也就是 node3->next 指向 node2,且 node2->next 指向 NULL
```mermaid
graph LR
subgraph linked list
head==>node1==>node2==>node3==>NULL
end
```
```mermaid
graph LR
subgraph linked list
head==>node1==>node2==>NULL
node3==>node2
end
```
#### 3. 針對 singly-linked list 的節點,實作 [Fisher–Yates shuffle](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle),儘量降低記憶體的使用量
什麼是 `Fisher–Yates shuffle`?
> 用來將一個有限集合生成一個隨機排列的方法,簡單來說,該算法可以洗牌序列。該算法產生無偏頗的排列,代表每種排列的可能性均等。
程式碼如下
```c=
// this swap function is under the premise that
// 1 <= p <= q <= total number of node in the linked list
// and is used to swap pth and qth node in the linked list
void swap(node_t **head, int p, int q)
{
// nothing to do if p,q are the same
if(p == q)
return;
// search for pth node in the linked list
node_t *prevP = NULL, *currP = *head;
for(int i = p-1; i>0; i--)
{
prevP = currP;
currP = currP->next;
}
// search for qth node in the linked list
node_t *prevQ = NULL, *currQ = *head;
for(int i = q-1; i>0; i--)
{
prevQ = currQ;
currQ = currQ->next;
}
// if pth node is head of linked list, i.e. p==1, make qth node a new head
if (p==1)
*head = currQ;
else
prevP->next = currQ;
// if q is 1, p must be 1 either, the process will return due to line7
prevQ->next = currP;
// swap next pointers
node_t *tmp = currQ->next;
currQ->next = currP->next;
currP->next = tmp;
}
void Fisher_Yates_shuffle(node_t **indirect)
{
// count how many nodes are in linked list
int totalNum = 0;
node_t *tmp = *indirect;
while(tmp)
{
totalNum += 1;
tmp = tmp->next;
}
srand(time(NULL));
// start from the first node and randomly choose a number q s.t. p<=q<=totalNum
for(int p = 1; p <= totalNum-1; p++)
{
int q = (rand() % (totalNum-p+1))+p;
swap(indirect, p, q);
}
}
```
主程式呼叫:
`Fisher_Yates_shuffle(&head);`
運作原理如下:
1. 先說明 `swap` 函式,主要將第 p 跟第 q 個節點交換
**第11-24行** 先找到第 p 跟第 q 個節點的位置,並記錄在 currP 及 currQ, prevP 跟 prevQ 則分別記錄 currP 及 currQ 的前一個節點
**第26-27行** 需考慮 p==1 的情況,也就是第 p 個節點是 head
**第33行** 之所以這裡不用 if-else 是因為在給定的前提中,1 <= p <= q。當 q=1 時,
p 勢必也是 1 ,因此會在 `line 8` retrun 掉了
**第36-38行** 做一些 `next` pointer 的交換
2. 此 `swap` 函式因為是 linked list,所以不像 array 在給定第 p 跟第 q 個元素做交換下可在O(1)完成,需 $O(n)$。
3. 接著看 `Fisher_Yates_shuffle` 函式,此函式精神在於在第 p 輪中,會隨機找一個介於 p 跟總節點數的數字 q,並將第 p 跟第 q 個交換
**第44-50行** 計算 linked list 的總節點數
**第55-60行** 從 linked list 的第一個節點開始,在之間隨機找一個節點做交換,再來換第二個節點...以此類推,一直重複做到倒數第二個節點
4. 此`Fisher_Yates_shuffle` 函式的時間複雜度為 $O(n^2)$,但空間複雜度只需 $O(1)$。