# 2023q1 Homework1 (lab0) contributed by < [tintinjian12999](https://github.com/tintinjian12999) > ## 實驗環境 ```shell $ gcc --version gcc (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0 $ ls cpu 架構: x86_64 CPU 作業模式: 32-bit, 64-bit Byte Order: Little Endian Address sizes: 43 bits physical, 48 bits virtual CPU(s): 8 On-line CPU(s) list: 0-7 每核心執行緒數: 2 每通訊端核心數: 4 Socket(s): 1 NUMA 節點: 1 供應商識別號: AuthenticAMD CPU 家族: 23 型號: 24 Model name: AMD Ryzen 7 3750H with Radeon Vega Mobile Gfx 製程: 1 Frequency boost: enabled CPU MHz: 1700.000 CPU max MHz: 2300.0000 CPU min MHz: 1400.0000 BogoMIPS: 4591.40 虛擬: AMD-V L1d 快取: 128 KiB L1i 快取: 256 KiB L2 快取: 2 MiB L3 快取: 4 MiB NUMA node0 CPU(s): 0-7 ``` ## Reviewed by `WeiHongWi` * 若 q_delete_mid , q_delete_dup 能夠畫圖來視覺化你的想法,會更加直觀 * 缺少利用 Valgrind 來評估程式效能 * 每個 function 的描述我認為非常詳盡,但能逐步紀錄開發和紀錄自己的想法,而不是一次寫完再解說我認為可以幫助提昇自己的思考模式 # 作業要求 詳細閱讀 [C Programming Lab](http://www.cs.cmu.edu/~213/labs/cprogramminglab.pdf) ,依據指示著手修改 queue.[ch] 和連帶的檔案,測試後用 Git 管理各項修改,記得也該實作 q_sort 函式。 * q_new: 創一個空的 queue * q_free: 釋放掉一個 queue * q_insert_head: 在 head 插入一個 element (LIFO) * q_insert_tail: 在 tail 插入一個 element (FIFO), O(1) * q_remove_head: 把 head 的 element 移除 * q_size: return佇列的大小 * q_reverse: 將佇列反轉,不配置或釋放任何的 element * q_sort: 將佇列由小排到大, sort by nature sort # 開發過程 ## q_new :::danger 注意作業書寫規範:中英文間用一個半形空白字元區隔。 程式碼縮排亦有相關規範,請依循! :notes: jserv ::: 安排新的記憶體空間給新的佇列(命名為 head ),且 `queue.h` 裡提到初始化 prev 與 next 兩個指標指向自身,且如果記憶體配置失敗則回傳 NULL 。 ```c struct list_head *q_new() { struct list_head *head = malloc(sizeof(struct list_head)); if(head == NULL) return NULL; head->previous = head; head->next = head; return head; } ``` > 後來發現 list.h 裡有 `INIT_LIST_HEAD` 可用,程式碼調整如下: ```c struct list_head *q_new() { struct list_head *head = malloc(sizeof(struct list_head)); if (head == NULL) return NULL; INIT_LIST_HEAD(head); return head; } ``` :::warning 1. `sizeof(struct list_head)` 可改為 `sizeof(*head)` 2. `if (head == NULL)` 可改為 `if (!head)` :notes: jserv ::: ## q_free 清除掉所有的佇列空間,根據`queue.h`敘述,如果 header 是 NULL ,則不須做任何操作,根據[list_for_each_entry_safe](https://biscuitos.github.io/blog/LIST_list_for_each_entry_safe/),在<s>遍歷</s> --- linked list 時,不能使用 `list_for_each_entry()`,該者會導致在刪除 node 時將指標指向 undefined state ,因此在此用 `list_for_each_entry_safe()` ,利用 safe 指標做緩存避免問題發生。 :::danger 注意 [資訊科技詞彙翻譯](https://hackmd.io/@sysprog/it-vocabulary)。 :notes: jserv ::: 在此題用 `list_for_each_entry_safe()` 走訪 linked list 並利用 `queue.h` 裡的 q_relaease_element 將 node 裡的 value 和指向該 node 的指標釋放,最後將作為 list 開頭的指標 l 釋放。 ```c void q_free(struct list_head *l) { if (l == NULL) return; element_t *entry, *safe; list_for_each_entry_safe (entry, safe, l, list) q_release_element(entry); free(l); } ``` ## q_insert_head 新增一個 new_element 作為新增節點的指標,配置一個空間存放該指標,我們知道 element 裡有 value 和 list 兩個部份,接著計算讀取進來的字串長度並加 1 (要放 `'\0'`),宣告一段長度為上述長度的空間給 element 給 value 使用,利用`strncpy`將讀取到的字串複製進 value 裡,最後用`q_insert_head`將新增的 node 加進整個 linked list 的前端。(以上配置若失敗則回傳 false 並釋放多餘的記憶體) ```c bool q_insert_head(struct list_head *head, char *s) { if (head == NULL) return false; element_t *new_element = malloc(sizeof(element_t)); if (new_element == NULL) return false; int str_len = strlen(s); new_element->value = malloc(sizeof(char) * (str_len + 1)); if (new_element->value == NULL) { free(new_element); return false; } strncpy(new_element->value, s, str_len); new_element->value[str_len] = '\0'; list_add(&new_element->list, head); return true; } ``` :::warning 撰寫更精簡的程式碼。 :notes: jserv ::: ## q_insert_tail 基本上概念一樣,只是將新的節點利用 `list_add_tail` 加入鏈結串列的最尾端。 ```c bool q_insert_tail(struct list_head *head, char *s) { if (head == NULL) return false; element_t *new_element = malloc(sizeof(element_t)); if (new_element == NULL) return false; int str_len = strlen(s); new_element->value = malloc(sizeof(char) * (str_len + 1)); if (new_element->value == NULL) { free(new_element); return false; } strncpy(new_element->value, s, str_len); new_element->value[str_len] = '\0'; list_add_tail(&new_element->list, head); return true; } ``` ## q_remove_head 傳進來的 header 裡的 next 指向的就是 linked list 的第一個 node,接著宣告一個 temp 指標儲存被 remove 的 node 裡的數值,利用`list_del()`移除第一個 node,將裡面的 value 複製到 sp 上且要加上`\0`才能作為新的字串,最後回傳被移除 node 的指標(也就是 temp ),當然題目提到要判斷 header 與 sp 是否為 NULL 也要考慮進去。 ```c element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize) { if (head == NULL || list_empty(head)) return NULL; element_t *temp = list_entry(head->next, element_t, list); list_del(head->next); if (sp != NULL && bufsize != 0) { strncpy(sp, temp->value, bufsize - 1); sp[bufsize - 1] = '\0'; } return temp; } ``` ## q_remove_tail 與上一題十分的相似,不同的地方在最後一個 node 是 header 裡的 prev 指向的位置(因為這裡使用的是環狀 linked list )。 ```c element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize) { if (head == NULL || list_empty(head)) return NULL; element_t *temp = list_entry(head->prev, element_t, list); list_del(head->prev); if (sp != NULL && bufsize != 0) { strncpy(sp, temp->value, bufsize - 1); sp[bufsize - 1] = '\0'; } return temp; } ``` #### 目前遇到執行時間可能非常數時間的問題 ## q_size 利用 `list_for_each` 走訪整個 linked list ,每經過一個 node 便將 count + 1,如此便能得到 node 的數量。 ```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 參考[你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list#%E4%BD%A0%E6%89%80%E4%B8%8D%E7%9F%A5%E9%81%93%E7%9A%84-C-%E8%AA%9E%E8%A8%80-linked-list-%E5%92%8C%E9%9D%9E%E9%80%A3%E7%BA%8C%E8%A8%98%E6%86%B6%E9%AB%94),利用若快指標比慢指標快兩倍的話,則當快指標走到 head (node 數為奇數)或快指標的下一個 node 為 head (node 數為偶數)時慢指標恰為整個鏈結串列的一半,之特點撰寫。 :::danger 既然給定的鏈結串列是環狀,那「盡頭」在哪? 用精準的詞彙書寫! :notes: jserv ::: ```c bool q_delete_mid(struct list_head *head) { // https://leetcode.com/problems/delete-the-middle-node-of-a-linked-list/ if (head == NULL || list_empty(head)) return false; struct list_head *fast = head->next, *slow = head->next; while (fast != head && fast->next != head) { fast = fast->next->next; slow = slow->next; } list_del(slow); q_release_element(list_entry(slow, element_t, list)); return true; } ``` ## q_delete_dup 用`temp`儲存指向上一個 node 的指標,從第二個 node 開始做比較,如果現在兩者的值相同則將`duplicate`計為1並移除 temp 儲存的 node ,若兩者值不同但`duplicate`為1代表 temp 儲存的 node 是剩餘沒刪到的重複 node 。 ```c bool q_delete_dup(struct list_head *head) { // https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/ if (head == NULL || list_empty(head) || list_is_singular(head)) return false; struct list_head *li, *temp; int duplicate = 0; for (li = head->next->next, temp = li->prev; li != head; li = li->next, temp = li->prev) { if (strcmp(list_entry(li, element_t, list)->value, list_entry(temp, element_t, list)->value) == 0) { duplicate = 1; list_del(temp); q_release_element(list_entry(temp, element_t, list)); } else if (duplicate == 1) { duplicate = 0; list_del(temp); q_release_element(list_entry(temp, element_t, list)); } } if (duplicate) { list_del(temp); q_release_element(list_entry(temp, element_t, list)); } return true; } ``` ## q_swap 由於`list_add`的本質是將當前的節點 remove 並插入到所指定節點的後方,因此利用這個函式就能夠達到 swap 的目的 ```c void q_swap(struct list_head *head) { // https://leetcode.com/problems/swap-nodes-in-pairs/ if(head == NULL || list_empty(head)) return; struct list_head *li; for(li = head->next;li != head && li->next != head;li = li->next) list_move(li, li->next); } ``` ## q_reverse 先將第一個節點的位址存到`first`作為判斷結束依據,接著利用`while()` 將最後一個節點插入到第一個 node 直到最後一個 node 的位址為 first (也就是說第一個 node 已經被擠到最後面) ```c void q_reverse(struct list_head *head) { if (head == NULL || list_empty(head)) return; struct list_head *li = head->next; while (li != head) { li = li->next; list_move(li->prev, head); } } ``` :::warning 撰寫更精簡的程式碼。 :notes: jserv ::: ## q_reverseK 當 count 等於 K 時,則利用`list_move()`將 temp 到 li 之間的所有 node 做翻轉。 ```c void q_reverseK(struct list_head *head, int k) { // https://leetcode.com/problems/reverse-nodes-in-k-group/ if (head == NULL || list_empty(head) || list_is_singular(head)) return; struct list_head *li = head->next, *lj, *temp = head, *next; int count = 0; while(li != head){ count++; next = li->next; if (count == k) { lj = temp->next->next; while(lj != next->next){ list_move(lj->prev, temp); lj = lj->next; } temp = next->prev; count = 0; } li = next; } } ``` ## q_sort 一開始使用簡單的bubble sort做撰寫。 ```c void q_sort(struct list_head *head) { if (head == NULL || list_empty(head)) return; struct list_head *current, *tail; tail = head; element_t *current_node, *next_node; while (tail != head->next) { current = head->next; while (current->next != tail) { current_node = list_entry(current, element_t, list); next_node = list_entry(current->next, element_t, list); if (strcmp(current_node->value, next_node->value) > 0) list_move(current, current->next); else current = current->next; } tail = current; } } ``` 後來參考[你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list#circular-linked-list),和 [Merge Sort 與它的變化](https://hackmd.io/@lambert-wu/list-merge-sort),與[seasonwang0905](https://hackmd.io/@seasonwang/B1B0lVqpo#q_sort)的作法改寫成 merge sort 的形式。 ```c struct list_head *mergeTwoLists(struct list_head *L1, struct list_head *L2) { struct list_head *head = NULL, **indirect = &head, **node; for (node = NULL; L1 && L2; *node = (*node)->next) { node = (strcmp(list_entry(L1, element_t, list)->value, list_entry(L2, element_t, list)->value) < 0) ? &L1: &L2; *indirect = *node; indirect = &(*indirect)->next; } *indirect = (struct list_head *)((uintptr_t) L1 | (uintptr_t) L2); return head; } ``` ```c struct list_head *merge_sort(struct list_head *head) { if (head == NULL || head->next == NULL) return head; struct list_head *fast = head, *slow = head, *mid; struct list_head *left,*right; while (fast != NULL && fast->next != NULL) { fast = fast->next->next; slow = slow->next; } mid = slow; slow->prev->next = NULL; left = merge_sort(head); right = merge_sort(mid); return mergeTwoLists(left, right); } ``` 上述的 `mergeTwoLists` 適用於以 NULL 為結尾的非環狀 Linked list ,因此在使用時要先將最後一個 node 裡指向下個 node 的指標設為 NULL。 在執行完 `mergeTwoLists` 後需要透過迴圈將 linked list 重新設為環狀。 ~~排列要將<s>打斷</s> ??? 的 linked list 重新接回。~~ :::warning 避免用「打斷」這樣不精準的詞彙,在作業系統中,可能會對應到 preempt 或 interrupt 等詞彙,顯然非你本來的意思。 :notes: jserv ::: ```c void q_sort(struct list_head *head) { if (head == NULL || list_empty(head)) return; head->prev->next = NULL; head->next = merge_sort(head->next); struct list_head *cur = head, *next = head->next; while (next != NULL) { next->prev = cur; cur = next; next = next->next; } cur->next = head; head->prev = cur; } ``` ## q_descend 比較 temp 與前一個節點的值,若前一個節點較小則將其全部 remove ,若前一個節點較大則將比較的基準移至前一個節點。 ```c int q_descend(struct list_head *head) { // https://leetcode.com/problems/remove-nodes-from-linked-list/ if (head == NULL || list_empty(head) || list_is_singular(head)) return 0; struct list_head *li, *temp = head->prev, *safe; for (li = head->prev->prev, safe = li->prev; li != head; li = safe, safe = li->prev) { if (strcmp(list_entry(temp, element_t, list)->value, list_entry(li, element_t, list)->value) > 0) { list_del(li); q_release_element(list_entry(li, element_t, list)); } else temp = li; } return q_size(head); } ``` :::danger 注意作業書寫規範:中英文間用一個半形空白字元區隔。 程式碼縮排亦有相關規範,請依循! :notes: jserv ::: ## q_merge 參考 [SPFishcool](https://hackmd.io/@SPFishcool/r1r4u_26j) 整理的結構圖與 [komark06](https://hackmd.io/@komark06/SyCUIEYpj#2023q1-Homework1-lab0) 的實作