# 2021q1 Homework1 (quiz1)
contributed by < `xxiex123` >
## 延伸問題1
解析以下code
``` cpp
static inline void list_add_node_t(node_t **list, node_t *node_t) {
node_t->next = *list;
*list = node_t;
}
static inline void list_concat(node_t **left, node_t *right) {
while (*left)
left = &((*left)->next);
*left = right;
}
void quicksort(node_t **list)
{
if (!*list)
return;
node_t *pivot = *list;
int value = pivot->value;
node_t *p = pivot->next;
pivot->next = NULL;
node_t *left = NULL, *right = NULL;
while (p) {
node_t *n = p;
p = p->next;
list_add_node_t(n->value > value ? &right : &left, n);
}
quicksort(&left);
quicksort(&right);
node_t *result = NULL;
list_concat(&result, left);
list_concat(&result, pivot); list_concat(&result, right);
*list = result;
}
```
我們先從簡單的兩個function著手瞭解:
**list_add_node_t**
```graphviz
digraph graph1{
node [shape=box];nodeA;
{
rank="same";
nodeA->null;
}
}
```
假設需要加入一個nodeB,經過list_add_node_t(&list,nodeB);后
(把nodeB從list頭部加入)
```graphviz
digraph graph2{
node [shape=box];
{
rank="same";
nodeB->nodeA->null;
}
}
```
nodeB是以pass by value的方式被assign到nodeA的前面。
**list_concat**
從上面的結果延續,假設想要從上面的list的尾部接上另外一組list,
```graphviz
digraph graph4{
node [shape=box];
{
rank="same";
D->C->null;
}
}
```
經過list_concat(&nodeB,D);之後
```graphviz
digraph graph3{
node [shape=box];
{
rank="same";
nodeB->nodeA->D->C->null;
}
}
```
其中,D為head的list是以pass by value的方式assign 到nodeB的list。
**quicksort**
在瞭解主程式碼之前,先知道一下quicksort的概念:
quicksort主要的想法是從一組數中取其中一個數,叫做pivot,它被用來作爲一個參照,區分找出這組數中、比pivot的值小的和比pivot大的,再把較小的數全部排到pivot的左邊,較大的排到右邊。由此可得到兩組未被排序好的數組,分別爲pivot左邊的數組和pivot 右邊的數組。
被quicksort 所區分出來的數組的個數會大於等於1,小於等於n-1(n為被quicksort處理的數組大小),所以每quicksort 一次都會降低排序問題的複雜度,因此只要不斷對子數組做quicksort,就能得到一個已排序好的數組。
以下為實際例子
``` graphviz
digraph graph4{
node[shape=record];
struct [label="<0>7|<1>4|<2>9|<3>3|<4>8"]
}
```
初始狀態,讓參數 j從大到小執行直到 j=1。
``` graphviz
digraph graph4{
node[shape=record];
struct [label="<0>7|<1>4|<2>9|<3>3|<4>8"]
node[shape=oval]
pivot->struct:0;
i->struct:4;
j->struct:4;
}
```
(loop j=4)因8>7, 把8換到i的位置(這裏的情況也就是他自己)并且i-1
``` graphviz
digraph graph4{
node[shape=record];
struct [label="<0>7|<1>4|<2>9|<3>3|<4>8"]
node[shape=oval]
pivot->struct:0;
i->struct:4;
j->struct:4;
}
```
(loop j=3)因3<7,不執行任何指令
``` graphviz
digraph graph4{
node[shape=record];
struct [label="<0>7|<1>4|<2>9|<3>3|<4>8"]
node[shape=oval]
pivot->struct:0;
i->struct:3;
j->struct:3;
}
```
(loop j=2),9>7,把9換到i的位置并且i-1.
``` graphviz
digraph graph4{
node[shape=record];
struct [label="<0>7|<1>4|<2>9|<3>3|<4>8"]
node[shape=oval]
pivot->struct:0;
struct:3->struct:2[color=red,dir=both]
i->struct:3;
j->struct:2;
}
```
(loop j=1),4<7,不執行任何指令
``` graphviz
digraph graph4{
node[shape=record];
struct [label="<0>7|<1>4|<2>3|<3>9|<4>8"]
node[shape=oval]
pivot->struct:0;
i->struct:2;
j->struct:1;
}
```
(loop j=0),for loop 結束,并且把pivot 換到i的位置
``` graphviz
digraph graph4{
node[shape=record];
struct [label="<0>7|<1>4|<2>3|<3>9|<4>8"]
node[shape=oval]
pivot->struct:0;
i->struct:2;
j->struct:0;
}
```
由此形成以pivot左右兩邊的兩組組未排序數組,因此我們要再次對它們做quick sort,因此一般quick sort是一種迭代函數
``` graphviz
digraph graph4{
node[shape=record];
struct [label="<0>3|<1>4|<2>7|<3>9|<4>8"]
node[shape=oval]
pivot->struct:2;
}
```
最終形成
``` graphviz
digraph graph4{
node[shape=record];
struct [label="<0>3|<1>4|<2>7|<3>8|<4>9"]
node[shape=oval]
pivot->struct:2;
}
```
知道了quick sort的概念后,我們便很容易瞭解用來 sort link list 的主程式碼,
```cpp=
void quicksort(node_t **list)
{
if (!*list)
return;
node_t *pivot = *list;
int value = pivot->value;
node_t *p = pivot->next;
pivot->next = NULL;
node_t *left = NULL, *right = NULL;
while (p) {
node_t *n = p;
p = p->next;
list_add_node_t(n->value > value ? &right : &left, n);
}
quicksort(&left);
quicksort(&right);
node_t *result = NULL;
list_concat(&result, left);
list_concat(&result, pivot); list_concat(&result, right);
*list = result;
}
```
這裏的quick sort與之前的例子概念一致,只是被設計成針對link-list的資料結構而不是前面的array。
以下圖的list為例子做實際示範
``` graphviz
digraph graph4{
node[shape=record]
struct1 [label="<v> 8|<next> next"];
struct2 [label="<v> 4|<next> next"];
struct3 [label="<v> 3|<next> next"];
struct4 [label="<v> 7|<next> next"];
struct5 [label="<v> 9|<next> next"];
struct6 [shape=none,label="null"];
rankdir=LR
struct1:next->struct2;
struct2:next->struct3;
struct3:next->struct4;
struct4:next->struct5;
struct5:next->struct6;
}
```
程式碼内:
3,4行是迭代函數的中斷條件
6-9行只是從list 中取head作爲pivot
11行宣告兩組list,分別爲left 和right,用來區分比pivot大或比pivot小的數
直到11行爲止,狀態如下
``` graphviz
digraph graph4{
node[shape=record]
struct1 [label="p"]
struct2 [label="<v> 4|<next> next"];
struct3 [label="<v> 3|<next> next"];
struct4 [label="<v> 7|<next> next"];
struct5 [label="<v> 9|<next> next"];
struct6 [shape=none,label="null"];
rankdir=LR
struct7 [label="left|{<v>nan|<next> next}"];
struct8 [label="right|{<v>nan |<next> next}"];
struct9 [shape=none,label="null"];
struct10 [shape=none,label="null"];
struct12 [shape=none,label="null"];
struct13 [shape=record,label="value|8"]
struct11 [label="pivot|{<v>8|<next>next}"];
struct1->struct2
struct2:next->struct3;
struct3:next->struct4;
struct4:next->struct5;
struct5:next->struct6;
struct7:next->struct9;
struct8:next->struct10;
struct11:next->struct12;
}
```
下面開始做數組的區分(程式碼12-16行)
若p不是指向null,就把p指向的node拿來和pivot做對比,如果比pivot小,就把其插入到left 的集合裏,相反則插入到right集合,
之後把p指向下一個元素,繼續進行該指令,直到p指向null爲止(list 結束比較)
比較結束后,產生如下3個list
``` graphviz
digraph graph4{
node[shape=record]
struct3 [label="<v> 3|<next> next"];
struct4 [label="<v> 7|<next> next"];
rankdir=LR
struct7 [label="right|{<v>9|<next> next}"];
struct8 [label="left|{<v>4 |<next> next}"];
struct9 [shape=none,label="null"];
struct10 [shape=none,label="null"];
struct12 [shape=none,label="null"];
struct11 [label="pivot|{<v>8|<next>next}"];
struct7:next->struct9;
struct8:next->struct3->struct4->struct10;
struct11:next->struct12;
}
```
left的list 值比pivot 低,right 的list 值都比pivot 高,并且left right 都是未被排序的,所以把left 和right 都丟入quicksort 做排序。
因爲right只有一個元素,所以進入quick sort后滿足第4行的條件,直接退出不需要做,而left 有多餘一個element 所以需要再跑一次quick sort。
``` graphviz
digraph graph4{
node[shape=record]
struct3 [label="<v> 3|<next> next"];
struct4 [label="<v> 7|<next> next"];
rankdir=LR
struct8 [label="left|{<v>4 |<next> next}"];
struct10 [shape=none,label="null"];
struct8:next->struct3->struct4->struct10;
}
```
做完quick sort 得到
``` graphviz
digraph graph4{
node[shape=record]
rankdir=LR
struct7 [label="right|{<v>7|<next> next}"];
struct8 [label="left|{<v>3 |<next> next}"];
struct9 [shape=none,label="null"];
struct10 [shape=none,label="null"];
struct12 [shape=none,label="null"];
struct11 [label="pivot|{<v>4|<next>next}"];
struct7:next->struct9;
struct8:next->struct10;
struct11:next->struct12;
}
```
因這裏left right 都只有一個元素,所以不需要再做quick sort。
在這時候就會執行第21-24行程式碼,主要是把這三個list 以
left-> pivot - > right 整合在一起。
因此得到
``` graphviz
digraph graph4{
node[shape=record]
struct3 [label="<v> 4|<next> next"];
struct4 [label="<v> 7|<next> next"];
rankdir=LR
struct8 [label="left|{<v>3 |<next> next}"];
struct10 [shape=none,label="null"];
struct8:next->struct3->struct4->struct10;
}
```
這時第二層的quick sort執行完畢,把left 變成上圖已排序好的形式了,所以第一層接著執行剛剛 combine 三個list 的動作,最終形成
``` graphviz
digraph graph4{
node[shape=record]
struct1 [label="<v> 3|<next> next"];
struct2 [label="<v> 4|<next> next"];
struct3 [label="<v> 7|<next> next"];
struct4 [label="<v> 8|<next> next"];
struct5 [label="<v> 9|<next> next"];
struct6 [shape=none,label="null"];
rankdir=LR
struct1:next->struct2->struct3->struct4->struct5->struct6;
}
```
測試程式碼内用到random()的函數,但random 函數在沒有給予seed的情況下,會以一比固定的亂數作爲random 輸出的參考,因此每次執行程式碼時,都用固定的亂數,造成每次的結果都一模一樣。
```cpp
int main(int argc, char **argv) {
size_t count = 20;
node_t *list = NULL;
while (count--)
list = list_make_node_t(list, random() % 1024);
```
要改變這種囧境,我們可以給予random函數一個seed,設定seed 的方法為 srand(**要給的seed**),想要每次執行都得到不同的亂數,通常以time(0)為seed,time 函數會回傳現在的即時時間,因時間一直在走動,所以產生出來的亂數也每分每秒都不一樣。
可以參考http://www.cplusplus.com/reference/cstdlib/srand/
```cpp
int main(int argc, char **argv) {
size_t count = 20;
srand(time(0));
node_t *list = NULL;
while (count--)
list = list_make_node_t(list, random() % 1024);
```
---
## 延申問題2
由參考[Optimized QuickSort — C Implementation (Non-Recursive)](https://alienryderflex.com/quicksort/)
的文章,該作者主要利用兩個array 來當作stack去處理recursion 的邏輯運作,但因爲這樣可能造成**處理大筆資料**的時候因預先設定好的array不足夠大而處理失敗(因爲所需要的array 最大空間可能會是‘所處理的資料數量’-1)。
但作者也有提到相對應的解決方案,利用了時間成本換取了執行成功的保証。
利用作者所提供的演算法套入我們討論的link-list quick sort問題,解決不需要recursion來執行quick sort。
具體改變如下
```cpp=
int findLength(node_t **list){
node_t *p =*list;int lenght =0;
while(p){
lenght++;
p=p->next;
}
return lenght;
}
void findLocat(node_t **list,int target){
while(target>1){
list=&((*list)->next);
target--;
}
if(target ==1){
node_t *t = (*list)->next;
(*list)->next=null;
list=&t;
}
}
void quicksort(node_t **list)
{
#define MAX_LEVELS 300
int value,beg[MAX_LEVELS], end[MAX_LEVELS], i=0,Lend=0, Rend =0;
Lend = findLength(*list);
beg[0]=0; end[0]=Lend-1;
while(i>=0){
node_t *left = NULL, *right = NULL;
node_t *pivot = *list;
findLocat(&pivot,beg[i]);
value = pivot->value;
node_t *p = pivot->next;
node_t *prev = pivot;
if(end[i]>0){
while (end[i]>0) {
node_t *n = p;
p = p->next;
if(n->value > value){
Rend++; prev = n;
}
else{
Lend++; prev->next = p;
list_add_node_t(&left,n);
}
end[i]--;
}
node_t *result = *list;
if(beg[i]==0)
result = NULL;
list_concat(&result, left);
list_concat(&result, pivot);
list_concat(&result, right);
*list = result;
if (Lend>=Rend) {
beg[i+1]=Lend+2;
end[i]=Lend-1; end[i+1]=Rend-1;i++; }
else {
beg[i+1]=beg[i];beg[i]=Lend+2;
end[i]=Rend-1; end[i+1]=Lend-1;i++; }
}
else
i--;
}}
```
想要代替 recursion ,通常都需要利用到類似 stack 的概念,因爲 recursion 的行爲就與 stack 有異曲同工之妙,因此學習作者利用 beg 與 end 的空間(FILO)來存放工作序列。
分類方法的具體差別在於 singly link-list 不像 array,不能直接指定位置,因此排序時的操作需要修改。
修改方法利用了 list 的特性(易拆解、易合并),來避免用到 list 尾部的資料,提升效率。
``` graphviz
digraph graph4{
node[shape=record]
struct1 [label="<v> 7|<next> next"];
struct2 [label="<v> 4|<next> next"];
struct3 [label="<v> 3|<next> next"];
struct4 [label="<v> 8|<next> next"];
struct5 [label="<v> 9|<next> next"];
struct6 [shape=none,label="null"];
rankdir=LR
struct1:next->struct2->struct3->struct4->struct5->struct6;
}
```
``` graphviz
digraph graph4{
node[shape=record]
struct1 [label="<v> 7|<next> next"];
struct2 [label="<v> 4|<next> next"];
struct3 [label="<v> 3|<next> next"];
struct4 [label="<v> 8|<next> next"];
struct5 [label="<v> 9|<next> next"];
struct6 [shape=none,label="null"];
struct7 [shape=none,label="null"];
rankdir=LR
pivot->struct1;
smaller->struct2->struct3->struct7;
bigger->struct4->struct5->struct6;
}
```
到這裏會記錄下 smaller 與bigger 的 list 的開始位置與結束位置,作爲下一筆要處理的資料存放到 beg 和 end 。
concat 之後:
``` graphviz
digraph graph4{
node[shape=record]
struct1 [label="<v> 4|<next> next"];
struct2 [label="<v> 3|<next> next"];
struct3 [label="<v> 7|<next> next"];
struct4 [label="<v> 8|<next> next"];
struct5 [label="<v> 9|<next> next"];
struct6 [shape=none,label="null"];
rankdir=LR
struct1:next->struct2->struct3->struct4->struct5->struct6;
}
```
從上圖可以看到 linked-list 的分類方法與平常的 array 的不同之處。
另外作者的描述,如果每一次選擇較短的資料做為下一筆的分類目標,則可以使該演算法適用于任意大小的 list ,而不像前面説的如果超過MAXLEVEL 就有可能排序失敗。
從數學的角度去分析,當一筆資料被分成兩半時,較短的一邊大小肯定小於等於原資料的二分之一。因此在 MAXLEVEL = 300 的同時,意味著可以處理 $2^{300}$ (比宇宙原子數量多) 大小的 list ,超過這個大小的問題 ,基本上微乎其微。
程式碼下方也加上了同樣的機制。
```cpp
if (Lend>=Rend) {
beg[i+1]=Lend+2;
end[i]=Lend-1; end[i+1]=Rend-1;i++; }
else {
beg[i+1]=beg[i];beg[i]=Lend+2;
end[i]=Rend-1; end[i+1]=Lend-1;i++; }
}
```
---
## 延申問題3
以上所探討的程式碼都是基於簡單的linked list 結構
```cpp
typedef struct __node {
int value;
struct __node *next;
} node_t;
```
而linux-list 的資料結構較爲複雜。linux-list 先是定義出一個list-head
```cpp
struct list_head {
struct list_head *prev;
struct list_head *next;
};
```
其是一個doubly-linked list,并且再針對其定義出各種作業函數
```cpp
container_of(ptr, type, member)
static inline void INIT_LIST_HEAD(struct list_head *head)
static inline void list_add(struct list_head *node, struct list_head *head)
static inline void list_del(struct list_head *node)
static inline int list_empty(const struct list_head *head)
static inline int list_is_singular(const struct list_head *head)
static inline void list_splice(struct list_head *list, struct list_head *head)static inline void list_cut_position(struct list_head *head_to,struct list_head *head_from,struct list_head *node)
static inline void list_move(struct list_head *node, struct list_head *head)
list_entry(node, type, member)
list_for_each(node, head)
```
因爲這些作業函數是以list 為circular為前提才能正常操作,因此若慾使用其作業函數,最好把linked-list用環狀鏈接。
有了這些作業函數,我們可以對list-head 做許多操作,因爲list-head也**只有**指向前後兩個list-head 形態的資料,所以可想而知上面這些作業函數不過是幫助找到list中的某個node。
這種資料形態并沒有自己的資料變數(像之前討論的就有value作爲儲存資料的變數),因此它是被用來建構在一個有資料變數的新結構裏面,使新的結構具備完整linked-list 的特質。如下:
```cpp
typedef struct __element {
char *value;
struct __element *next;
struct list_head list;
} list_ele_t;
```
資料變數為value, next 是指到下一個__element 形態的物件,可以當作singular linked list 來使用,而 list_head 形態的list就是用來做doubly-linked list的工具。
這個外層的結構可以依照需求去自行定義,而list_head 扮演著的是提供結構一個doubly-linked list的工具。