--- tag: linux2022 --- # 2022q1 Homework1 (lab0) contributed by < happyjack91630 > ### Reviewed by `kevinshieh0225` - `q_free` 逐一拜訪可用 `list_for_each_safe` - 根據 `list.h` : `#define list_entry(node, type, member) container_of(node, type, member)` - 函式標題打錯很多字喔 - `q_delete_mid` 將 `do-while` 改為 `while` 可使的若佇列長度為 1 時,少做一次指派。 - `q_shuffle` 可用 `list_move_tail` 取代字串操作。 ## 實驗環境 ```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): 8 On-line CPU(s) list: 0-7 Thread(s) per core: 2 Core(s) per socket: 4 Socket(s): 1 NUMA node(s): 1 Vendor ID: GenuineIntel CPU family: 6 Model: 142 Model name: Intel(R) Core(TM) i5-8265U CPU @ 1.60GHz Stepping: 12 CPU MHz: 1800.000 CPU max MHz: 3900.0000 CPU min MHz: 400.0000 BogoMIPS: 3600.00 Virtualization: VT-x L1d cache: 128 KiB L1i cache: 128 KiB L2 cache: 1 MiB L3 cache: 6 MiB NUMA node0 CPU(s): 0-7 ``` ## [作業要求](https://hackmd.io/@sysprog/linux2022-lab0) * 詳細閱讀 [lab0](https://hackmd.io/@sysprog/linux2022-lab0) 說明,依據指示著手修改 queue.[c] 和連帶的檔案,測試後用 Git 管理各項修改。 - [x] `q_new`: 建立新的「空」佇列 - [x] `q_free`: 釋放佇列所佔用的記憶體 - [x] `q_insert_head`: 在佇列開頭 (head) 插入 (insert) 給定的新節點 (以 LIFO 準則) - [x] `q_insert_tail`: 在佇列尾端 (tail) 插入 (insert) 給定的新節點 (以 FIFO 準則) - [x] `q_remove_head`: 在佇列開頭 (head) 移去 (remove) 給定的節點 - [x] `q_release_element`: 釋放特定節點的記憶體空間 - [x] `q_size`: 計算目前佇列的節點總量 - [x] `q_delete_mid`: 移走佇列中間的節點 - [x] `q_delete_dup`: 在已經排序的狀況,移走佇列中具備重複內容的節點 - [x] `q_swap`: 交換佇列中鄰近的節點 - [x] `q_reverse`: 以反向順序重新排列鏈結串列,該函式不該配置或釋放任何鏈結串列節點,換言之,它只能重排已經存在的節點 - [x] `q_sort`: 以==遞增順序==來排序鏈結串列的節點 * 在 qtest 提供新的命令 shuffle - [x] Fisher–Yates shuffle 演算法 ## 針對佇列的操作 ### 結構體說明 ```c= struct list_head { struct list_head *prev; struct list_head *next; }; ``` `struct list_head`為 `list.h` 中定義的結構,經由`prev`與`next`分別指向前一個和下一個節點,便能實作circular double-linked-list。 ```c= typedef struct { char *value; struct list_head list; } element_t; ``` `struct element_`為 `queue.h` 中定義的結構,這結構裡面存了一組 char array 和 `list_head`用來記錄前後節點。 ![](https://i.imgur.com/1z2jjlm.png) > 圖修改自[你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list) ## 針對佇列的操作 ### q_new 建立新的「空」佇列,若沒有配置指定大小的記憶體,則回傳 NULL。 ```c= struct list_head *q_new() { struct list_head *new = malloc(sizeof(struct list_head)); if (!new) { return NULL; // malloc fail } INIT_LIST_HEAD(new); return new; } ``` 建立新的「空」佇列,就是要將`prev`與`next`指標指向自己,可以利用`list.h`裡的`INIT_LIST_HEAD`此巨集函式來處理。 ![](https://i.imgur.com/L7vOD6Z.png) ### q_free 釋放佇列所佔用的記憶體 ```c= void q_free(struct list_head *l) { if (l == NULL) { return; } struct list_head *tmp = l->next; while (tmp != l) { element_t *del = container_of(tmp, element_t, list); tmp = tmp->next; free(del->value); free(del); } free(tmp); } ``` 為了要先避免`l`為空,要先判斷是否為`NULL`,如果不是才執行迴圈。 這裡要使用到`container_of`此巨集函式,用來獲得包含此`list_head`的`elemet_t`的地址,進一步將此`element_t`裡的`value`以及此`element_t`釋放掉。 ```c= //#define container_of(node, type, member) element_t *del = container_of(tmp, element_t, list); //container of便是通過list_head的地址(tmp)計算得到element_t(Node 0)的起始位置 ``` ![](https://i.imgur.com/qNre0jS.png) #### :memo: 小問題 `container of`和`list_entry`這兩個巨集函式都是利用member的地址(node)計算得到type的起始位置。不太明白這兩個函式的差異性? ### q_inset_head 在佇列開頭 (head) 插入 (insert) 給定的新節點 (以 LIFO 準則) ```c= bool q_insert_head(struct list_head *head, char *s) { if (head == NULL) { return false; } element_t *new = malloc(sizeof(element_t)); if (new == NULL) { return false; } int char_size = strlen(s); new->value = malloc(char_size + 1); if (new->value == NULL) { free(new); return false; } memcpy(new->value, s, char_size); //memcpy(dst,src,size); new->value[char_size] = '\0'; list_add(&new->list, head); return true; } ``` 要先`malloc`出`element_t`可用的地址空間`(new)`,如果`malloc`失敗則直接return false,反之則利用`strlen`算出`s`的字元數`char_size`,用來`malloc`出`new->value`此字元串列所需的空間,這邊要小心,由於必須多儲存一個空字元`\0`,所以要創`char_size + 1`的空間。只要有使用`malloc`,要檢查是否有創立成功,如果沒有則必須連同前面創的`new`釋放掉,才不會造成memory leak。利用`memcpy`來將`s`複製進`new->value`空間裡。最後在使用`list_add`此函式將`new`插入到head。 ### q_inset_tail 在佇列尾端 (tail) 插入 (insert) 給定的新節點 (以 FIFO 準則) ```c= bool q_insert_tail(struct list_head *head, char *s) { if (head == NULL) { return false; } element_t *new = malloc(sizeof(element_t)); if (new == NULL) { return false; } int char_size = strlen(s); new->value = malloc(char_size + 1); if (new->value == NULL) { free(new); return false; } memcpy(new->value, s, char_size); new->value[char_size] = '\0'; list_add_tail(&new->list, head); return true; } ``` 與前一個function`q_insert_head`操作邏輯一樣,只是將`list_add`換成`list_add_tail`,就能將new插入到queue的尾巴了。 ### q_remove_head 在佇列開頭 (head) 移去 (remove) 給定的節點 ```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 *remove_head = list_first_entry(head, element_t, list); // for head entry if (remove_head == NULL) { return NULL; } list_del(&remove_head->list); if (sp) { memcpy(sp, remove_head->value, bufsize - 1); sp[bufsize - 1] = '\0'; } return remove_head; } ``` 要移去開頭的節點,必須得知開頭節點以及對應的`element_t`的地址,這時可以使用跟`container of`一樣功能的巨集函式,就是`list_first_entry`,差別在於`list_first_entry`是專門用在找到開頭節點資訊的函式。remove的要求是斷開與其他節點的連結,所以只需要用`list_del`就能斷開了,不需要`free`掉其對應的`element_t`的資訊。最後再將`head`的`value`放進`sp`裡面(要注意的是如果`value`的大小超過了`bufsize`,那只需要放入`bufsize`容量的`value`即可)。 ### q_remove_tail 在佇列開頭 (head) 移去 (remove) 給定的節點 ```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 *remove_tail = list_last_entry(head, element_t, list); // for tail entry if (remove_tail == NULL) { return NULL; } list_del(&remove_tail->list); if (sp) { memcpy(sp, remove_tail->value, bufsize - 1); sp[bufsize - 1] = '\0'; } return remove_tail; } ``` 與前一個function`q_remove_head`操作邏輯一樣,只需要使用`list_last_entry`,就能取得`tail`對應的`element_t`的地址資訊了。 ### q_release_element 釋放特定節點的記憶體空間 ```c= void q_release_element(element_t *e) { free(e->value); free(e); } ``` ### q_szie 計算目前佇列的節點總量 ```c= int q_size(struct list_head *head) { if (head == NULL) { return 0; } int length = 0; struct list_head *node; list_for_each (node, head) { length++; } return length; } ``` 在不更改原有存在的結構體,必須遍歷所有的queue節點,才能算出queue的長度為何。這裡可以使用`list_for_each`此巨集函式,來幫助我們遍歷所有的節點,進而計算出長度。 ### q_delete_mid 移走佇列中間的節點 ```c= bool q_delete_mid(struct list_head *head) { if (head == NULL || list_empty(head)) { return false; } struct list_head *slow_ptr = head; struct list_head *fast_ptr = head; do { slow_ptr = slow_ptr->next; fast_ptr = fast_ptr->next->next; } while (fast_ptr->next != head && fast_ptr != head); element_t *mid_node = list_entry(slow_ptr, element_t, list); // slow_ptr will be the middle list_del(slow_ptr); free(mid_node->value); free(mid_node); return true; } ``` ![](https://i.imgur.com/OqJe3gf.png) > 圖取自[GeeksforGeeks - Find the middle of a given linked list](https://www.geeksforgeeks.org/write-a-c-function-to-print-the-middle-of-the-linked-list/) 利用類似single link-list找middle node的原理運用在double circular link-list,一個`slow_ptr`每次只往前進一步,一個`fast_ptr`每次往前進兩步 只要`fast_ptr`或者`fast_ptr->next`到了`head`,則此時的`slow_ptr`就會是整條link-list的中間點了。這題要注意題目為q_**delete**_mid,所以不只要斷開連結,也要將該節點的`element_t`的所有資訊都刪掉。 ### q_delete_dup(還要再優化) 在已經排序的狀況,移走佇列中具備重複內容的節點 ```c= bool q_delete_dup(struct list_head *head) { if (head == NULL || list_empty(head)) { return false; } struct list_head *cur = head->next; element_t *cur_e; bool flag = false; // record is cur duplicate ? while (cur->next != head) { cur_e = list_entry(cur, element_t, list); element_t *cur_next_e = list_entry(cur->next, element_t, list); if (strcmp(cur_e->value, cur_next_e->value) == 0) { struct list_head *cur_next_next = cur->next->next; cur->next = cur_next_next; cur_next_next->prev = cur; free(cur_next_e->value); free(cur_next_e); flag = true; // cur_e->value == cur_next_e->value, so cur is duplicate } else { if (flag) { // cur is duplicate so need to be delete cur->next->prev = cur->prev; cur->prev->next = cur->next; cur = cur->next; free(cur_e->value); free(cur_e); } else { cur = cur->next; flag = false; } } } if (flag) { // queue only left one item(cur) and this item also duplicate cur->next->prev = cur->prev; cur->prev->next = cur->next; // cur = cur->next; free(cur_e->value); free(cur_e); } return true; } ``` 一開始先設一個`flag`,用來判斷每次的`cur`是否為重複`value`的一員。 :::info ```c= if (strcmp(cur_e->value, cur_next_e->value) == 0) { struct list_head *cur_next_next = cur->next->next; cur->next = cur_next_next; cur_next_next->prev = cur; free(cur_next_e->value); free(cur_next_e); flag = true; // cur_e->value == cur_next_e->value, so cur is duplicate ``` 檢查`cur_e->value`和`cur_next_e->value`是否一樣,一樣的話就把`cur_next`刪掉,且`cur`為重複`value`的一員,所以`flag`變成`true`。 ![](https://i.imgur.com/VNpttOz.png) ::: :::info ```c= if (flag) { // cur is duplicate so need to be delete cur->next->prev = cur->prev; cur->prev->next = cur->next; cur = cur->next; free(cur_e->value); free(cur_e); } else { cur = cur->next; flag = false; } ``` 如果`flag`為`true`,則要將目前的`cur`刪掉,反之`cur`就保留,往下個節點前進。 ![](https://i.imgur.com/zytf6eK.png) ::: :::info ```c= if (flag) { // queue only left one item(cur) and this item also duplicate cur->next->prev = cur->prev; cur->prev->next = cur->next; // cur = cur->next; free(cur_e->value); free(cur_e); } ``` 這個條件式會成立的場景為,全部節點只剩下`cur`一點,而且此節點為重複的一員`(flag =true)`,所以也要將`cur`刪掉。 ![](https://i.imgur.com/3pCoSoZ.png) ::: ### q_swap(還要再優化) 交換佇列中鄰近的節點 ```c= void q_swap(struct list_head *head) { // https://leetcode.com/problems/swap-nodes-in-pairs/ if (head == NULL) { return; } struct list_head *cur = head->next; struct list_head *prev = head; struct list_head *next = cur->next; while (cur->next != head && cur != head) { cur->next = next->next; next->next->prev = cur; cur->prev = next; next->next = cur; next->prev = prev; prev->next = next; // prev->prev =cur; prev = cur; cur = cur->next; next = cur->next; } } ``` 此題參考[leetcode 24.Swap Node in Pairs](https://leetcode.com/problems/swap-nodes-in-pairs/discuss/222534/C-0ms)的做法,只是要再考慮`prev`如何連接就好了。 ### q_reverse 以反向順序重新排列鏈結串列,該函式不該配置或釋放任何鏈結串列節點,換言之,它只能重排已經存在的節點 ```c= void q_reverse(struct list_head *head) { if (head == NULL || list_empty(head)) { return; } struct list_head *cur = head; struct list_head *tmp; do { tmp = cur->prev; cur->prev = cur->next; cur->next = tmp; cur = cur->prev; } while (cur != head); } ``` 此題參考[GeeksforGeeks - Reverse a link list](https://www.geeksforgeeks.org/reverse-a-linked-list/)的做法,只是我是從tail端reverse回來,且必須多考慮`prev`的方向變化。 ### q_sort > 參考 kevinshieh0225 以遞增順序來排序鏈結串列的節點 ```c= struct list_head *mergeTwoLists(struct list_head *L1, struct list_head *L2) { struct list_head *head = NULL, **ptr = &head, **cur; for (cur = NULL; L1 && L2; *cur = (*cur)->next) { // compare accending pair by pair element_t *node1 = container_of(L1, element_t, list); element_t *node2 = container_of(L2, element_t, list); cur = (strcmp(node1->value, node2->value) <= 0) ? &L1 : &L2; *ptr = *cur; ptr = &(*ptr)->next; } *ptr = (struct list_head *) ((uintptr_t) L1 | (uintptr_t) L2); return head; } static struct list_head *mergesort_list(struct list_head *head) { if (!head || !head->next) { return head; } struct list_head *slow = head; for (struct list_head *fast = head->next; fast && fast->next; fast = fast->next->next) { slow = slow->next; } struct list_head *mid = slow->next; slow->next = NULL; struct list_head *left = mergesort_list(head); struct list_head *right = mergesort_list(mid); return mergeTwoLists(left, right); } void q_sort(struct list_head *head) { if (head == NULL || list_empty(head)) { return; } head->prev->next = NULL; // circular to non-circular head->next = mergesort_list(head->next); struct list_head *ptr = head; while (ptr->next) { // reconstruct circular link list (prev) ptr->next->prev = ptr; ptr = ptr->next; } ptr->next = head; head->prev = ptr; } ``` 此題雖然要使用circular double link-list做merge sort,但是其實我們可以先把circular變成non-circular`(head->prev->next = NULL)`就會容易許多。在副程式`mergesort_list`,也是使用`slow_ptr`和`fast_ptr`來切出link-list的middle node,就能拿middle node的左邊(含middle node)和右邊去做排序`mergeTwoLists`。`mergeTwoLists`就是參考了[你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list)裡的**Merge Sort的實作**。要注意得是要使用`uintptr_t(typedef unsigned long int uintptr_t)`這項型態是必須多`#include <stdint.h>`才能使用。 ## qtest 新功能實作:Fisher-Yates shuffle > 參考 kevinshieh0225 先在`qtest.c`裡找到一個`console_init`的function是專門用來放`./qtest`所打的command,所以在這裏面加上一行等等所需要用的shufle。 ```c= static void console_init() { ..... ADD_COMMAND(shuffle, " | shuffle by Fisher-Yates shuffle"); ..... } ``` Fisher-Yates shuffle則是參考了[【筆記】洗牌演算法:Fisher-Yates Shuffle](https://medium.com/@globelex65/%E7%AD%86%E8%A8%98-%E6%B4%97%E7%89%8C%E6%BC%94%E7%AE%97%E6%B3%95-fisher-yates-shuffle-c9feeb463154)實做出來。 ```c= void q_shuffle(struct list_head *head) { if (head == NULL || list_empty(head)) { return; } int q_length = q_size(head); if (q_length == 1) { return; } struct list_head *cur; for (cur = head->next; cur->next != head; cur = cur->next) { int target; do { target = rand() % q_length; } while (target == 0); // prevent target = cur struct list_head *target_list = cur; for (int i = 0; i < target; i++) { target_list = target_list->next; } element_t *cur_e = list_entry(cur, element_t, list); element_t *target_list_e = list_entry(target_list, element_t, list); char *tmp = cur_e->value; cur_e->value = target_list_e->value; target_list_e->value = tmp; q_length--; } } ``` 一開始本來想將`q_shuffle`定義在`queue.c`裡,同時也要將`q_shuffle`定義在`queue.h`,但是如果這樣做,在`git commiat -a`因為無法修改`queue.h`而報錯,所以只能將`q_shuffle`放在`qtest.c`裡面。 ```c= static bool do_shuffle(int argc, char *argv[]) { if (argc != 1) { report(1, "%s takes no arguments", argv[0]); return false; } if (!l_meta.l) report(3, "Warning: Calling shuffle on null queue"); error_check(); set_noallocate_mode(true); if (exception_setup(true)) q_shuffle(l_meta.l); exception_cancel(); set_noallocate_mode(false); show_queue(3); return !error_check(); } ``` 最後在實作能被`ADD_COMMAND`呼叫到的function,此function參考了`do_reverse`所寫的。