# 2024q1 Homework1 (lab0) contributed by < `yenshipotato` > ### Reviewed by `yenslife` #### `q_reverse` > 將走訪到的節點之 prev 與 next 交換 利用 list API 來簡化 `q_reverse` 實作:其實可以用 `list_move` 直接將走訪到的節點移到開頭,增加程式碼可讀性。 ```diff struct list_head *cur, *next; list_for_each_safe (cur, next, head) { + list_move(cur, head); - cur->next = cur->prev; - cur->prev = next; } - now = head->next; - head->next = head->prev; - head->prev = now; ``` #### `q_remove_tail` / `q_remove_head` 直接在 `q_remove_tail` 中呼叫 `q_remove_head` 即可,減少重複程式碼。 注意 `list_first_entry` 會回傳 `head` 的下一個節點,所以要傳入 `head->prev->prev` 才是指向尾端節點。 ```graphviz digraph main { node[shape=plaintext] head [fontcolor="red"]; l [label="list_first_entry(head->prev->prev, ...)",fontcolor="blue"]; node[shape=box] s0 [label="1"]; s1 [label="2"]; s2 [label="3"]; s3 [label="4"]; s4 [label="5"]; s5 [label="6"]; { rank="same"; s0->s1 s1->s0 s1->s2 s2->s1 s2->s3 s3->s2 s3->s4 s4->s3 s4->s5 s5->s4 s5->s0 s0->s5 } head->s0 l->s5 } ``` ```diff /* Remove an element from tail of queue */ element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize) { - if (!head || list_empty(head)) - return NULL; - element_t *rm_node = list_last_entry(head, element_t, list); - if (sp) { - strncpy(sp, rm_node->value, bufsize - 1); - sp[bufsize - 1] = '\0'; - } - list_del(&rm_node->list); + return q_remove_head(head->pre->prev, sp, bufsize); } ``` > 感謝建議,已更新 #### Commit message 從無到有的函式實作用 "Fix" 開頭可能不太恰當,建議用 "Add" 或是 "Implement"。 ## 開發環境 ```shell $ gcc --version gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0 $ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Address sizes: 46 bits physical, 48 bits virtual Byte Order: Little Endian CPU(s): 20 On-line CPU(s) list: 0-19 Vendor ID: GenuineIntel Model name: 13th Gen Intel(R) Core(TM) i5-13600K CPU max MHz: 5100.0000 CPU min MHz: 800.0000 BogoMIPS: 6988.80 ``` ## 指定的佇列操作 ### q_size 根據作業要求中「預期目標 + 開發環境設定」章節內的指示,以對應的 C 語言程式碼取代 `q_size` 中的 `return -1;` ### `q_new` :::danger 列出程式碼之前,要充分討論你的想法、考量因素,也該談及你作法的副作用 (side effect)。 ::: <s>閱讀完 list.h 裡提供的 API 後,開始實作 queue.c 裡的功能。</s> ```c struct list_head *q_new() { struct list_head *new_head = malloc(sizeof(struct list_head)); if (new_head) INIT_LIST_HEAD(new_head); return new_head; } ``` :::danger 說好的進度呢? ::: ### `q_free` `list_for_each_entry_safe` 走訪佇列每一個節點,並 `q_release_element` 釋放當前節點,最後再釋放 `head` 。 ```c void q_free(struct list_head *head) { if (!head) return; element_t *cur_entry, *safe_entry; list_for_each_entry_safe (cur_entry, safe_entry, head, list) q_release_element(cur_entry); free(head); } ``` ### `q_insert_head` 宣告一個 `node` 用以存放新節點位址的指標,如果 `malloc` 失敗的話回傳 `false` 。若函式沒有回傳則判斷 `value` 在賦值後是否為空字串,若不然,則將節點新增至佇列中;反之則釋放 `node` 並回傳 `false` 。 ```c bool q_insert_head(struct list_head *head, char *s) { element_t *node = malloc(sizeof(element_t)); if (!node) return false; if ((node->value = strdup(s)) != NULL) list_add(&node->list, head); else { free(node); return false; } return true; } ``` ### `q_insert_tail` 功能上與 `q_insert_head` 無異,只需把 `list_add` 替換成 `list_add_tail` ```diff /* Insert an element at tail of queue */ if ((node->value = strdup(s)) != NULL) - list_add(&node->list, head); + list_add_tail(&node->list, head); ``` ### `q_remove_head` 查看 `queue.h` 後確認 `sp` 與 `bufsize` 的用途,宣告 `rm_node` 用以存放待移除節點位址的指標,若 `sp` 不為 `NULL` ,則將節點 `value` 中的字串複製到 `sp` ,最後呼叫 `list_del` 將節點從佇列中移除。 ```c element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize) { if (!head || list_empty(head)) return NULL; element_t *rm_node = list_first_entry(head, element_t, list); if (sp) { strncpy(sp, rm_node->value, bufsize - 1); sp[bufsize - 1] = '\0'; } list_del(&rm_node->list); return rm_node; } ``` ### `q_remove_tail` 功能上與 `q_remove_head` 無異,只需將取得待移除節點位址的函式替換成 `list_last_entry` 即可 ```diff return NULL; - element_t *rm_node = list_first_entry(head, element_t, list); + element_t *rm_node = list_last_entry(head, element_t, list); if (sp) { ``` :::danger diff 該針對「你程式碼之間的變異」,而非針對給定的程式碼。 開發紀錄中,應該要看到你的「開發過程」。 提高程式碼的可重用程度。 ::: ### `q_delete_mid` 在閱讀了教材[你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list#%E6%A1%88%E4%BE%8B%E6%8E%A2%E8%A8%8E-LeetCode-21-Merge-Two-Sorted-Lists)其中關於快慢指標的的案例後,決定以此方法進行實作。宣告一組 `fast` 和 `slow` 快慢指標,其中 `fast` 以一次兩節點的方式走訪佇列; `slow` 則一次一節點,當 `fast` 到達佇列結尾時, `slow` 會在佇列中央,再移除 `slow` 所在的節點即可 ```c bool q_delete_mid(struct list_head *head) { if (!head || list_empty(head)) return false; struct list_head *fast = head->next, *slow = head->next; while (fast->next->next != head && fast->next != head) { fast = fast->next->next; slow = slow->next; } element_t *rm_node = list_entry(slow, element_t, list); list_del(slow); q_release_element(rm_node); return true; } ``` :::danger diff 該針對「你程式碼之間的變異」,而非針對給定的程式碼。 開發紀錄中,應該要看到你的「開發過程」。 ::: ### `q_reverse` 使用 `list_for_each_safe` 走訪佇列,並將走訪到的節點之 `prev` 與 `next` 交換,最後再處理 `head` 。 ```c void q_reverse(struct list_head *head) { if (!head || list_empty(head)) return; struct list_head *now, *next; list_for_each_safe (now, next, head) { now->next = now->prev; now->prev = next; } now = head->next; head->next = head->prev; head->prev = now; } ``` :::danger diff 該針對「你程式碼之間的變異」,而非針對給定的程式碼。 開發紀錄中,應該要看到你的「開發過程」。 ::: ### `q_reverseK` 參考 [chiangkd](https://github.com/sysprog21/lab0-c/commit/06697595174ce67dc538943e69f5f4e69dacc14f),以 `list_cut_position` 與 `list_splice_tail_init` 、 `list_splice_init` ,便可將佇列以 k 個為單位執行 `q_reverse` 。 ```c void q_reverseK(struct list_head *head, int k) { if (!head || list_empty(head)) return; int len = q_size(head), node_num; struct list_head *temp = head->next; if (len < k) return; LIST_HEAD(temp_head); do { for (node_num = 0; node_num < k && temp != head; node_num++) temp = temp->next; if (node_num == k) { LIST_HEAD(cut); list_cut_position(&cut, head, temp->prev); q_reverse(&cut); list_splice_tail_init(&cut, &temp_head); } } while (temp != head); list_splice_init(&temp_head, head); } ``` :::danger diff 該針對「你程式碼之間的變異」,而非針對給定的程式碼。 開發紀錄中,應該要看到你的「開發過程」。 ::: ### `q_swap` `q_reverseK` 的特殊情境,與 `q_reverseK` with k = 2 無異。 ```c void q_swap(struct list_head *head) { if (!head || list_empty(head) || list_is_singular(head)) return; q_reverseK(head, 2); } ``` :::danger diff 該針對「你程式碼之間的變異」,而非針對給定的程式碼。 開發紀錄中,應該要看到你的「開發過程」。 ::: ### `q_ascend` 、 `q_descend` #### 輔助函式 `cmp` 為使程式碼更好維護與簡潔,新增了 `cmp` 函式,接收兩個 `struct list_head*` 回傳內部節點 `value` 的比較結果 ```c static int cmp(struct list_head *a, struct list_head *b) { return strcmp(list_entry(a, element_t, list)->value, list_entry(b, element_t, list)->value); } ``` :::danger 明確指出你參考的 GitHub 帳號名稱。 ::: 在觀摩 [25077667](https://github.com/sysprog21/lab0-c/commit/5b689d81b8e80a716177f92cd1a262d7c813faba)、[chiangkd](https://github.com/sysprog21/lab0-c/commit/cb78f8f06e613a6cf1d43df18145a94dceb31d37)、[cheezad](https://github.com/sysprog21/lab0-c/commit/9e84e978f056d94da9c7092ca31124507ee2ff40)的做法後,發現他們都額外新增了反向走訪的功能。於是我思考能不能直接做 `q_reverse` 搭配 `list_for_each_safe` ,我認為是一樣的效果,還可以讓程式碼更簡潔,決定直接以實作驗證我的想法。 :::danger 你的實驗佐證在哪裡? > `q_reverse` 後,節點的 `next` 與 `prev` 會交換,於是 `list_for_each_safe` 中的走訪 `next` 節點在這裡就會是走訪原始佇列的 `prev` ,儘管這個做法所需的時間將會比直接新增反向走訪功能還長,但時間複雜度仍維持 O(n) ,而充分重複利用撰寫好的函式也讓程式碼更簡潔。 > > 提供數學推導和實驗,工程人員要用客觀事實說話。 :notes: jserv ::: #### `q_ascend` 於反轉後走訪節點時,移除 `value` 比前一個節點大的節點,再反轉即是結果。 ```c int q_ascend(struct list_head *head) { if (!head || list_empty(head)) return 0; struct list_head *now, *safe; q_reverse(head); list_for_each_safe (now, safe, head->next) { if (now == head) break; if (cmp(now, now->prev) > 0) { list_del(now); } } q_reverse(head); return q_size(head); } ``` #### `q_descend` 功能上與 `q_ascend` 無異,只需更改移除的條件(較大 -> 較小) ```diff - if (cmp(now, now->prev) > 0) { + if (cmp(now, now->prev) < 0) { ``` 在使用 qtest 測試時發現有記憶體錯誤,經確認是沒有釋放節點的空間。 ```diff + element_t *rm_node = list_entry(now, element_t, list); list_del(now); + q_release_element(rm_node); ``` 修正後再度測試,沒有發現錯誤 ### `q_delete_dup` 我在這裡思考很久,因為看了 queue.h 中並沒有提到執行此函式時佇列會是排序好的狀態,但附上的 leetcode 題目連結則說佇列預設是排序好的狀態,於是我去看 traces 目錄中有關於此功能的檔案,發現在 `dedup` 前會執行 `sort` ,於是我假設佇列在執行前會是排序好的狀態。而在排序好的情況之下,佇列中 `value` 重複的節點必定相鄰,於是只需要走訪佇列時檢查該節點與下一個節點是否重複,若然,移除該節點後繼續下一輪檢查。如此,便只需要走訪每個節點一次。 ```c bool q_delete_dup(struct list_head *head) { if (!head || list_empty(head)) return false; element_t *now, *safe_now; bool dup = false; list_for_each_entry_safe(now, safe_now, head, list) { if (now->list.next != head && strcmp(now->value, safe_now->value) == 0) { list_del(&now->list); q_release_element(now); dup = true; } else if (dup) { list_del(&now->list); q_release_element(now); dup = false; } } return true; } ``` ### `q_sort` > commit [ca67e78](https://github.com/yenshipotato/lab0-c/commit/ca67e783b25bfbd5c35d340783dbb0b2cd5f3f75) 這裡我參考了 [list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) 新增了 `merge` 和 `merge_final` 兩個函式輔助執行,前者執行兩個鏈結串列的單向合併(去掉了 `prev` );後者則會鏈結串列的 `prev` 重建,以組成新的雙向鏈結串列。並搭配使用上述所提到的輔助函式 `cmp` 作為 `sort` 內的比較函式,最後將 list_sort.c 內的 sort 部分改寫,使得可以在此程式內運行。 :::danger 你如何確保目前的測試已涵蓋排序演算法的最差狀況? ::: ### `q_merge` 我重新查閱了 queue.h 發現有提供 `queue_contex_t` 作為佇列的存取點,接著,我首先想到的策略是,將除了第一個以外的所有佇列接到第一個佇列的末端,最後再用 `q_sort` 排序,並據此開始實作。 ```c int q_merge(struct list_head *head, bool descend) { if (!head || list_empty(head)) return 0; queue_contex_t *now, *first = list_entry(head->next, queue_contex_t, chain); list_for_each_entry(now, head, chain) { if (now == first) continue; if (&now->chain == head) break; list_splice_tail_init(now->q, first->q); } q_sort(first->q, descend); return q_size(first->q); } ``` 後來我看到 `queue.h` 聲明程式在呼叫此功能前,所有佇列都會是以排序好的狀態,說明我的方法在效率上還有改善的餘地。 至此, `make test` 裡的分數達到 95/100。根據作業說明應閱讀論文並著手修改 dudect 以修復判斷常數執行時間的功能 --- ## `dudect` :::danger 指出論文和實作程式碼有出入的地方並充分討論。 ::: 在我參閱了 [dudect.h](https://github.com/oreparaz/dudect/blob/master/src/dudect.h#L429) 後,在原始碼裡的 `dudect_main` 發現了作業檔案 `dudect/fixture.c` 也有的函式 `update_statistics` ,但不同的是,`dudect_main` 裡除了 `update_statistics` 還有 `prepare_percentiles` ,據 dudect.h 裡的註解,這是用以 set different thresholds for cropping measurements,結合作業說明所說, percentile 相關的功能沒有被實作,於是我先試著將有關於 `prepare_percentiles` 的部分加進作業裡。 直接加入了 dudect.h 中的 `cmp` 和 `percentile` 到 fixture.c 中 並把 `prepare_percentiles` 裡的參數改寫 ```c static void prepare_percentiles(int64_t *exec_times) { qsort(exec_times, N_MEASURES, sizeof(int64_t), (int (*)(const void *, const void *))cmp); for (size_t i = 0; i < N_MEASURES; i++) { exec_times[i] = percentile( exec_times, 1 - (pow(0.5, 10 * (double)(i + 1) / N_MEASURES)), N_MEASURES); } } ``` ```diff bool ret = measure(before_ticks, after_ticks, input_data, mode); differentiate(exec_times, before_ticks, after_ticks); + prepare_percentiles(exec_times); update_statistics(exec_times, classes); ret &= report(); ``` 經 make test 測試後通過,分數為 100/100 ## 在 qtest 提供新的命令 shuffle 在參閱 [Fisher–Yates shuffle](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle)後,以 original method 實作,在 `qtest.c` 中實作負責 shuffle 的函式,主要邏輯與 Fisher–Yates shuffle 原始的方法相同。 在佇列尚未被選中的部分裡隨機挑一個節點,將被選中節點移至最後面,並重複上述行為直到所有節點都被選中。 :::danger `qtest.c` 已呼叫 `srand`,不該在 `do_shuffle` 額外呼叫。 ::: ```c bool do_shuffle() { if (!current) return false; for (int i = q_size(current->q); i > 0; i--) { struct list_head *node = current->q; int n = rand() % i + 1; for (; n > 0; n--) node = node->next; list_del(node); list_add_tail(node, current->q); } q_show(3); return true; } ``` :::danger 使用指定的程式碼縮排風格。 ::: ### 以統計的原理來分析你的實作,探究洗牌的「亂度」 $H_0$ : Shuffle 的每個可能發生的機率相同,即符合 Uniform distribution $H_1$ : Shuffle 至少有一可能結果發生的機率不同 我以對 `[1 2 3 4]` 執行 1000008 次 shuffle 為數據,進行卡方檢定 | 可能結果 | 觀測次數 | 預期次數 | | --------- | -------- |:--------:| | [1 2 3 4] | 41484 | 41667 | | [1 2 4 3] | 41786 | 41667 | | [1 3 2 4] | 41677 | 41667 | | [1 3 4 2] | 41809 | 41667 | | [1 4 2 3] | 41640 | 41667 | | [1 4 3 2] | 41620 | 41667 | | [2 1 3 4] | 41751 | 41667 | | [2 1 4 3] | 41853 | 41667 | | [2 3 1 4] | 41894 | 41667 | | [2 3 4 1] | 41856 | 41667 | | [2 4 1 3] | 41409 | 41667 | | [2 4 3 1] | 41709 | 41667 | | [3 1 2 4] | 41935 | 41667 | | [3 1 4 2] | 41571 | 41667 | | [3 2 1 4] | 41394 | 41667 | | [3 2 4 1] | 41799 | 41667 | | [3 4 1 2] | 41634 | 41667 | | [3 4 2 1] | 41358 | 41667 | | [4 1 2 3] | 41758 | 41667 | | [4 1 3 2] | 41722 | 41667 | | [4 2 1 3] | 41694 | 41667 | | [4 2 3 1] | 41396 | 41667 | | [4 3 1 2] | 41574 | 41667 | | [4 3 2 1] | 41685 | 41667 | 經計算,卡方值為 15.170134638922889 ,自由度為 23(24-1) ,根據查表結果 p-value 會落在 (0.8, 0.9) ,顯著水準設定為 0.05 ,因為 0.8>0.05 ,故不拒絕虛無假說。 沒有足夠證據能否定此 shuffle 演算法符合 Uniform Distribution。 ![Figure_1](https://hackmd.io/_uploads/ByJtrxJyC.png) 而根據圖表的結果來看 shuffle 的頻率大致符合 Uniform distribution 。 #### 測試程式 - [ ] `qtest` 的輸入 `input.txt` ``` new it 1 it 2 it 3 it 4 shuffle ``` - [ ] 執行 1000008 次 shuffle 且僅將 shuffle 後的結果輸出 ```bash #!/bin/bash for (( i=1; i<=1000008; i=i+1 )); do ./qtest < data/input.txt > data/temp.txt && sed -n "6p" data/temp.txt >> data/output.txt done ```