Try   HackMD

2025q1 Homework1 (lab0)

contributed by < alexc-313 >

作業書寫規範:

  • 無論標題和內文中,中文和英文字元之間要有空白字元 (對排版和文字搜尋有利)
  • 文字訊息 (尤其是程式執行結果) 請避免用圖片來表示,否則不好搜尋和分類
  • 共筆書寫請考慮到日後協作,避免過多的個人色彩,用詞儘量中性
  • 不要在筆記內加入 [TOC] : 筆記左上方已有 Table of Contents (TOC) 功能,不需要畫蛇添足
  • 不要變更預設的 CSS 也不要加入任何佈景主題: 這是「開發紀錄」,用於評分和接受同儕的檢閱
  • 在筆記中貼入程式碼時,避免非必要的行號,也就是該手動將 c=cpp= 變更為 ccpp。行號只在後續討論明確需要行號時,才要出現,否則維持精簡的展現。可留意「你所不知道的 C 語言: linked list 和非連續記憶體」裡頭程式碼展現的方式
  • HackMD 不是讓你張貼完整程式碼的地方,GitHub 才是!因此你在開發紀錄只該列出關鍵程式碼 (善用 diff 標示),可附上對應 GitHub commit 的超連結,列出程式碼是為了「檢討」和「便於他人參與討論」
  • 留意科技詞彙的使用,請參見「資訊科技詞彙翻譯」及「詞彙對照表
  • 不要濫用 :::info, :::success, :::warning 等標示,儘量用清晰的文字書寫。:::danger 則僅限授課教師作為批注使用
  • 避免過多的中英文混用,已有明確翻譯詞彙者,例如「鏈結串列」(linked list) 和「佇列」(queue),就使用該中文詞彙,英文則留給變數名稱、人名,或者缺乏通用翻譯詞彙的場景
  • 在中文敘述中,使用全形標點符號,例如該用「,」,而非 ","。注意書名號的使用,即 ,非「小於」和「大於」符號
  • 避免使用不必要的 emoji 字元

開發環境

$ gcc --version
gcc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0

$ lscpu
Architecture:             x86_64
  CPU op-mode(s):         32-bit, 64-bit
  Address sizes:          48 bits physical, 48 bits virtual
  Byte Order:             Little Endian
CPU(s):                   12
  On-line CPU(s) list:    0-11
Vendor ID:                AuthenticAMD
  Model name:             AMD Ryzen 5 7640U w/ Radeon 760M Graphics
    CPU family:           25
    Model:                116
    Thread(s) per core:   2
    Core(s) per socket:   6
    Socket(s):            1
    Stepping:             1
    Frequency boost:      enabled
    CPU(s) scaling MHz:   32%
    CPU max MHz:          4971.0000
    CPU min MHz:          400.0000
    BogoMIPS:             6986.86
Virtualization features:  
  Virtualization:         AMD-V
Caches (sum of all):      
  L1d:                    192 KiB (6 instances)
  L1i:                    192 KiB (6 instances)
  L2:                     6 MiB (6 instances)
  L3:                     16 MiB (1 instance)
NUMA:                     
  NUMA node(s):           1
  NUMA node0 CPU(s):      0-11

針對佇列操作的程式碼實作

q_head

Commit 117a180

使用 malloc 分配所需的記憶體,並用 list.h 中的 INIT_LIST_HEAD 函式來初始化佇列的 head。

q_free

Commit aca44ec

在判斷 head 不是空指標後,用 list.h 中的 list_for_each_safe 巨集走訪每個節點,因為此巨集使用 *safe 來預先存放下一個指標,我們便可以使用 free 先釋放 value 使用的記憶體,再用第二個 free 釋放 node 本身所使用的記憶體,在走訪完整個佇列後,再用一次 free 釋放 head

q_insert head/tail

Commit a38b8fc

因為這兩個函式在概念上非常類似,所以在實作上我將這兩個函式放在同一個commit中。方法都是先判斷head 不是空指標後,用 malloc 逐次分配並確認記憶體成功的被分配,依據 CERN Common vulnerabilities guide for C programmers 的建議,改用 strncpy 函式複製要插入佇列的數值,再分別使用 list.h 中的 list_addlist_add_tail 函式將複製完成的變數插入佇列中。

q_remove head/tail

Commit f62cfd5

如同上個 commit,這兩個函式在概念上非常類似,所以在實作上我也將這兩個函式放在同一個commit中。方法都是先判斷 head 不是空指標且非空佇列後,用 list.h 中的 list_first_entrylist_last_entry 找出目標節點的 value,再用 strncpy 函式複製到指定的變數,並確保字串的最後是終止字元 \0 ,最後用 list_del_init 來移除節點並確保移除後的節點的指標有被妥善的處理,以避免後續使用上發生未定義的行為。

q_size

Commit 961c87b

在判斷 head 是否空指標與是否是空佇列後,使用 list.h 中的 list_for_each 走訪並記錄節點數量。

q_delete_mid

Commit e1647b8

在判斷 head 是否空指標與是否是空佇列後,使用快慢指標找出佇列的中點,將節點從佇列中釋放,並釋放節點所使用的記憶體。

q_delete_dup

Commit f610366

在判斷 head 是否空指標與是否是空佇列後,使用 list.h 中的 list_for_each_entry_safe 走訪整個佇列,當目前的節點的 value 跟下一個節點的 value 相同時,用變數 found_dup 記錄,並刪除目前節點。當目前節點不等於下個節點時,若 found_dup == true ,代表目前節點與上個已刪除的節點相同,故刪除該節點。

q_delete_node

Commit 112896d

在觀察到許多函式皆有使用以下程式碼後

element_t *node = list_entry(n_ptr, element_t, list);
list_del(n_ptr);
free(node->value);
free(node);

我增加了 q_delete_node 來取代這些重複的程式碼。這個函式多了

if (node->value)
    free(node->value);

避免盲目的釋放記憶體。

q_swap

Commit 6608629

使用 list.h 中的 list_move 函式讓 curcur->next 互換,此時 cur 在原本 cur->next 的位置,再將 cur 設為 cur->next 就可完成兩兩互換。

q_reverse

Commit bc01842

在判斷 head 是否空指標與是否是空佇列後,使用 list.h 中的 list_for_each_safe 走訪整個佇列把每一個節點逐一用 list_move 移動到佇列的 head->next 位置。

q_reverseK

Commit eb73a28

使用兩個 for 迴圈,用 *start 來記錄第一個將被倒置的節點,內部迴圈用來倒置 *start 後的 K 個節點,外部迴圈用更新 *start

merge_two

Commit 2424ca7

這是一個 q_sort 的輔助程式,此函式接收兩個 *list_head 參數,並使用迭代的方式將兩個佇列以遞增或遞減的方式合併,實作上我使用 list_del 將兩個節點從各自的佇列中移除,在比較其 value 後用 list_add_tail 把較小或較大的節點加到新的佇列 merge ,當其中一個佇列為空時,若其中一個佇列有尚未加入 merge 的節點,利用 list_splice 把剩餘的節點加在 merge 尾端,並在將第一個或第二個 *list_head 指向合併完成的佇列後回傳。

Commit 8873e16

為了讓 merge_two 能被 q_merge 使用,我們必須做一些修改,因為我在 q_merge 中的寫法需要確保 merge_two 回傳並將第一個 *list_head 指向合併完的佇列。

if (!list_empty(head_1)) {
    list_splice_init(&head, head_1);
} else {
    list_splice_init(&head, head_2);
    list_splice_init(head_2, head_1);
}

所以在第二個佇列剰於的情況下,會多使用一個 list_splice_init 把第二個佇列接到第一個佇列上。

q_sort

Commit 2424ca7

這個函式用遞迴實做 merge sort 演算法。
在用快慢指標找到佇列的中點後使用 list_cut_position 函式將佇列剖半,再呼叫自己並傳入剖半完的佇列,最後呼叫 merge_two 函式將所有佇列組合起來。

q_ascend/descend

Commit 8827730

參考 list.h 中的 list_for_each_safe 寫出

for (node = (head)->prev, safe = node->prev; 
    node != (head);
    node = safe, safe = node->prev)

來安全的反著走訪所有的節點,用 biggest 變數存放最大的字串,再用 q_delete_node 刪除 valuebiggest 大或小的節點。

q_merge

Commit 94075cc

用 for 迴圈走訪除了第一個以外每個 queue_contex_t ,呼叫 merge_two 函式將佇列一個個合併到第一個佇列裡。

結果

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

由於我尚未完成 lab0-c 的 Welch's test 測試項目,所以除了 trace-17 的部份沒有通過以外,其他部份皆順利通過。

亂數

shuffle

Commit a87564f

q_test.c 中加入 do_shuffle 函式,再用 ADD_COMMAND 就可以使用新的命令 shuffle

其中 do_shuffle 的實做方法如下:
先用 q_size 取得佇列長度 len ,用 rand 抽出一個在區間

[0,len1] 中的隨機數,並將 len-1 跟隨機數做指標交換,再將 len - 1。

統計方法驗證 shuffle

測試 [1, 2, 3, 4] 做 shuffle 1,000,000 次結果:

Expectation:  41666
Observation:  {'1234': 41517, '1243': 41531, '1324': 41621, '1342': 41430, '1423': 42093, '1432': 41945, '2134': 41828, '2143': 41186, '2314': 41658, '2341': 41775, '2413': 41515, '2431': 41375, '3124': 41727, '3142': 41975, '3214': 41575, '3241': 41663, '3412': 41318, '3421': 41791, '4123': 41669, '4132': 41943, '4213': 41993, '4231': 41601, '4312': 41728, '4321': 41543}
chi square sum:  28.45183122929967

直方圖:
Figure_1

有 24 個樣本,自由度為 23 ,直方圖呈現 uniform distribution , chi square sum 為 28.45 。查卡方分佈表可知 p-value 介於 0.1~0.9 ,因為 P value(0.1~0.9)> alpha(0.05),所以統計檢定的結果不拒絕虛無假說。統計檢定的結果不拒絕虛無假說

(H0),也就是樣本資料不拒絕 shuffle 的結果遵循 Uniform distribution,因為沒有足夠證據推翻。

啟發和疑惑

由於 Fisher–Yates shuffle 是適用於陣列的演算法,在佇列上使用因為取用每個節點的時間並非

O(1) 造成時間複雜度大幅增加,從
O(n)
變成
O(n2)
,應該要使用適合佇列的演算法或是將佇列轉換成陣列。