# 2020q3 Homework1 (lab0) contributed by <`Sisker1111`> ###### tags: `進階電腦系統理論與實作` [TOC] ## 簡介 ### 養成良好coding習慣 lab 中使用許多工具協助養成良好的寫程式習慣,包含: * Git pre-commit hook * [git-good-commit](https://github.com/tommarshall/git-good-commit): 學習撰寫良好的 Git commit message * [cppcheck](http://cppcheck.sourceforge.net/): 靜態程式碼分析工具 * [clang-format](https://clang.llvm.org/docs/ClangFormat.html): 確保一致的程式風格 * [GNU Aspell](http://aspell.net/): 拼字檢查 * [valgrind](https://valgrind.org/): 動態程式分析工具 * [AddressSanitizer](https://github.com/google/sanitizers/wiki/AddressSanitizer): 檢查記憶體的使用 等...... 詳細的環境設定以及lab的相關說明請參閱 [I01: lab0](https://hackmd.io/@sysprog/2020-lab0?fbclid=IwAR2YOjtMYzE_kB5vqvBUOSM9OlR5hgvz7NHUGg21NOcN9Hb8vccxEV-yC3o)、[C Programming Lab: Assessing Your C Programming Skills](https://www.cs.cmu.edu/afs/cs/academic/class/15213-s20/www/labs/cprogramminglab.pdf) ### :rocket: 改寫自 CMU 計算機概論的作業 [CMU](https://www.cmu.edu/) (位於美國賓州匹茲堡的卡內基美隆大學) 電腦科學系的 [Introduction to Computer Systems (ICS)](http://www.cs.cmu.edu/~213/index.html) 課程準備了 [C Programming Lab](https://www.cs.cmu.edu/afs/cs/academic/class/15213-s20/www/labs/cprogramminglab.pdf) 作為檢閱學生 C 語言程式設計的認知: * 記憶體管理; * 建立並管理需要透過指標操作的資料結構; * 字串操作; * 改善特定關鍵操作的效能; * 提高程式碼的穩固程度,例如能夠處理無效的參數,包含 NULL 指標; 在給定的程式碼中,[queue.h](https://github.com/sysprog21/lab0-c/blob/master/queue.h) 包含以下鏈結串列 ([linked list](https://en.wikipedia.org/wiki/Linked_list)) 結構定義: ```cpp /* Linked list element */ typedef struct ELE { char *value; struct ELE *next; } list_ele_t; ``` 接著用鏈結串列來實作佇列 ([queue](https://en.wikipedia.org/wiki/Queue_(abstract_data_type))) ```cpp /* Queue structure */ typedef struct { list_ele_t *head; /* Linked list of elements */ /* 其他自行定義的結構成員 */ } queue_t; ``` 這過程可用下圖來表示:佇列的上層建構於 `queue_t` 類型的物件。一開始,此資料結構僅包含單一成員 `head`,之後陸續以鏈結串列增添給定的字串,如 `"cab"`, `"bead"`, `"cab"` 等等。佇列內的每個元素由單向的鏈結串列構成,每個元素由 `list_ele_t` 類型的物件來表示,後者包含真正保存字串的 `value` 和指向下個鏈結串列元素開頭地址的 `next`。 ![圖 1](https://i.imgur.com/uZqLc9b.png) > 圖 1: 透過鏈結串列實作的佇列。每個鏈結串列元素都有個 `value` 指標,指向一段連續的記憶體 (即 C-style string,以 NULL 字元結尾的字串),字元依據 ASCII 編碼 (以十六進位顯示)。 C 語言中,字串以連續的`char` 型態物件的排列來表示,對多數的硬體來說,`char` 型態表示為 1 個 byte。,而為了儲存長度 `l` 的字串,需要至少 `l + 1` 個 `char` 元素,並在最後設定為 `\0` 字元。上圖的 `[cab, bead, cab]`,其中字元 `a` 和 `e` 以十六進位表示為 ASCII 編碼的 `61` 和 `65`。觀察上圖,字串 `"cab"` 和 `"bead"` 在執行時期可能對應到兩段不相鄰的記憶體。 在給定的 [C Programming Lab](http://www.cs.cmu.edu/~213/labs/cprogramminglab.pdf) 程式碼中,佇列透過 `queue_t *` 指標型態來操作,程式碼需要考慮以下二種特別狀況,作出對應的處置: 1. `NULL` 佇列:是指標值為 `NULL`; 2. 「空」(empty) 的佇列是指向有效結構,但開頭 (`head`) 的值為 `NULL`; :::info 另一種解讀方式: 若你有空的紅包袋,裡面的東西是 empty; 若你連紅包袋都沒有,就可以說 NULL; ::: [queue.c](https://github.com/sysprog21/lab0-c/blob/master/queue.c) 僅提供介面 ([interface](https://en.wikipedia.org/wiki/Interface-based_programming)) 但尚未有完整程式實作,需要由學員補完並逐步精進,包含以下: * `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`==: 以==遞增順序==來排序鏈結串列的元素。 ## 如何測試 測試所有的測資: > make test # 基本實作 ## Queue structure ``` c= typedef struct { list_ele_t *head; list_ele_t *tail; int size; } queue_t; ``` 除了原始的head之外,加入了 * tail: 指向 queue 的尾端,目的是為了 $O(1)$ 的 insert tail * size: 計算 queue 的大小,目的是為了 $O(1)$ 得到 queue size ## q_new ```c=1 queue_t *q_new() { queue_t *q = malloc(sizeof(queue_t)); if (!q) return NULL; q->tail = NULL; q->head = NULL; q->size = 0; return q; } ``` 建立一個新的 queue,如果 malloc 失敗,則return`NULL`,成功則 return 一個空的 queue. ## q_free ```c=1 void q_free(queue_t *q) { if (!q) return; if (q->head) { list_ele_t *tmp = q->head; while (q->head) { // free all element in link-list q->head = q->head->next; free(tmp->value); free(tmp); tmp = q->head; } } free(q); } ``` 必須先檢查 queue 是否為NULL,將現有的 queue 整個釋放掉,注意要先將 queue 上的每個 element 都釋放掉,最後才釋放 queue 本身. ## q_insert_head ```c=1 bool q_insert_head(queue_t *q, char *s) { if (!q) return false; list_ele_t *newh; /* TODO: What should you do if the q is NULL? */ newh = malloc(sizeof(list_ele_t)); if (!newh) // queue q is not exist || malloc return NULL return false; newh->value = malloc(sizeof(char) * (strlen(s) + 1)); if (!newh->value) { free(newh); return false; } strncpy(newh->value, s, strlen(s) + 1); if (!q->head) { q->tail = newh; } newh->next = q->head; q->head = newh; q->size++; return true; } ``` ### Code Description 1. 開頭一樣先判斷 queue 是否 為 `NULL`. 2. 7~9行 malloc 一個新點`newh`並檢查是否有 malloc 成功. 3. 11~15行 malloc 一個空間用來存放字串,同樣需檢查 malloc 是否成功,失敗需要將`newh`釋放. 4. 16行 將 input 字串指派給`newh->value` 5. 18~24行 將`q->head`指到`newh`,也將`q->tail`指到 queue 的最後一個 element . ## q_insert_tail ```c= bool q_insert_tail(queue_t *q, char *s) { if (!q) return false; list_ele_t *newh; newh = malloc(sizeof(list_ele_t)); if (!newh) // queue q is not exist || malloc return NULL return false; newh->value = malloc(sizeof(char) * (strlen(s) + 1)); if (!newh->value) { free(newh); return false; } strncpy(newh->value, s, strlen(s) + 1); if (!q->head) { newh->next = q->head; q->head = newh; q->tail = newh; q->size++; return true; } newh->next = NULL; q->tail->next = newh; q->tail = q->tail->next; q->size++; return true; } ``` ### Code Description 1. 開頭先判斷 queue 是否 為 `NULL`. 2. 16~22行 用`q->head`是否為`NULL`來判斷要字串`s`是否為 queue 的第一個字串. 3. 23~27行 把`q->tail->next`指到新點`newh`,並把`q->tail`指到`newh`. ## q_remove_head ```c= bool q_remove_head(queue_t *q, char *sp, size_t bufsize) { /* TODO: You need to fix up this code. */ /* TODO: Remove the above comment when you are about to implement. */ if (!q || !q->head) // queue q is not exist return false; if (sp) { if (bufsize > strlen(q->head->value)) { strncpy(sp, q->head->value, strlen(q->head->value)); sp[strlen(q->head->value)] = '\0'; } else { strncpy(sp, q->head->value, bufsize - 1); sp[bufsize - 1] = '\0'; } } list_ele_t *tmp = q->head; q->head = q->head->next; free(tmp->value); free(tmp); q->size--; return true; } ``` ### Code Description remove 一個 queue 中存在`head`的字串,並把此字串存入`sp` 1. 7~15行 用來限制可以存入`sp`的最大字串長度. 2. 17~22行 把`q->head`找到`q->head->next`並釋放原本`head`的記憶體. ## q_size ```c= int q_size(queue_t *q) { return q ? q->size : 0; } ``` 回傳 queue 的 size. ## q_reverse ```c= void q_reverse(queue_t *q) { if (!q) return; list_ele_t *cursor = NULL; list_ele_t **indirect = &q->head; q->tail = q->head; while (*indirect) { list_ele_t *next = (*indirect)->next; (*indirect)->next = cursor; cursor = *indirect; *indirect = next; } *indirect = cursor; } ``` 將 queue 中的 element 反向串接,記得要把`q->head` & `q->tail`指到新的位置,方法類似於 quiz1 的 reverse . ## q_sort 這題要自己從頭想真的有點難,最後還是參考網路上其他人的作法,參考網址如下: https://www.techiedelight.com/merge-sort-singly-linked-list/ ```c= list_ele_t *merge(list_ele_t *left_list, list_ele_t *right_list) { list_ele_t *tmp = NULL; if (strcmp(left_list->value, right_list->value) <= 0) { tmp = left_list; left_list = left_list->next; } else { tmp = right_list; right_list = right_list->next; } list_ele_t *head = tmp; while (left_list && right_list) { if (strcmp(left_list->value, right_list->value) <= 0) { tmp->next = left_list; tmp = tmp->next; left_list = left_list->next; } else { tmp->next = right_list; tmp = tmp->next; right_list = right_list->next; } } if (left_list) tmp->next = left_list; if (right_list) tmp->next = right_list; return head; } void merge_sort(list_ele_t **head) { if (!(*head) || !(*head)->next) return; list_ele_t *fast = (*head)->next; list_ele_t *slow = *head; while (fast && fast->next) { slow = slow->next; fast = fast->next->next; } fast = slow->next; slow->next = NULL; merge_sort(head); merge_sort(&fast); *head = merge(*head, fast); } void q_sort(queue_t *q) { if (!q) return; merge_sort(&q->head); if(!q->tail) return; while(q->tail->next) q->tail = q->tail->next; } ``` ### Code Description 1. 使用stcmp來做字串的比大小 2. 排序時也會順便將`q->head`指到正確的位置,排序後記得也要把`q->tail`指到正確的地方 # 測試與分析 ## 使用自動評分系統評分 ![](https://imgur.com/94py6lR.png) * trace-15-perf 我原本照著網址的做法直接做會跑不過,推測是因為memory過多或者遞迴太多次導致,後來簡化了一些寫法,以及把function合併後就對了. * trace-17-complexity 這個trace有時候會突然跑不過,目前還不確定是什麼原因. ## Valgrind 可使用指令: ``` $ valgrind --tool=massif ./qtest -f ./traces/trace-02-ops.cmd $ ms_print massif.out.PID $ massif-visualizer massif.out.PID ``` #### 分析`trace-02-ops.cmd` 執行結果: ```cmd> # Test of insert_head, insert_tail, and remove_head cmd> option fail 0 cmd> option malloc 0 cmd> new q = [] cmd> ih gerbil q = [gerbil] cmd> ih bear q = [bear gerbil] cmd> ih dolphin q = [dolphin bear gerbil] cmd> it meerkat q = [dolphin bear gerbil meerkat] cmd> it bear q = [dolphin bear gerbil meerkat bear] cmd> it gerbil q = [dolphin bear gerbil meerkat bear gerbil] cmd> rh dolphin Removed dolphin from queue q = [bear gerbil meerkat bear gerbil] cmd> rh bear Removed bear from queue q = [gerbil meerkat bear gerbil] cmd> rh gerbil Removed gerbil from queue q = [meerkat bear gerbil] cmd> rh meerkat Removed meerkat from queue q = [bear gerbil] cmd> rh bear Removed bear from queue q = [gerbil] cmd> rh gerbil Removed gerbil from queue q = [] Freeing queue ``` ![](https://imgur.com/jwhjN0N.png) #### 分析`trace-09-robust.cmd` 執行結果: ```cmd> # Test remove_head with NULL argument cmd> option fail 10 cmd> option malloc 0 cmd> new q = [] cmd> ih bear q = [bear] cmd> rhq Removed element from queue q = [] cmd> Freeing queue ``` ![](https://imgur.com/0Zs5tik.png) #### 分析`trace-17-complexoty.cmd` 執行結果: ```cmd> # Test if q_insert_tail and q_size is constant time complexity cmd> option simulation 1 cmd> it Probably constant time cmd> size Probably constant time cmd> option simulation 0 Freeing queue ``` ![](https://imgur.com/AZwwHfA.png)