# 2021q1 Homework1 (lab0) contributed by < `D4nnyLee` > ## 實做 queue.[ch] 看過許多人的實做方法後,我在思考如何不在 `queue_t` 中新增 `tail` 變數也可以使 `q_insert_tail()` 的時間複雜度為 $O(1)$ 。 而我目前得到的答案為 [Circular Singly Linked List](https://www.javatpoint.com/circular-singly-linked-list) :::warning 很好,你已經發現 Linux 核心原始程式碼的設計考量了,對照看 Linux 程式碼來觀察其 circular linked list 的使用。 :notes: jserv ::: 以下用幾個圖例來畫出實際的結構: > 這邊利用 qtest 的指令來表達做了什麼動作 1. `new` ```graphviz digraph { head [shape = ellipse] dummy [shape = box] head -> dummy dummy -> dummy [label = " next"] } ``` 2. `ih abc` ```graphviz digraph { head [shape = ellipse] dummy [shape = box] abc [shape = box] head -> dummy { rank = same dummy -> abc [label = " next"] abc -> dummy [label = " next", constraint = false] } } ``` 3. `it def` ```graphviz digraph { head [shape = ellipse] dummy [shape = box] abc [shape = box] def [shape = box] head -> dummy { rank = same dummy -> abc [label = " next"] abc -> def [label = " next"] def -> dummy [label = " next"] } } ``` 但是原本的 qtest.c 的檢查機制不允許 linked list 有環,且我的實做方式有使用到 [Dummy Node](https://algorithm.yuanbin.me/zh-tw/basics_data_structure/linked_list.html#dummy-node) 的技巧,因此第一筆存進來的資料不是存在 `queue_t` 中的 `head` 節點,而是存在 `head->next` 節點。 因此我對 qtest.c 做了以下修改,讓檢查符合我的結構的邏輯。 > 可能需要改的地方不只這些,如果有發現其他需要改的歡迎在以下提出。 ```diff @@ -215,7 +215,7 @@ static bool do_insert_head(int argc, char *argv[]) bool rval = q_insert_head(q, inserts); if (rval) { qcnt++; - if (!q->head->value) { + if (!q->head->next->value) { report(1, "ERROR: Failed to save copy of string in list"); ok = false; } else if (r == 0 && inserts == q->head->value) { @@ -224,14 +224,14 @@ static bool do_insert_head(int argc, char *argv[]) "list element"); ok = false; break; - } else if (r == 1 && lasts == q->head->value) { + } else if (r == 1 && lasts == q->head->next->value) { report(1, "ERROR: Need to allocate separate string for each " "list element"); ok = false; break; } - lasts = q->head->value; + lasts = q->head->next->value; } else { fail_count++; if (fail_count < fail_limit) @@ -300,7 +300,7 @@ static bool do_insert_tail(int argc, char *argv[]) bool rval = q_insert_tail(q, inserts); if (rval) { qcnt++; - if (!q->head->value) { + if (!q->head->next->value) { report(1, "ERROR: Failed to save copy of string in list"); ok = false; } @@ -358,7 +358,7 @@ static bool do_remove_head(int argc, char *argv[]) if (!q) report(3, "Warning: Calling remove head on null queue"); - else if (!q->head) + else if (q->head->next == q->head) report(3, "Warning: Calling remove head on empty queue"); error_check(); @@ -424,7 +424,7 @@ static bool do_remove_head_quiet(int argc, char *argv[]) bool ok = true; if (!q) report(3, "Warning: Calling remove head on null queue"); - else if (!q->head) + else if (q->head->next == q->head) report(3, "Warning: Calling remove head on empty queue"); error_check(); @@ -558,7 +558,8 @@ bool do_sort(int argc, char *argv[]) bool ok = true; if (q) { - for (list_ele_t *e = q->head; e && --cnt; e = e->next) { + for (list_ele_t *e = q->head->next; e != q->head && --cnt; + e = e->next) { /* Ensure each element in ascending order */ /* FIXME: add an option to specify sorting order */ if (strcasecmp(e->value, e->next->value) > 0) { @@ -586,9 +587,9 @@ static bool show_queue(int vlevel) } report_noreturn(vlevel, "q = ["); - list_ele_t *e = q->head; + list_ele_t *e = q->head->next; if (exception_setup(true)) { - while (ok && e && cnt < qcnt) { + while (ok && e != q->head && cnt < qcnt) { if (cnt < big_queue_size) report_noreturn(vlevel, cnt == 0 ? "%s" : " %s", e->value); e = e->next; @@ -603,7 +604,7 @@ static bool show_queue(int vlevel) return false; } - if (!e) { + if (e == q->head) { if (cnt <= big_queue_size) report(vlevel, "]"); else ``` ### `queue_t` ```cpp /* Queue structure */ typedef struct { list_ele_t *head; /* Linked list of elements */ int size; } queue_t; ``` 就像前面提到的,我想要不新增 `tail` 這個變數來完成要求,所以這邊我只多新增了一個 `size` 代表 queue 中有多少筆資料。 > 不包含 Dummy Node ### `ele_new()` 為了讓建立一個新的節點時更便利,不用每次需要一個新的節點的時候都需要寫重複的程式碼。 而回傳值也設計的跟 `malloc()`, `q_new()` 一樣,當無法分配記憶體的時候就回傳 NULL 。 ```cpp /* Create a new node which stores the content of s. * Return NULL if could not allocate space. */ static list_ele_t *ele_new(char *s) { list_ele_t *node = malloc(sizeof(list_ele_t)); if (!node) return NULL; node->next = node; node->value = NULL; if (s) { size_t len = strlen(s) + 1; node->value = malloc(len); if (!node->value) { free(node); return NULL; } strncpy(node->value, s, len); } return node; } ``` ### `ele_free()` 釋放利用 `ele_new()` 分配出來的空間。 擷取自 [malloc(3) — Linux manual page](https://man7.org/linux/man-pages/man3/free.3.html) > The free() function frees the memory space pointed to by ptr, > which must have been returned by a previous call to malloc(), > calloc(), or realloc(). Otherwise, or if free(ptr) has already > been called before, undefined behavior occurs. If ptr is NULL, > no operation is performed. 可以看到 `free()` 即使傳入的值是 NULL 也不會發生錯誤,因此實作上就可以減少一些 `if` 的使用。 ```cpp static void ele_free(list_ele_t *node) { if (node) free(node->value); free(node); } ``` ### `q_new()` 事先利用 `malloc()` 和 `ele_new()` 來要空間,只要其中一個回傳 NULL (分配空間失敗),就回傳 NULL 。 且為了防止 Memory Leak ,不管是哪個函式失敗都會利用 `free()`, `ele_free()` 來嘗試釋放空間。 ```cpp /* * Create empty queue. * Return NULL if could not allocate space. */ queue_t *q_new() { queue_t *q = malloc(sizeof(queue_t)); list_ele_t *newh = ele_new(NULL); // dummy node /* TODO: What if malloc returned NULL? */ if (!q || !newh) { free(q); ele_free(newh); return NULL; } q->head = newh; q->size = 0; return q; } ``` ### `q_free()` 因為整個 linked list 包含了一個 Dummy Node 和 `q->size` 個節點,所以只要跑一個 `q->size + 1` 次的迴圈,每次都用 `ele_free()` 來釋放一個節點的空間就完成了。 ```cpp /* Free all storage used by queue */ void q_free(queue_t *q) { /* TODO: How about freeing the list elements and the strings? */ /* Free queue structure */ if (!q) return; list_ele_t *ele = q->head; for (int i = q->size; i >= 0; i--) { // loop for q->size + 1 times list_ele_t *tmp = ele; ele = ele->next; ele_free(tmp); } free(q); } ``` ### `q_insert_head()` 基本的 linked list 插入操作,時間複雜度為 $O(1)$。 1. 初始 ```graphviz digraph { node [shape = box] head [shape = ellipse] many [shape = none, label = "..."] newh head -> dummy { rank = same dummy -> node_1 -> many -> node_n -> dummy } } ``` 2. . ```graphviz digraph { node [shape = box] head [shape = ellipse] many [shape = none, label = "..."] newh -> node_1 [color = red] head -> dummy { rank = same dummy -> node_1 -> many -> node_n -> dummy } } ``` 3. . ```graphviz digraph { node [shape = box] head [shape = ellipse] many [shape = none, label = "..."] head -> dummy { rank = same dummy -> newh [color = red] newh -> node_1 -> many -> node_n -> dummy } } ``` ```cpp /* * Attempt to insert element at head of queue. * Return true if successful. * Return false if q is NULL or could not allocate space. * Argument s points to the string to be stored. * The function must explicitly allocate space and copy the string into it. */ bool q_insert_head(queue_t *q, char *s) { list_ele_t *newh; /* TODO: What should you do if the q is NULL? */ newh = ele_new(s); /* Don't forget to allocate space for the string and copy it */ /* What if either call to malloc returns NULL? */ if (!q || !newh) { ele_free(newh); return false; } newh->next = q->head->next; q->head->next = newh; ++q->size; return true; } ``` ### `q_insert_tail()` 先利用 `q_insert_head()` 在開頭新增一個新的節點,再將此節點與 Dummy Node 交換就可以達成常數時間的實作。 ```cpp /* * Attempt to insert element at tail of queue. * Return true if successful. * Return false if q is NULL or could not allocate space. * Argument s points to the string to be stored. * The function must explicitly allocate space and copy the string into it. */ bool q_insert_tail(queue_t *q, char *s) { /* Remember: It should operate in O(1) time */ if (!q_insert_head(q, s)) return false; q->head->value = q->head->next->value; q->head->next->value = NULL; q->head = q->head->next; return true; } ``` ### `q_size()` ```cpp /* * Return number of elements in queue. * Return 0 if q is NULL or empty */ int q_size(queue_t *q) { /* Remember: It should operate in O(1) time */ return q ? q->size : 0; } ``` ### `q_remove_head()` 因為 `q_size()` 在 Empty Queue 和 NULL Queue 的時候都會回傳 0 ,而在這兩個情況 `q_remove_head()` 要回傳 `false` ,所以就直接利用 `q_size()` 的回傳值來判斷是否回傳 `false` 。 ```cpp /* * Attempt to remove element from head of queue. * Return true if successful. * Return false if queue is NULL or empty. * If sp is non-NULL and an element is removed, copy the removed string to *sp * (up to a maximum of bufsize-1 characters, plus a null terminator.) * The space used by the list element and the string should be freed. */ bool q_remove_head(queue_t *q, char *sp, size_t bufsize) { if (!q_size(q)) return false; if (sp && bufsize) { strncpy(sp, q->head->next->value, bufsize - 1); sp[bufsize - 1] = '\0'; } list_ele_t *tmp = q->head->next; q->head->next = q->head->next->next; --q->size; ele_free(tmp); return true; } ``` ### `q_reverse()` 因為我實作的是 Circular Singly Linked List ,所以反轉就相當於把每一條邊指向前一個節點,且不用特別維護 `q->head` 的值,因為反轉前和反轉後的 `q->head` 應該是要一樣的。 ```cpp /* * Reverse elements in queue * No effect if q is NULL or empty * This function should not allocate or free any list elements * (e.g., by calling q_insert_head, q_insert_tail, or q_remove_head). * It should rearrange the existing ones. */ void q_reverse(queue_t *q) { list_ele_t *node, *next, *prev; if (!q) return; prev = q->head; node = prev->next; next = node->next; for (int i = q->size; i >= 0; i--) { // loop for q->size + 1 times node->next = prev; prev = node; node = next; next = node->next; } } ``` 以下用 `q->size == 3` 的例子一步一步視覺化執行過程: > `prev`: 黃色 > `node`: 紅色 > `next`: 藍色 > 更動到的邊: 紅色 1. 初始 ```graphviz digraph { node [shape = box] head [shape = ellipse] head -> dummy { rank = same dummy -> a -> b -> c -> dummy } } ``` 2. `i = 3` ```graphviz digraph { node [shape = box] head [shape = ellipse] head -> dummy dummy [color = yellow, style = filled] a [color = red, style = filled] b [color = blue, style = filled] { rank = same dummy -> a a -> b [color = white] b -> c c -> dummy a -> dummy [color = red] } } ``` 3. `i = 2` ```graphviz digraph { node [shape = box] head [shape = ellipse] head -> dummy a [color = yellow, style = filled] b [color = red, style = filled] c [color = blue, style = filled] { rank = same dummy -> a a -> b [color = white] b -> c [color = white] c -> dummy a -> dummy b -> a [color = red] } } ``` 4. `i = 1` ```graphviz digraph { node [shape = box] head [shape = ellipse] head -> dummy b [color = yellow, style = filled] c [color = red, style = filled] dummy [color = blue, style = filled] { rank = same dummy -> a a -> b [color = white] b -> c [color = white] a -> dummy b -> a c -> b [color = red] } } ``` 5. `i = 0` ```graphviz digraph { node [shape = box] head [shape = ellipse] head -> dummy c [color = yellow, style = filled] dummy [color = red, style = filled] a [color = blue, style = filled] { rank = same dummy -> a [color = white] a -> b [color = white] b -> c [color = white] a -> dummy b -> a c -> b dummy -> c [color = red, constraint = false] } } ``` 也等同於 ```graphviz digraph { node [shape = box] head [shape = ellipse] head -> dummy { rank = same dummy -> c -> b -> a -> dummy } } ``` ### `q_sort()` 我的作法為典型的 Merge Sort ,並且有使用到遞迴呼叫,所以我需要先寫另一個 `merge_sort()` 函式來幫助我做到遞迴呼叫。 ```cpp /* * Sort elements in range (beg, end) * Note: not in [beg, end) */ static void merge_sort(list_ele_t *beg, list_ele_t *end) { if (beg->next == end || beg->next->next == end) // length <= 1 return; list_ele_t *slow = beg->next, *left, *right, *left_end; for (list_ele_t *fast = slow; fast->next != end && fast->next->next != end; fast = fast->next->next) slow = slow->next; merge_sort(slow, end); left_end = right = slow->next; merge_sort(beg, right); left = beg->next; for (; left != left_end || right != end; beg = beg->next) { if (right == end || (left != left_end && strcasecmp(left->value, right->value) <= 0)) { beg->next = left; left = left->next; } else { beg->next = right; right = right->next; } } beg->next = end; } ``` 呼叫 `merge_sort(beg, end)` 代表是要排序 `(beg, end)` 區間的節點,這裡是使用左開右開區間(不同於常見的左閉右開),並且將排序完的節點與 `beg`, `end` 串接起來。 最後的 `for` 迴圈就兼顧了合併兩個小的已排序 list 與串接排序完節點與 `beg`, `end` 。