# 2020q1 Homework1 (lab0)
contributed by < `johnnycck` >
###### tags: `linux2020`
## 作業要求
1. 使用 git 版本控制,並熟悉 git 使用語法及規則
2. 修改 queue.c && queue.h,以 Linked-list 實做 queue,並完成以下功能
* `q_new`: 建立新的「空」佇列;
* `q_free`: 釋放佇列所佔用的記憶體;
* `q_insert_head`: 在佇列開頭 (head) 加入 (insert) 給定的新元素 (以 LIFO 準則);
* `q_insert_tail`: 在佇列尾端 (tail) 加入 (insert) 給定的新元素 (以 FIFO 準則);
* `q_remove_head`: 在佇列開頭 (head) 移除 (remove) 給定的元素。
* `q_size`: 計算佇列中的元素數量。
* `q_reverse`: 以反向順序重新排列鏈結串列,該函式不該配置或釋放任何鏈結串列元素,換言之,它只能重排已經存在的元素;
* `q_sort`:以==遞增順序==來排序鏈結串列的元素
* `q_insert_tail` 和 `q_size` 需要將原本 $O(n)$ 時間複雜度的實作改寫為 $O(1)$ 時間複雜度,亦即無論佇列空間為何,該操作僅需要固定數量的步驟
3. 修改排序所用的比較函式,變更為 [natural sort](https://github.com/sourcefrog/natsort),在 "simulation" 也該做對應的修改,得以反映出 [natural sort](https://github.com/sourcefrog/natsort) 的使用。
## 參考資料
* [2020 年系統軟體課程](https://www.facebook.com/groups/system.software2020/)
* [Linux 核心設計](http://wiki.csie.ncku.edu.tw/linux/schedule)
* [作業解說錄影](https://youtu.be/lAurEgzZ6YY?t=9912)
* [C Programming Lab](http://www.cs.cmu.edu/~213/labs/cprogramminglab.pdf)
* [How to Write a Git Commit Message](https://chris.beams.io/posts/git-commit/)
* [為你自己學 Git](https://gitbook.tw/)
* [natural sort](https://github.com/sourcefrog/natsort)
## 實做紀錄
### Define queue_t
```cpp
typedef struct {
list_ele_t *head; /* Linked list of elements */
list_ele_t *tail;
int size;
} queue_t;
```
* 將queue的型態確定,因 `q_size` 需要 $O(1)$ ,所以在型態中配置 `int size` 紀錄 queue 元素個數
### q_new
```cpp
queue_t *q_new()
{
queue_t *q = malloc(sizeof(queue_t));
if (q == NULL && (sizeof(queue_t) > 0)) {
return NULL;
} else { /* What if malloc returned NULL? */
q->head = NULL;
q->tail = NULL;
q->size = 0;
return q;
}
}
```
* 若是 `malloc` 失敗,回傳 `NULL`
* 若是 `malloc` 成功,初始化 `queue`
### q_free
```cpp
void q_free(queue_t *q)
{
if (q == NULL)
return;
else if (q->size == 1) {
q->size = 0;
free(q->head->value);
free(q->head);
free(q);
} else if (q->size == 0) {
free(q);
} else {
while (q->head != NULL) {
q->tail = q->head;
q->head = q->head->next;
free(q->tail->value);
free(q->tail);
}
q->size = 0;
free(q);
}
}
```
:::warning
考慮以下變更:
```diff
@@ -1,6 +1,5 @@
- while (q->head != NULL) {
+ for (;q->hea; q->head = q->head->next) {
q->tail = q->head;
- q->head = q->head->next;
free(q->tail->value);
free(q->tail);
- }
+ }
```
要點:
1. 用 `for` 迴圈改寫,讓程式碼更聚焦在 `q->tail` 的操作;
2. 日後可用 foreach 巨集改寫
:notes: jserv
:::
* 以 `queue` 中元素個數來做判定,分為三種方式解決
1. `queue` 沒有元素,直接 free(q)
2. `queue` 有一個元素,free `q->head` 的`value`及 `queue` 本身
3. `queue` 有多個元素,以 `while` 迴圈依次從 `queue->head` 開始 free,為了不浪費空間,將 `q->tail`當作指向 `q->head` 的指標
* 每一種方法都會將 `q->size` 歸零
### q_insert_head
```cpp
bool q_insert_head(queue_t *q, char *s)
{
list_ele_t *newh;
/* Solve: What should you do if the q is NULL? */
if (q == NULL) {
return false;
} else {
q->size++;
newh = malloc(sizeof(list_ele_t));
if (newh == NULL)
return false;
char *input;
size_t string_size;
string_size = strlen(s);
input = malloc(string_size + 1);
if (input == NULL) {
free(newh);
return false;
}
int i;
for (i = 0; s[i] != '\0'; i++) {
input[i] = s[i];
}
input[i] = '\0';
newh->value = input;
newh->next = q->head;
if (q->head == NULL && q->tail == NULL)
q->head = q->tail = newh;
else
q->head = newh;
return true;
}
}
```
1. 使用 `malloc` allocate一塊空間給 `q->value` ,為了避免使用**較不安全**的 `strcpy` ,複製 `string` 過程使用 `for` 迴圈完成
2. 若 `queue` 原先沒有元素,將 `q->head` && `q->tail` 都指向新增的元素:若 `queue` 原先已經有元素,只須將 `q->head` 指向新增的元素
### q_insert_tail
```cpp
bool q_insert_tail(queue_t *q, char *s)
{
list_ele_t *newt;
if (q == NULL) {
return false;
} else {
q->size++;
newt = malloc(sizeof(list_ele_t));
if (newt == NULL)
return false;
char *input;
size_t string_size;
string_size = strlen(s);
input = malloc(string_size + 1);
if (input == NULL) {
free(newt);
return false;
}
int i;
for (i = 0; s[i] != '\0'; i++) {
input[i] = s[i];
}
input[i] = '\0';
newt->value = input;
newt->next = NULL;
if (q->head == NULL && q->tail == NULL)
q->head = q->tail = newt;
else {
q->tail->next = newt;
q->tail = newt;
}
return true;
}
}
```
1. 操作基本上跟 `q_insert_head` 一樣,若 `queue` 原先沒有元素,將 `q->head` && `q->tail` 都指向新增的元素:若 `queue` 原先已經有元素,只須將 `q->tail` 指向新增的元素
2. 作業要求 `q_insert_tail` 時間複雜度為 $O(1)$,因有紀錄 `q->tail`,可以輕易做到
### q_remove_head
```cpp=
bool q_remove_head(queue_t *q, char *sp, size_t bufsize)
{
if(q == NULL)
return false;
else if (q->size == 0 || sp == NULL) {
return false;
} else {
q->size--;
size_t i;
char *tmp;
list_ele_t *link;
tmp = q->head->value;
for (i = 0; tmp[i] != '\0'; i++) {
sp[i] = tmp[i];
if (i == bufsize - 2) {
i++;
break;
}
}
sp[i] = '\0';
link = q->head;
q->head = q->head->next;
free(link->value);
free(link);
return true;
}
}
```
1. 因為要將 remove 的 `q->value` 移至 `*sp`且要注意不能超過 `bufsize`,因此在複製階段以 `for` 迴圈控制,若是超過 `bufsize` 會立刻離開迴圈
2. 在 `trace-07-perf` 中有對空的 `queue` 做 `q_remove_head` ,原先程式碼為:
```cpp
if (q == NULL || q->size == 0 || sp == NULL)
return false;
```
會造成 `q->size` 是NULL pointer狀態下被判斷,會造成 `segmentation fault`,因此修改為:
```cpp
if (q == NULL)
return false;
else if (q->size == 0 || sp == NULL)
return false;
```
若是 `queue` 尚未被初始化就執行 `q_remove_head`,會直接 `return`,避免錯誤
### q_size
```cpp=
int q_size(queue_t *q)
{
if (q == NULL || q->size == 0)
return false;
else
return q->size;
}
```
:::warning
考慮以下變更:
```cpp=
int q_size(queue_t *q)
{
return q ? q->size : 0;
}
```
簡潔又清晰,後續程式碼可比照此手法改寫。
:notes: jserv
:::
> 謝謝老師的建議,已經做修改[name=johnnykao530]
* 因為 `queue`型態就有紀錄元素個數 `q->size`,因此只須 `return q->size`,即可達成時間複雜度 $O(1)$ 的條件
### q_reverse
```cpp
{
if (q != NULL && q->size > 0) {
list_ele_t *prev = NULL, *curr, *next = NULL, *tmp;
tmp = curr = q->head;
while (curr != NULL) {
next = curr->next;
curr->next = prev;
prev = curr;
curr = next;
}
q->tail = tmp;
q->head = prev;
}
}
```
:::warning
1. 可將 `q != NULL` 改寫為 `q`
2. 縮減 `list_ele_t *next` 的 scope,可挪到 `while` 迴圈內
:notes: jserv
:::
> 謝謝老師的建議,已將 `q_reverse()` 的 `list_ele_t *next` 的 scope挪到 `while` 迴圈內,並將使用的 tmp nodes 從原先的4個降至2個[name=johnnykao530]
#### Revised 版本
```cpp
void q_reverse(queue_t *q)
{
if (q && q->size > 0) {
list_ele_t *curr, *next;
q->tail = curr = q->head;
while (curr != NULL) {
next = curr->next;
curr->next = q->head;
q->head = curr;
curr = next;
}
q->tail->next = NULL;
}
}
```
* 使用三個指標依序指向 `queue` 元素的前、中、後,從 `q->head` 開始逐漸更改 `next` 指向的位置,最後完成 `q_reverse` 操作
* 一開始每次操作 `q_reverse` 都用迴圈來回跑 `queue`的元素,造成在 `perf` 測試到 `q_reverse` 時都會發生 `TLE`,上網查資料後才用了此方法(太久沒撰寫 linked list 程式,都還給大學老師了...)
### q_sort
分為四個 `function` 完成:
1. q_sort()
```cpp
void q_sort(queue_t *q)
{
if (q == NULL || q->size <= 1)
return;
else {
q->head = mergeSortList(q->head);
list_ele_t *tmp = q->head;
while (tmp->next)
tmp = tmp->next;
q->tail = tmp;
}
}
```
:::warning
考慮以下變更:
```diff
@@ -2,11 +2,10 @@
{
if (q == NULL || q->size <= 1)
return;
- else {
- q->head = mergeSortList(q->head);
- list_ele_t *tmp = q->head;
- while (tmp->next)
- tmp = tmp->next;
- q->tail = tmp;
- }
+
+ q->head = mergeSortList(q->head);
+ list_ele_t *tmp;
+ for (tmp = q->head; tmp->next; tmp = tmp->next)
+ ; /* iterate */
+ q->tail = tmp;
}
```
要點:
1. 移除非必要的 `else`,降低程式縮排的深度 (depth);
2. 將 `while` 改為 `for`,之後引入 foreach 巨集時,更易銜接;
:notes: jserv
:::
> 謝謝老師,已做修正[name=johnnykao530]
#### Revised 版本
```cpp=
void q_sort(queue_t *q)
{
if (q == NULL || q->size <= 1)
return;
q->head = mergeSortList(q->head);
list_ele_t *tmp = q->head;
for(tmp = q->head; tmp->next; tmp=tmp->next)
;
q->tail = tmp;
}
```
* `q_sort` 的主函式,負責將 `q->head` 傳送給 `mergeSortList`,並回傳得排序好的 `q->head`,並使用 `while` 迴圈重新得出 `q->tail`
2. mergeSortList()
```cpp
list_ele_t *mergeSortList(list_ele_t *head)
{
// merge sort
if (!head || !head->next) {
return head;
}
list_ele_t *fast = head->next;
list_ele_t *slow = head;
// split list
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
fast = slow->next;
slow->next = NULL;
// sort each list
head = mergeSortList(head);
fast = mergeSortList(fast);
return merge(head, fast);
}
```
* 參閱 [Linked List Sort](https://npes87184.github.io/2015-09-12-linkedListSort/),完成時間複雜度為 $O(nlog{n})$ 的 `merge sort`
:::warning
`mergeSortList` 函式僅限於同一個 C 原始程式碼中使用,於是可在宣告加上 `static`,表明其 visibility 不公開 (exposed),有助於編譯器施加最佳化。
:notes: jserv
:::
3. merge()
```cpp
list_ele_t *merge(list_ele_t *l1, list_ele_t *l2)
{
// merge with pseudo node
list_ele_t *temp = NULL;
list_ele_t **p = &temp;
while (l1 && l2) {
if (natur_cmp(l1->value, l2->value) <= 0) {
*p = l1;
l1 = l1->next;
} else {
*p = l2;
l2 = l2->next;
}
p = &(*p)->next;
}
if (l1)
*p = l1;
if (l2)
*p = l2;
return temp;
}
```
* 若是使用 `recursive` 方法實做 `merge sort` ,在過大資料量時會發生 stack overflow 的狀況,因此在此使用 `iteration` 的方法實做
:::warning
需要解釋 stack overflow 的起因,以及到哪一個深度時候遇到,這部分的分析很重要!
:notes: jserv
:::
* 在 `compare` 時使用自製的 `natural sort`完成
4. natur_cmp()
```cpp
int natur_cmp(char *a, char *b)
{
if (strcasecmp(a, b) == 0)
return 0;
else {
int i = 0, dig_numa = 0, dig_numb = 0;
while (1) {
while (a[i] == b[i] && !isdigit(a[i]) && !isdigit(b[i]))
i++;
while (isdigit(a[i])) {
i++;
dig_numa++;
}
i -= dig_numa;
while (isdigit(b[i])) {
i++;
dig_numb++;
}
i -= dig_numb;
if (dig_numa > dig_numb)
return 1;
else if (dig_numb > dig_numa)
return -1;
else if (dig_numa == dig_numb) {
if (dig_numa == 0) {
return strcasecmp(a, b);
} else {
int j = dig_numa + i;
for (; i < j; i++) {
if (a[i] > b[i])
return 1;
else if (a[i] < b[i])
return -1;
else
continue;
}
if (strlen(a) > dig_numa) {
dig_numa = 0;
dig_numb = 0;
i++;
} else
return 0;
}
}
}
}
return false;
}
```
:::warning
上方程式碼過冗長,請思考縮減的方法
:notes: jserv
:::
* 原先使用 `strcasecmp()`來判斷 `q->value` 的大小(看到`qtest.c` 中的 `do_sort()`直接使用此函數),但之後發現要以 [natural sort](https://github.com/sourcefrog/natsort)方式完成,因此進行修改
* 自行編寫 `natural sort` 的判斷函式,並沒有使用 `library` 之 `natural sort`
* `trace-15-perf` && `trace-16-perf` 一直遇到 `TLE` ,在 trace code 後發現測試的程式碼大量 `insert` 相同的 `value` ,若是每個相同的 `value` 都要跑大量的 `loop` 以及 `branch` ,很容易造成 `TLE`的狀況,因此在執行`natur_cmp`時,優先判斷`value_a` && `value_b`的大小,若是相同直接回傳 *0* ,更改後也成功通過 `perf test`
* 作業要求要更改 `Simulation` 的判斷式,因此修改`qtest.c` 中的 `do_sort()`,將原先使用的 `strcasecmp()` 更改為 `natur_cmp()`
## Massif 的運用
## Dude, is my code constant time?
## select 系統呼叫