# 2021q1 Homework1 (quiz1)
contributed by < `WilliamShen0715` >
## Topic
- [x] 以 linked list 實現 QuickSort 的程式碼原理概述
- [x] 解決 random 多次執行結果相仿之問題
- [x] Non-recursive QuickSort ( Optimized QuickSort )
- [ ] 探討並改寫 linux-list 的 quick sort
- [ ] 基於 A Comparative Study of Linked List Sorting Algorithms 實現高效率的 linked list 排序演算法(https://pages.mtu.edu/~shene/PUBLICATIONS/1996/3Conline.pdf)
## 針對 linked list 實作 Quick Sort
首先是一些基本型態的宣告以及副程式
### 定義節點
```cpp
typedef struct __node {
int value;
struct __node *next;
} node_t;
```
### 增加新節點至串列
```cpp
static inline void list_add_node_t(node_t **list, node_t *node_t) {
node_t->next = *list;
*list = node_t;
}
```
參數為一個串列與一個節點
1. `node_t->next = *list` 將節點的下一個節點連接串列的排頭
2. `*list = node_t` 將排頭指向此節點
```graphviz
digraph{
rankdir=LR
p [label="new node" shape="box"]
n2 [label="list(A)" shape="box"]
n3 [label="B" shape="box"]
n4 [label="C" shape="box"]
remaining [label="......" shape="none"]
p -> n2 -> n3 -> n4 -> remaining
}
```
```graphviz
digraph{
rankdir=LR
p [label="list(new node)" shape="box"]
n2 [label="A" shape="box"]
n3 [label="B" shape="box"]
n4 [label="C" shape="box"]
remaining [label="......" shape="none"]
p -> n2 -> n3 -> n4 -> remaining
}
```
### 結合左右串列
```cpp
static inline void list_concat(node_t **left, node_t *right) {
while (*left)
left = &((*left)->next)
*left = right;
}
```
參數為左右串列
1. `while (*left) left = &((*left)->next)` 移至左串列末端的下一個節點(NULL)
2. `*left = right` 此節點指向右串列
```graphviz
digraph{
rankdir=LR
n2 [label="left(A)" shape="box"]
n3 [label="NULL" shape="none"]
n4 [label="last node" shape="box"]
remaining [label="......" shape="none"]
n2 -> remaining -> n4 -> n3
}
```
```graphviz
digraph{
rankdir=LR
n2 [label="A" shape="box"]
n3 [label="left(NULL)" shape="none"]
n4 [label="last node" shape="box"]
remaining [label="......" shape="none"]
n2 -> remaining -> n4 -> n3
}
```
```graphviz
digraph{
rankdir=LR
p [label="A" shape="box"]
n2 [label="last node" shape="box"]
n3 [label="..." shape="none"]
n4 [label="right" shape="box"]
remaining [label="......" shape="none"]
p -> remaining -> n2 -> n4 -> n3
}
```
### Quick Sort
``` 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;
}
```
這裡就是 QuickSort 的主程式,主要分成4個步驟:
1. 首先選定 pivot ( 這裡選擇第一個節點當pivot ) 作為分割子問題(子串列)的參考節點
``` cpp
if (!*list) return;
node_t *pivot = *list;
int value = pivot->value;
node_t *p = pivot->next;
pivot->next = NULL;
```
第一行是遞迴的終止條件,接著選擇第一個節點當 pivot,並設定相關初始條件
```graphviz
digraph{
rankdir=LR
pivot [label="NULL" shape="none"]
left [label="value | next" shape="box"]
left -> pivot
}
```
2. 依序走訪整個串列並比較大小,將 pivot 之外的所有點接到 left 跟 right 上,以此分別得到 "全部比 pivot 小" , "pivot" , "全部比 pivot 大" 這3堆,其中大與小分別對應 left 跟 right ,也同時是此排列問題的子問題。
``` c
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);
}
```
以下為示意圖
```graphviz
digraph{
rankdir=LR
pivot [label="B" shape="box"]
left [label="A" shape="box"]
right [label="C" shape="box"]
pivot2 [label="F" shape="box"]
left2 [label="D" shape="box"]
right2 [label="E" shape="box"]
left -> pivot -> right-> left2 -> right2 -> pivot2
}
```
left:
```graphviz
digraph{
rankdir=LR
pivot [label="D(<pivot)" shape="box"]
left [label="A(<pivot)" shape="box"]
right [label="E(<pivot)" shape="box"]
left -> pivot -> right
}
```
right:
```graphviz
digraph{
rankdir=LR
pivot [label="C(>pivot)" shape="box"]
left [label="B(>pivot)" shape="box"]
right [label="F(>pivot)" shape="box"]
left -> pivot -> right
}
```
3. 對 left , right 2個子串列分別做遞迴呼叫,由此得到排序好的左串列跟右串列
``` c
quicksort(&left);
quicksort(&right);
```
這裡就因此以遞迴的方式排好 left 跟 right 中的所有節點
4.這裡以前面的副程式 list_concat 結合 left, right 跟 pivot ,最終得到排序好的字串
```cpp
void quicksort(node_t **list)
{
node_t *result = NULL;
list_concat(&result, left);
list_concat(&result, pivot);
list_concat(&result, right);
*list = result;
}
```
這個排好的串列即為最後的 result
```graphviz
digraph{
rankdir=LR
pivot [label="pivot" shape="box"]
left [label="left (Sorted)" shape="box"]
right [label="right (Sorted)" shape="box"]
left -> pivot -> right
}
```
## random 多次執行結果相仿之問題
:::danger
下方的「一般來說」對於論述有幫助嗎?充其量只是某一個使用方式,連解決方案都稱不上。工程人員說話要精準。
:notes: jserv
:::
呼叫 [random(3)](https://man7.org/linux/man-pages/man3/random.3.html) 會有輸出相同的問題,這是由於沒有設定用於亂數的種子。一般來說,給予包含時間資訊的種子可以有效避免這種問題。
e.g. 下面將 `srand(time(NULL))` 執行後再執行 rand(),再分別執行 2 次看看結果
```cpp
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
void main()
{
srand(time(NULL));
printf("%d\n", rand());
}
```
第一次:
```1979```
第二次:
```1985```
第三次:
```1995```
結果確實都不一樣。不過看起來卻不夠"亂",這是因為當執行的時間很接近時,時間種子所的到的值也會相對接近。
由於 C 的 rand() 產生亂數的原理是基於 Linear Congruential Method 的 Pseudorandom, 也就是將前一項 rand 值以線性的方式對應,再取餘數
遞迴關係如下:
```Rn =( a*Rn-1 + b ) mod K```
a,b 是常數, K為給定的範圍區間
若未設定隨機函數的種子,則首項預設為0,否則首項就是給定的種子,因此,我們發現,儘管給定了時間作為亂數種子,卻沒有改變"決定亂數的方式",造成亂數分佈不理想。
對於隨機性問題的改良方案,在 C++ STL 中有能夠調整對應參數的 [<random>](https://www.cplusplus.com/reference/random/) 可用。
在這之中,有別於常常被使用的時間做為種子,這個函式庫提供了專門用於產生亂數種子的 std::random_device ,因為它足夠隨機卻不適合預測大數的特性,可作為更好用的亂數種子。
使用上則必須在呼叫前分別事先執行2個步驟以確保數字的分布足夠亂:
1.設定引擎 ( engine )
2.設定資料分布 ( distribution )
這分別對應決定隨機的演算法以及給定數值的區間,以此可以得到符合預期的亂數
:::danger
注意作業規範:中英文間用一個空白字元區隔。注重細節並充分掌握,來日才可挑戰 Linux 核心這樣的龐然大物。
上述的描述不足以解釋亂數產生器原理和考量,請改進!
:notes: jserv
:::
---
## Non-recursive QuickSort ( Optimized QuickSort )
參考 [Optimized QuickSort](https://alienryderflex.com/quicksort/) 此篇的做法,並將它原本的陣列改為鏈結串列的方式呈現,基本想法是將原本遞迴的部分以堆疊的方式處理,將操作的頭尾指標放入堆疊中,再逐一取出來處理。
基本的宣告與初始化與上面遞迴版本的處理相同,只改變 quicksort 這個主程式片段。
程式碼如下:
```cpp
void quicksort_iterative(node_t **list)
{
#define MAX_LEVELS 1000
if (!*list)
return;
int i = 0, j = 0;
node_t *stack[MAX_LEVELS];
node_t *sorted;
sorted = NULL;
stack[0] = *list;
```
首先是對於相關面術語堆疊的宣告,這裡宣告了一個堆疊作為每次執行 quicksort 的位置紀錄,包含首尾,所以每次操作時接放入或取出頭部和尾部的指標。
這裡定義 MAX_LEVELS 代表的是堆疊的空間大小,須確保切割的片段數 < ( MAX_LEVELS\2 ) 以免造成記憶體錯誤。
從這裡也可以看出,前面所實作的遞迴版本,若每次的 pivot 選取不當,無法有效的區分出兩個區域,而是集中在一邊,則可能造成 O(n) 的空間複雜度,理想狀況則是 O(lgn) ,相較於這裡使用的方法,這裡每次切割的堆疊皆只會用到2個指標的大小,在空間的利用上有很大的改善。
```iterate2
while(i>=0 && i<MAX_LEVELS){
if(!stack[i]){
i--;
continue;
}
if(!stack[i]->next){
stack[i]->next = sorted;
sorted = stack[i];
i--;
continue;
}
node_t *pivot = stack[i];
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);
}
stack[i] = left;
stack[i+1] = pivot;
stack[i+2] = right;
i +=2;
}
*list = sorted;
}
```
這裡的行為就是檢查堆疊並將它一一取出,每次取出之後,跟遞迴版本的操作相同,分為左右兩個子串列,並將這兩個子串列連同 pivot 依序放入此堆疊中。
用這個方式不斷的操作此堆疊,拆分 left 跟 right ,直到 stack 為空,跳出此迴圈,此時得到的鏈結串列即為排序好的串列。
## 探討並改寫 linux-list 的 quick sort
## 基於 A Comparative Study of Linked List Sorting Algorithms 實現高效率的 linked list 排序演算法