# 2021q1 Homework1 (lab0) contributed by < `mahavishnuj` > :::danger 注意書寫規範:中英文間用一個半形空白字元區隔 :notes: jserv ::: ## 實驗環境 這次的實驗由於必須使用 Ubuntu Linux 藉此測試針對Mac系統所出的`Multipass` 使用類似Docker的技術可以在 `MacOS` 中達到較為輕量化的 `VM` ```shell $ uname -a Linux moral-gator 5.4.0-66-generic #74-Ubuntu SMP Wed Jan 27 22:54:38 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux $ gcc --version gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0 ``` :::danger multipass 使用 macOS 的 [Hypervisor.framework](https://developer.apple.com/documentation/hypervisor) 或 [VirtualBox](https://www.virtualbox.org/),後兩者跟 Docker 行為落差較大,請注意用語。 :notes: jserv ::: ## 實驗過程與心得 ### q_size() 在跟著指示在 `queue` 這個結構裡面加入 `int size` 透過這個方式能夠讓我們能夠更了解 `queue` 裡頭的各種資訊,除此之外更加入了另一個 `Pointer` `*tail` 以便於之後從尾端插入資料能在 **$O(1)$** 的時間內達成。相較於沒有 `*tail` 的狀況需要從 `head` 依序慢慢的找節省了不少時間 ```c= typedef struct { list_ele_t *head; /* Linked list of elements */ list_ele_t *tail; int size; } queue_t; ``` 我的方法是先判斷 `q` 是否為空,不是內容為空是根本沒有新增 `queue` 或是內完全不含任何數值都是回傳 `0` 若是內容有物則直接回傳 `q` 內 `size` 的內容 ```c= int q_size(queue_t *q) { if (!q || !q->head ) { return 0; } return q->size; } ``` ### q_new() 再來實作新增 `queue` 這個程式,其實在程式碼中的 `TODO` 就有貼心小提醒,所以在一開始除了用 `malloc` 宣告出一個 `queue` 之餘,還在後面檢查是否有成功宣告並配置記憶體,若是失敗則回傳 `NULL` 值 ```c= queue_t *q_new() { queue_t *q = malloc(sizeof(queue_t)); if (!q) { return NULL; } q->head = NULL; q->tail = NULL; q->size = 0; return q; } ``` ### q_insert_head 透過取的 `head` 指標的位置,作為起始位置並將插入新的元素放在左端,除了 `q` 本身若是不存在的話或是節點宣告失敗必須回傳`NULL` 需要注意若是在宣告 `value` 時失敗需要將 `newh` 釋放出來,來避免造成 `Memory Leak` 的狀況。 ```c= bool q_insert_head(queue_t *q, char *s) { if (q == NULL) { return false; } list_ele_t *newh = malloc(sizeof(list_ele_t)); if (newh == NULL) { return false; } newh->value = malloc(sizeof(char) * (strlen(s) + 1)); /*allocate more than 1 size of string length*/ if (newh->value == NULL) { free(newh); return false; } strncpy(newh->value, s, strlen(s) + 1); newh->next = q->head; if (q->size == 0) { q->tail = newh; } q->head = newh; q->size += 1; return true; } ``` ### Insert_tail 原則上宣告新節點以及檢查記憶體的方式跟 `head` 一樣,在 `Requirement` 裡面有提到需要達到 ***O(1)*** 的時間複雜度,直觀的方法就是從 `head` 慢慢依序往下找直到最後一個 `Node` 後面接上新的點,但這樣的做法曠時廢日,透過這樣子多一個指標指向 `tail` 的方式可以大大縮短時間,需要注意的時若是佇列裡面是空的,添加完需要把 `head` 與 `tail` 都指向同一個點。 ```c= bool q_insert_tail(queue_t *q, char *s) { if (!q) { return false; } list_ele_t *newh = malloc(sizeof(list_ele_t)); if (!newh) { free(newh); return false; } newh->value = malloc(sizeof(char) * (strlen(s) + 1)); strncpy(newh->value, s, strlen(s) + 1); newh->next = NULL; if (!q->tail) { q->tail = newh; q->head = newh; } else { q->tail->next = newh; q->tail = newh; } q->size++; return true; } ``` ### q_free() `free` 的過程其實挺直白的,透過 `head` 的指標慢慢往下找到下一個點,值得一提的部分是在 `free` 動作之前應該先把 `head` 移動到下一個點,避免在不小心 `free` 之後遺失 `head` 的位置,還有必須先 `free` `value` 這個 `array` 空間之後在處理整個資料結構。 ```c= void q_free(queue_t *q) { if (!q) { return; } list_ele_t *tmp = q->head; while (q->head) { q->head = q->head->next; free(tmp->value); free(tmp); tmp = q->head; } free(q); } ``` ### q_remove_head 在移出 `head` 的同時還需要回傳 `head` 節點所含的內容,除了檢查佇列是否為空之外,還要檢查是否為空佇列。之後在把需要移除的節點 `address` 存於 `tmp` 當中,並將 `head` 先移動到下一個點上面,需要注意的部分是需要在 `sp` 最後加上一個 `\0`,在 `free` 的部分也記得要把 `value` 一起釋放。 ```c= bool q_remove_head(queue_t *q, char *sp, size_t bufsize) { if (!q || !q->head) { return false; } list_ele_t *tmp = q->head; q->head = tmp->next; if (sp) { strncpy(sp, tmp->value, bufsize - 1); sp[bufsize - 1] = '\0'; } free(tmp->value); free(tmp); q->size--; return true; } ``` ### q_reverse 看到教材有分享利用`Pointer to Pointer`做 `Indirection` 來操作指標內容,但在實做上面還是先用三個指標的方式來操作。 ```c= void q_reverse(queue_t *q) { if (!q) { return; } if (q->size < 2) { return; } list_ele_t *cur = q->head; list_ele_t *nxt = cur; list_ele_t *prev = NULL; q->tail = q->head; while (nxt) { nxt = cur->next; cur->next = prev; prev = cur; cur = nxt; } q->head = prev; } ``` ```graphviz digraph graphname{ node[shape=box] head[shape=plaintext,fontcolor=red] cur[shape=plaintext,fontcolor=blue] prev[shape=plaintext,fontcolor=red] nxt[shape=plaintext,fontcolor=purple] rankdir=LR { rankdir = LR A[label=5] B[label=6] C[label=8] D[label=1] NULL1[label=NULL,shape=plaintext] NULL2[label=NULL,shape=plaintext] } { rank="same"; head->A cur->A; nxt->A } A->B->C->D->NULL1 prev->NULL2 } ``` - 一開始先將 `prev` 指標指向 `NULL` , `head`, `cur`, 與 `nxt` 指向最一開始的節點。 進入 `while loop` 之後先將 `nxt` 指向下一個節點, `cur` 指向 `prev` 所指的點,之後再分別將 `prev` 與 `cur` 往前移 ```graphviz digraph graphname{ node[shape=box] head[shape=plaintext,fontcolor=red] cur[shape=plaintext,fontcolor=blue] prev[shape=plaintext,fontcolor=red] nxt[shape=plaintext,fontcolor=purple] tail[shape=plaintext,fontcolor=orange] rankdir=LR { rankdir = LR A[label=5] B[label=6] C[label=8] D[label=1] NULL1[label=NULL,shape=plaintext] NULL2[label=NULL,shape=plaintext] } { rank="same"; tail->A head->A cur->A; nxt->B } B->C->D->NULL1 prev->A->NULL2 } ``` - 執行兩次後的結果,而 `head` 的部分直到執行到最後才會做修改, `tail` 的部分則是一開始便改變 ```graphviz digraph graphname{ node[shape=box] head[shape=plaintext,fontcolor=red] cur[shape=plaintext,fontcolor=blue] prev[shape=plaintext,fontcolor=red] nxt[shape=plaintext,fontcolor=purple] rankdir=LR { rankdir = LR A[label=5] B[label=6] C[label=8] D[label=1] NULL1[label=NULL,shape=plaintext] NULL2[label=NULL,shape=plaintext] } { rank="same"; prev->A nxt->C head->A cur->C } B->A->NULL2 C->D->NULL1 } ``` ### q_sort 在實作排序的部分,參考了很多同學還有網路上的相關資料,先用一個 `function` 將 `linked-list` 分成兩個子字串。 ```c= list_ele_t *split_list(list_ele_t *head) { if (!head || !head->next) { return head; } list_ele_t *rabt = head->next; list_ele_t *turt = head; while (rabt && rabt->next) { turt = turt->next; rabt = rabt->next->next; } rabt = turt->next; turt->next = NULL; list_ele_t *l1 = split_list(head); list_ele_t *l2 = split_list(rabt); return merge_sort(l1, l2); } ``` - 利用兩個指標,以偵查 cycle 所使用的龜兔賽跑概念,指標 `rabt` 移動距離是 `turt` 的兩倍 ```graphviz digraph graphname{ node[shape=box] head[shape=plaintext,fontcolor=red] turt[shape=plaintext,fontcolor=red] rabt[shape=plaintext,fontcolor=blue] rankdir=LR { rankdir = LR A[label=5] B[label=6] C[label=8] D[label=1] NULL1[label=NULL,shape=plaintext] } { rank="same"; head->A turt->A } A->B->C->D->NULL1 rabt->B } ``` - 執行完後 `rabt` 與 `turt` 會分別在不同的點,並且將其斷開成兩截 ```graphviz digraph graphname{ node[shape=box] head[shape=plaintext,fontcolor=red] rabt[shape=plaintext,fontcolor=blue] turt[shape=plaintext,fontcolor=red] rankdir=LR { rankdir = LR A[label=5] B[label=6] C[label=8] D[label=1] NULL1[label=NULL,shape=plaintext] } { rank="same"; head->A } A->B->C->D->NULL1 turt->C rabt->NULL1 } ``` - 斷成兩截後分別回傳兩段 `linked-list` 的 `head` 指標 ```graphviz digraph graphname{ node[shape=box] head[shape=plaintext,fontcolor=red] rabt[shape=plaintext,fontcolor=blue] rankdir=LR { rankdir = LR A[label=5] B[label=6] C[label=8] D[label=1] NULL1[label=NULL,shape=plaintext] NULL2[label=NULL,shape=plaintext] } { rank="same"; head->A rabt->C } A->B->NULL1 C->D->NULL2 } ``` - 然後再依照其 `value` 的大小去做排序,使用跟 `QuickSort` 一樣 `Divided and Conquer` 的策略進行 ```c= list_ele_t *merge_sort(list_ele_t *l1, list_ele_t *l2) { list_ele_t *tmp, *q; if (strcmp(l1->value, l2->value) < 0) { tmp = l1; l1 = l1->next; } else { tmp = l2; l2 = l2->next; } q = tmp; while (l1 && l2) { if (strcmp(l1->value, l2->value) < 0) { tmp->next = l1; tmp = tmp->next; l1 = l1->next; } else { tmp->next = l2; tmp = tmp->next; l2 = l2->next; } } if (l1) { tmp->next = l1; } if (l2) { tmp->next = l2; } return q; } ```