# 2023q1 Homework1 (lab0) contributed by < `Damien-Chen` > :::danger 不要打錯自己的 GitHub 帳號名稱。 :notes: jserv ::: ## 開發環境 Windows 11-WSL2-Ubuntu-20.04.5-LTS ```shell $ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Byte Order: Little Endian Address sizes: 48 bits physical, 48 bits virtual CPU(s): 6 On-line CPU(s) list: 0-5 Thread(s) per core: 2 Core(s) per socket: 3 Socket(s): 1 Vendor ID: AuthenticAMD CPU family: 25 Model: 68 Model name: AMD Ryzen 5 6600HS Creator Edition Stepping: 1 CPU MHz: 3293.725 BogoMIPS: 6587.45 Virtualization: AMD-V Hypervisor vendor: Microsoft Virtualization type: full L1d cache: 96 KiB L1i cache: 96 KiB L2 cache: 1.5 MiB L3 cache: 16 MiB ``` ## queue.c 實作 :::danger 注意書寫規範:中英文間用一個空白字元區隔。 :notes: jserv ::: ### 基礎資料結構 在實作前,先對 `list.h` 和 `queue.h` 中定義的資料結構有個基礎認識,搭配 [你所不知道的 C 語言: linked list 和非連續記憶體篇](https://hackmd.io/@sysprog/c-linked-list),對之後的理論與實作能夠更好想像和理解。 list.h 中定義的 linked list 元素如下 ```c struct list_head { struct list_head *prev; struct list_head *next; }; ``` 從 prev 和 next 兩指標可以發現這是個 circular doubly-linked list。 :::warning 注意連字號的位置。 :notes: jserv ::: 在 queue.h 中 將 `list_head` 結構體嵌入每個鏈結串列單元中,如下 ```c typedef struct { char *value; struct list_head list; } element_t; ``` 因此每個鏈結串列單元包含一個字元 value,指向前一個元素的指標prev,指向下一個元素的指標 next。 ### q_new() 該函式要求建立一個空的佇列,所以配置一個 list_head,且該元素的 prev 和 next 皆指向自己。 這裡直接使用內建函式,使程式碼更簡潔。 INIT_LIST_HEAD() : 將 prev 和 next 指向自己。 ```c struct list_head *q_new() { struct list_head *q = malloc(sizeof(struct list_head)); if(!q){ return NULL; } INIT_LIST_HEAD(q); return q; } ``` ### q_free() 走訪所有節點並依序刪除,使用的內建函式如下: list_for_each_entry_safe() : 取得每個節點的開頭位置 ```c void q_free(struct list_head *l) { // need to check whether list_head is NULL // otherwise segmentation fault will happen if(!l){ return; } element_t *curr, *temp; list_for_each_entry_safe(curr, temp, l, list){ q_release_element(curr); } free(l); } ``` ### q_insert_head() 於佇列的開頭加入一個節點,因此配置記憶體須為 element_t 結構,且該結構還包含需要插入的字串,所以配置一個字串 new_s,須注意後面的+1為 '\0' 的空間。 ```c bool q_insert_head(struct list_head *head, char *s) { if (!head) { return false; } element_t *new = malloc(sizeof(element_t)); if (!new) { return false; } char *new_s = malloc((strlen(s) + 1) * sizeof(char)); if (!new_s) { free(new); return false; } memcpy(new_s, s, strlen(s) + 1); new->value = new_s; list_add(&new->list, head); return true; } ``` ### q_insert_tail() 和 insert_head相同,只要更改為 list_add_tail即可。 ```c bool q_insert_tail(struct list_head *head, char *s) { if (!head) { return false; } element_t *new = malloc(sizeof(element_t)); if (!new) { return false; } char *new_s = malloc((strlen(s) + 1) * sizeof(char)); if (!new_s) { free(new); return false; } memcpy(new_s, s, strlen(s) + 1); new->value = new_s; list_add_tail(&new->list, head); return true; } ``` ### q_remove_head() Remove只需將節點的指標unlink,不須釋放記憶體。 根據queue.h中函式的說明撰寫即可,須注意當bufsize = 0時,bufsize - 1 會出現錯誤,因此加入bufsize != 0 當作判斷條件。 list_first_entry() : 取得linked list 開頭的entry list_del() : 將 node 從其所屬的 linked list 結構中移走,且不釋放記憶體。 ```c element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize) { if (!head || list_empty(head)) { return NULL; } element_t *ele = list_first_entry(head, element_t, list); if (sp && bufsize != 0) { strncpy(sp, ele->value, bufsize - 1); sp[bufsize - 1] = '\0'; } list_del(&ele->list); return ele; } ``` ### q_remove_tail() 和remove_head相同,只需更改巨集函式即可 ```c element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize) { if (!head || list_empty(head)) { return NULL; } element_t *ele = list_last_entry(head, element_t, list); if (sp && bufsize != 0) { strncpy(sp, ele->value, bufsize - 1); sp[bufsize - 1] = '\0'; } list_del(&ele->list); return ele; } ``` ### q_size() 使用list_for_each()走訪每個節點即可。 ```c int q_size(struct list_head *head) { if (!head || list_empty(head)) { return 0; } int len = 0; struct list_head *curr; list_for_each (curr, head) { len++; } return len; } ``` ### q_delete_mid() 要找出中間節點,可使用快慢指標方式,快的每次往前走兩步,慢的往前走一步,當快指標到達linked list結尾時(也就是head->prev)就停下,此時慢指標的位置好就是中間節點的位置,將其移除並釋放記憶體。 :::danger 注意書寫規範:中英文間用一個空白字元區隔。 :notes: jserv ::: ```c bool q_delete_mid(struct list_head *head) { if (!head || list_empty(head)) { return false; } struct list_head *fast = head; struct list_head *slow = head; do { fast = fast->next->next; slow = slow->next; } while (fast->prev != head && fast != head); list_del(slow); q_release_element(list_entry(slow, element_t, list)); return true; } ``` ### q_delete_dup() ```c bool q_delete_dup(struct list_head *head) { if (!head) { return false; } struct list_head *curr = head->next; while (curr != head) { if (curr->next != head) { struct list_head *safe = curr->next; element_t *c = container_of(curr, element_t, list); element_t *n = container_of(curr->next, element_t, list); if (!strcmp(c->value, n->value)) { do { struct list_head *next = n->list.next; list_del(&n->list); q_release_element(n); if (next == head) { break; } n = container_of(next, element_t, list); } while (!strcmp(c->value, n->value)); safe = curr->next; list_del(&c->list); q_release_element(c); } curr = safe; } } return true; } ``` ### q_swap() 走訪每個節點,每次走訪將該節點移動至下一個節點後面。一開始我是使用for迴圈的形式(如下程式碼中的註解部分),後來觀察<Lambert>的程式後發現可以直接使用list_for_each()使程式更簡潔。 ```c void q_swap(struct list_head *head) { if (!head || list_empty(head)) { return; } struct list_head *curr; list_for_each (curr, head) { if (curr->next == head) { break; } list_move(curr, curr->next); } /* The following is original form */ // for (struct list_head *curr = head->next; // curr->next != head && curr != head; curr = curr->next) { // list_move(curr, curr->next); // } } ``` ### q_reverse() 走訪每個節點,將其刪除後,再加到開頭。因為要在迴圈過程中更改節點,需使用list_for_each_safe()。 ```c void q_reverse(struct list_head *head) { if (!head || list_empty(head)) { return; } struct list_head *curr, *n; list_for_each_safe (curr, n, head) { list_del(curr); list_add(curr, head); } } ``` ### q_reverseK() 最直覺的想法就是,使用一個counter搭配上述的reverse()。每走訪一個節點,counter加1,當counter == 1時,將前面到目前的節點傳遞給reverse(),反轉後在插入原本位置。 ```c void q_reverseK(struct list_head *head, int k) { if (!head || list_empty(head) || k <= 0) { return; } struct list_head *curr, *safe, start, *pivot; INIT_LIST_HEAD(&start); pivot = head; int cnt = 0; list_for_each_safe(curr, safe, head){ cnt++; if(cnt == k){ list_cut_position(&start, pivot, curr); q_reverse(&start); list_splice_init(&start, pivot); pivot = safe->prev; cnt = 0; } } } ``` 當cnt == k 時,呼叫list_cut_position(),該函式將從pivot到curr中的所有節點全部移到start,再將start反轉後,插入pivot的起始位置。更新pivot位置為下一次的切點。用於當作暫時的head。 ### q_sort() 這裡我參考<paul90317>的方法實現merge sort。 ```c static inline void l_merge(struct list_head *head, struct list_head *left, struct list_head *right) { while (!list_empty(left) && !list_empty(right)) { if (strcmp(list_entry(left->next, element_t, list)->value, list_entry(right->next, element_t, list)->value) < 0) { list_move_tail(left->next, head); } else { list_move_tail(right->next, head); } } if (!list_empty(left)) { list_splice_tail_init(left, head); } else { list_splice_tail_init(right, head); } } ``` ```c void q_sort(struct list_head *head) { if (!head || q_size(head) < 2) return; struct list_head *fast = head, *slow = head; do { fast = fast->next->next; slow = slow->next; } while (fast != head && fast->next != head); LIST_HEAD(left); LIST_HEAD(right); list_splice_tail_init(head, &right); list_cut_position(&left, &right, slow); q_sort(&left); q_sort(&right); l_merge(head, &left, &right); } ``` ### q_descend() 該函式判斷如果該節點的右邊所有節點,只要有一個值比自己大就刪除,因此可以從右往左找最大值,每次只需跟最大值比較即可。 ```c int q_descend(struct list_head *head) { if (!head || list_empty(head)) { return 0; } element_t *entry = list_entry(head->prev, element_t, list), *safe = list_entry(entry->list.prev, element_t, list); char *best = entry->value; while (&entry->list != head) { if (strcmp(entry->value, best) < 0) { list_del(&entry->list); q_release_element(entry); } else { best = entry->value; } entry = safe; safe = list_entry(entry->list.prev, element_t, list); } return q_size(head); } ``` ### q_merge() 把所有佇列合併為一個,再呼叫 sort 排序 ```c int q_merge(struct list_head *head) { if (!head || list_empty(head)) return 0; int n = q_size(head); while (n > 1) { struct list_head *fast = head->next, *slow = head->next; for (int i = 0; i < n / 2; ++i) { LIST_HEAD(temp); l_merge(&temp, container_of(fast, queue_contex_t, chain)->q, container_of(fast->next, queue_contex_t, chain)->q); list_splice_tail(&temp, container_of(slow, queue_contex_t, chain)->q); fast = fast->next->next; slow = slow->next; } if (n & 1) list_splice_tail_init(container_of(fast, queue_contex_t, chain)->q, container_of(slow, queue_contex_t, chain)->q); n = (n + 1) / 2; } return q_size(container_of(head->next, queue_contex_t, chain)->q); } ``` ## 分數 目前分數為 95/100,卡在時間複雜度無法通過。 :::danger 注意書寫規範:中英文間用一個空白字元區隔。 :notes: jserv ::: ## 研讀 linux 核心原始程式碼 list_sort.c linux 使用的 merge sort 和一般常見的不同,從註解中可以看到其設計目標: ``` This mergesort is as eager as possible while always performing at least * 2:1 balanced merges. Given two pending sublists of size 2^k, they are * merged to a size-2^(k+1) list as soon as we have 2^k following elements. * * Thus, it will avoid cache thrashing as long as 3*2^k elements can * fit into the cache. Not quite as good as a fully-eager bottom-up * mergesort, but it does use 0.2*n fewer comparisons, so is faster in * the common case that everything fits into L1. ``` 該方法會維持兩個要 merge 的 list 比例至少為 2 : 1,只要這 2 : 1 的 list 可以被放到 cache,就能有效避免 cache thrashing。可以得知這個演算法的設計是較為 cache friendly 的,且以硬體為出發點的觀點設計。課堂學到或是刷題完全不曾提過此設計觀念。 了解到 linux 的 merge sort 設計目標後,接著欣賞其程式碼,