--- tags: linux2022 --- # 2022q1 Homework1 (lab0) contributed by < `naihsin` > ## 實驗環境 ```shell $ gcc --version gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0 $ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Byte Order: Little Endian Address sizes: 39 bits physical, 48 bits virtual CPU(s): 4 On-line CPU(s) list: 0-3 Thread(s) per core: 2 Core(s) per socket: 2 Socket(s): 1 NUMA node(s): 1 Vendor ID: GenuineIntel CPU family: 6 Model: 61 Model name: Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz Stepping: 4 CPU MHz: 3099.851 CPU max MHz: 3100.0000 CPU min MHz: 500.0000 BogoMIPS: 5399.73 Virtualization: VT-x L1d cache: 64 KiB L1i cache: 64 KiB L2 cache: 512 KiB L3 cache: 3 MiB NUMA node0 CPU(s): 0-3 ``` ## 作業要求 依據上述指示著手修改 queue.[ch] 和連帶的檔案,測試後用 Git 管理各項修改,要能滿足 $ make test 自動評分系統的所有項目。 queue.c 僅提供介面 (interface) 但尚未有完整程式實作,需要由學員補完並逐步精進,包含以下: - q_new: 建立新的「空」佇列; - q_free: 釋放佇列所佔用的記憶體; - q_insert_head: 在佇列開頭 (head) 插入 (insert) 給定的新節點 (以 LIFO 準則); - q_insert_tail: 在佇列尾端 (tail) 插入 (insert) 給定的新節點 (以 FIFO 準則); - q_remove_head: 在佇列開頭 (head) 移去 (remove) 給定的節點; - q_release_element: 釋放特定節點的記憶體空間; - q_size: 計算目前佇列的節點總量; - q_delete_mid: 移走佇列中間的節點,詳見 LeetCode 2095. Delete the Middle Node of a Linked List - q_delete_dup: 在已經排序的狀況,移走佇列中具備重複內容的節點,詳見 LeetCode 82. Remove Duplicates from Sorted List II - q_swap: 交換佇列中鄰近的節點,詳見 LeetCode 24. Swap Nodes in Pairs - q_reverse: 以反向順序重新排列鏈結串列,該函式不該配置或釋放任何鏈結串列節點,換言之,它只能重排已經存在的節點; - q_sort: 以遞增順序來排序鏈結串列的節點,可參閱 Linked List Sort 得知實作手法; ## 開發紀錄 ### q_new - 先 malloc 一塊記憶體空間給 head - 若 malloc 失敗則 return NULL - 接著把 head 的兩個指標 next, prev 初始化,兩個指標皆指向自己 ```c struct list_head *q_new() { struct list_head *head = malloc(sizeof(struct list_head)); if (!head) return NULL; INIT_LIST_HEAD(head); return head; } ``` ### q_free - 判斷 queue 的 head(也就是 ```l``` )是否為空 - 若 ```l``` 為空,則代表「沒有」佇列 - 若 ```l``` 不為空,此時佇列有兩種狀態,分別是「空」佇列與「不是空」佇列(也就是佇列裡面有 element ) - 使用 ```list_for_each_entry_safe``` 走訪所有包含 list_head 結構的 element entry - 使用 ```q_release_element``` 把每一個 element free 掉 - 最後別忘了還要把 ```l``` free 掉 ```c void q_free(struct list_head *l) { if (!l) return; element_t *el, *safe; list_for_each_entry_safe (el, safe, l, list) { q_release_element(el); } free(l); } ``` ### q_insert_head - 判斷 head 是否為空(也就是判斷有沒有佇列) - 若沒有佇列則 return false - malloc ```element_t``` 大小給 ptr 指標 - 判斷 malloc 是否有成功 - 若 malloc 失敗則 return false - 使用 ```list_add``` 把 ptr 指向 list 成員的位址加入到 list head - 接著 malloc ```strlen(s) + 1``` 大小給 ptr->value ,包含空字元 - 判斷 malloc 是否成功 - 把 ```s``` 複製到新的 element 中 ```c bool q_insert_head(struct list_head *head, char *s) { if (!head) return false; element_t *ptr = (element_t *) malloc(sizeof(element_t)); if (!ptr) return false; list_add(&ptr->list, head); ptr->value = (char *) malloc(strlen(s) + 1); if (!ptr->value) return false; strncpy(ptr->value, s, strlen(s) + 1); return true; } ``` ### q_insert_tail - 判斷 head 是否為空(也就是判斷有沒有佇列) - 若沒有佇列則 return false - malloc ```element_t``` 大小給 ptr 指標 - 判斷 malloc 是否有成功 - 若 malloc 失敗則 return false - 使用 ```list_add``` 把 ptr 指向 list 成員的位址加入到 list tail - 接著 malloc ```strlen(s) + 1``` 大小給 ptr->value ,包含空字元 - 判斷 malloc 是否成功 - 把 ```s``` 複製到新的 element 中 ```c bool q_insert_tail(struct list_head *head, char *s) { if (!head) return false; element_t *ptr = (element_t *) malloc(sizeof(element_t)); if (!ptr) { return false; } list_add_tail(&ptr->list, head); ptr->value = (char *) malloc(strlen(s) + 1); if (!ptr->value) return false; strncpy(ptr->value, s, strlen(s) + 1); return true; } ``` ### q_remove_head - 判斷 head 是否為空(也就是判斷有沒有佇列) - 若沒有佇列則 return NULL - 判斷 list 是否為空 - 若為空則 return NULL - 用 ```list_entry``` 找到從 head 數來的第一個 element entry 的位址 - 判斷 sp 字串是否為空 - 若為空,把 element 的字串複製到 sp 中 - 記得要把 sp padding 一個空字元 - 用 ```list_del_init``` 把剛剛找到的 element 的 list 成員 從 list 中移除 - 把找到的 element return 回去 ```c element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize) { if (!head || list_empty(head)) return NULL; element_t *ptr = list_entry(head->next, element_t, list); if (sp) { strncpy(sp, ptr->value, bufsize); sp[bufsize - 1] = '\0'; } list_del_init(&ptr->list); return ptr; } ``` ### q_remove_tail - 判斷 head 是否為空(也就是判斷有沒有佇列) - 若沒有佇列則 return NULL - 判斷 list 是否為空 - 若為空則 return NULL - 用 ```list_entry``` 找到從 tail 數來的第一個 element entry 的位址 - 判斷 sp 字串是否為空 - 若為空,把 element 的字串複製到 sp 中 - 記得要把 sp padding 一個空字元 - 用 ```list_del_init``` 把剛剛找到的 element 的 list 成員 從 list 中移除 - 把找到的 element return 回去 ```c element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize) { if (!head || list_empty(head)) return NULL; element_t *ptr = list_entry(head->prev, element_t, list); if (sp) { strncpy(sp, ptr->value, bufsize); sp[bufsize - 1] = '\0'; } list_del_init(&ptr->list); return ptr; } ``` ### q_release_element - 在 ```q_insert_head```, ```q_insert_tail``` 中,分別 malloc 整個 ```element``` 結構與 ```element->value``` ,使用到兩次 malloc ,在釋放 element 也必須把這兩個結構 free 掉 ```c void q_release_element(element_t *e) { free(e->value); free(e); } ``` ### q_size - 判斷 head 是否為空(也就是判斷有沒有佇列) - 若沒有佇列則 return 0 - 用 ```list_for_each``` 走訪 list 所有的節點 - ```list_for_each``` 是一個 macro ,展開為 ```c #define list_for_each(node, head) \ for (node = (head)->next; node != (head); node = node->next) ``` - 設置 len 變數,算出總共有幾個節點 - return len 回去 ```c int q_size(struct list_head *head) { if (!head) return 0; int len = 0; struct list_head *li; list_for_each (li, head) len++; return len; } ``` ### q_delete_mid - 判斷 head 是否為空(也就是判斷有沒有佇列) - 若沒有佇列則 return false - 判斷 queue 是否為空 - 若 queue 為空則 return false - 接著設置快慢指針,找到中間的節點,快指針一次走兩步,慢指針一次走一步,當快指針到達終點時,慢指針才走完一半,所以可以利用這種特性來找到中間的節點 - 使用 ```list_del_init``` 把慢指針從 list 中移除 - 用 ```list_entry``` 找到慢指針被存放到 element 結構的位址 - 用 ```q_release_element``` 把找到的 element release ```c bool q_delete_mid(struct list_head *head) { // https://leetcode.com/problems/delete-the-middle-node-of-a-linked-list/ if (!head || !q_size(head)) return false; struct list_head *slow = head->next, *fast = head->next; while (fast->next != head->next && fast->next->next != head->next) { slow = slow->next; fast = fast->next->next; } list_del_init(slow); q_release_element(list_entry(slow, element_t, list)); return true; } ``` ### q_delete_dup - 判斷 queue 是否為空 - 若為空則 return false - 設置 cur 指針來走訪每個節點 - 當 cur->next == head 則代表已經走訪完一圈 - 設置 first, second 來紀錄相鄰兩個節點所處的 element 結構的位址 - 當 first->value 跟 second->value 的值相等,則把 second 移除,且把 cur->next 從 list 中移除 - 當 first->value 跟 second->value 的值不相等,把 cur 設置成 cur->next ,繼續下一對相鄰節點比較 ```c bool q_delete_dup(struct list_head *head) { // https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/ if (!q_size(head)) return false; struct list_head *cur = head->next; while (cur->next != head) { element_t *first = list_entry(cur, element_t, list); element_t *second = list_entry(cur->next, element_t, list); if (!strcmp(first->value, second->value)) { list_del_init(cur->next); q_release_element(second); } else { cur = cur->next; } } return true; } ``` ### q_swap - 設置 left, right 紀錄相鄰的兩個節點 - 當 left == head || right == head 則代表已經走訪完一圈的所有節點 - 使用 ```list_move``` 把 left 節點從 list 中移除,且再把 left 加入到 right 在 list 中的下一個位址,如此一來就可以達成 swap 兩個節點的操作 - 更新 left = left->next, right = left->next,繼續下一輪的 swap 操作 ```c void q_swap(struct list_head *head) { // https://leetcode.com/problems/swap-nodes-in-pairs/ struct list_head *left = head->next, *right = left->next; while (left != head && right != head) { list_move(left, right); left = left->next; right = left->next; } } ``` ### q_reverse - 判斷 head 是否為空(也就是判斷有沒有佇列) - 若沒有佇列則 return - 使用 ```list_for_each_safe``` 走訪 list 中的所有節點 - 用 ```list_move``` 刪除節點 ```c void q_reverse(struct list_head *head) { if (!head) return; struct list_head *li, *safe; list_for_each_safe (li, safe, head) { list_move(li, head); } } ``` :::warning 若為 singular,不需要繼續處理。 :notes: jserv ::: ### q_sort - 這邊我選擇用 divide and conquer 的方式來實做程式 - 判斷 head 是否為空(也就是判斷有沒有佇列) - 若沒有佇列則 return - 判斷 list 是否為空 - 若為空則 return - 先把 head->prev->next 設成 NULL ,讓最後一個節點的 next 指向 NULL,方便設置後續 ```q_merge``` 的結束條件 - 呼叫 ```q_merge``` 來切割節點直到剩下一個節點 - 接著 ```q_merge``` 會回傳已經排序好的節點 - 此時的 queue 並不是完整的 doubly linked-list - while 是找到回傳 list 的最後一個節點 - 復原 head 與 最後一個節點的關係 ```c void q_sort(struct list_head *head) { if (!head || list_empty(head)) return; head->prev->next = NULL; struct list_head *ptr = q_merge(head->next); head->next = ptr; ptr->prev = head; while (ptr->next) { ptr = ptr->next; } ptr->next = head; head->prev = ptr; } ``` ### q_merge - 判斷 ptr->next 是否為空 - 若為空則代表已經切割至剩下一個節點,return ptr - 使用快慢指針來找到中間節點,詳細說明可參考 ```q_delete_mid``` - 從中間節點切割左右兩邊的 list - 使用 recursive ,再次把左邊的 list 切割,直到切割到剩下一個節點 - 使用 recursive ,再次把右邊的 list 切割,直到切割到剩下一個節點 - 使用 ```q_mergefinal``` 把左邊與右邊的 list 排序合併 ```c struct list_head *q_merge(struct list_head *ptr) { if (!ptr->next) return ptr; struct list_head *slow = ptr, *fast = ptr; while (fast->next && fast->next->next) { slow = slow->next; fast = fast->next->next; } struct list_head *left, *right; fast = slow->next; slow->next = NULL; left = q_merge(ptr); right = q_merge(fast); return q_mergefinal(left, right); } ``` ### q_mergefinal - 這邊使用到 ```pointer to pointer``` 目的是為了減少記憶體的分配 - 設置 head 指標紀錄排序好的第一個節點, head 先指向 left (也就是左邊節點) - 設置指標的指標 ```**ptr``` ,讓 ptr 紀錄 head 的記憶體位址,以便根據要修改當前節點的記憶體位址來變更 ptr - 設置 prev 指標紀錄前一個節點, 以便讓下一個節點的 prev 指回前一個節點 - 設置指標的指標 ```**node``` ,讓 node 紀錄即將要加入 list 的節點的記憶體位址,一開始設成 NULL - 使用 for 迴圈直到 left 跟 right 皆不為 NULL - 使用 ```list_entry``` 找到 left 跟 right 所在的 element 結構的記憶體位址 - 比較 el->value (左節點)跟 el2->value (右節點)的 element value - 若 el->value (左節點)比較小,則把 node 設成 ```&left``` 也就是 left 節點所處的記憶體位址 - 反之,則把 node 設成 ```&right``` 也就是 right 節點所處的記憶體位址 - 接著把 ```*node->prev = prev``` 也就是把 ```left->prev = prev``` 或 ```right->prev = prev``` ,把 node 與前一個節點的關係接上 - 更新 prev ,把 prev 設成當前節點 *node - 更新 *ptr ,把當前要修改的記憶體位址的節點設成 *node - 前幾個步驟,已經順利把節點接上,接著要更新 ptr ,把紀錄當前節點的記憶體位址移到下一個節點 - 更新 *node ,把當前指向 left 或 right 的節點更新為 left->next 或 right->next , 等價於 ```left = left->next``` ```c struct list_head *q_mergefinal(struct list_head *left, struct list_head *right) { struct list_head *head = left, *prev = NULL, **ptr = &head, **node; for (node = NULL; left && right; *node = (*node)->next) { element_t *el = list_entry(left, element_t, list); element_t *el2 = list_entry(right, element_t, list); node = (strcmp(el->value, el2->value) < 0) ? &left : &right; (*node)->prev = prev; prev = *node; *ptr = *node; ptr = &(*ptr)->next; } *ptr = left ? left : right; (*ptr)->prev = prev; return head; } ```