contributed by < yuyuan0625 >
56han
q_delete_mid
針對環狀且雙向的佇列,是否有更快的方法?
考慮雙向環狀的條件可以發現,用兩個指標從頭和尾向對方移動,
只需要存取 個節點就好,然而快慢指標需要存取 個節點。
關於快慢指標需要存取 個節點
範例 :假設有一個由 5 個節點組成的鏈結串列。對於fast
指針:
在第一次迭代中,fast
指向第 3 個節點(直接存取第 1 個和第 3 個節點,間接存取第 2 個節點)。
在第二次迭代中,由於每次移動都是兩步,fast
指針會嘗試指向第 5 個節點(直接存取第 3 個和第 5 個節點,間接存取第4個節點)。
在這個過程中,第 1 個和第 3 個節點被直接走訪了 2 次(一次為fast
指向的節點,另一次為fast->next
),而第 2 個、第 4 個和第 5 個節點至少被存取了一次。
不該只看開發紀錄,審查學員的 git commits 了嗎?jservImage Not Showing Possible ReasonsLearn More →
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
164253
q_remove_head
和 q_remove_tail
有許多重複部分
q_merge
中,將每個佇列都和第一個合併
會造成最差複雜度達到
q_free
commit db3a900
發現程式無法通過 trace-13 (Test of malloc failure on new),最後發現是 q_free
漏掉了檢查 head
是否存在就直接對 head
進行 element_t
的相關操作。
修改程式碼如下:
q_insert_head
利用 strdup(s)
將字串複製到 element_t
的 value
,並使用 list_add
將新增的節點加到鏈結串列的前端。
q_insert_tail
原本是將 q_insert_head
的程式碼的 list_add
修改成 list_add_tail
。後續想到可以透過更改傳入的指標就可以將整個 q_insert_head
程式碼重用,維持程式的簡潔。
q_remove_*
先前漏掉了以下條件
@sp: string would be inserted
@bufsize: size of the string
If sp is non-NULL and an element is removed, copy the removed string to *sp (up to a maximum of bufsize-1 characters, plus a null terminator.)
因此我對兩個 remove 函式進行進行以下變更:
只複製到 bufsize - 1 的資料
commit cde6823
q_delete_mid
利用快慢指標尋找鏈結串列的中點,並使用 q_release_elemen
將其 entry 刪除。
q_delete_dup
利用 list_for_each_entry_safe
比較兩相鄰節點的 value
值,若相同則刪除當前節點,並將 is_dup = true
,到下一步時就會將該節點也刪除,並重製is_dup = false
,直到走訪完所有節點。
q_reverse
實做方法:利用 list_for_each_safe
走訪每個節點,並使用 list_move
將節點移至鏈結串列的前端,最後就可以獲得和原本順序相反的鏈結串列。
q_reverseK
將鏈結串列分割為長度為 k
的段落,並利用 q_reverse()
將此段落反轉,接著再合併回原始的鏈結串列中,重複此操作直到所剩的鏈結串列長度小於 k
。
原先我忽略 curr
已被更改過,最後還使用 tmp_head = curr
紀錄鏈結串列的切割點,導致存取到錯誤的位置。卡了很久以後看到 komark06 的實作和精美的圖例才發現要改用 safe->prev
才能指向正確的位置。
這次的經驗也讓我後續在更改鏈結串列後會更小心的檢查是否有誤用到已更改的指標,後續也會利用時間學習使用 Graphviz
使用 git diff
或 diff
命令來產生程式碼之間的變更,不要憑感覺填入,留意位移量。
好的已修正!
q_ascend
一開始的想法是從鏈結串列前端往後檢查,但後來發現從尾端往前檢查會更有效率,可以單純用兩兩 element_t
的 value
做比較就好,若不符合就刪除。
q_descend
只要將q_ascend中的判斷式由 >
改成 <
即可
q_swap
q_swap
即為鏈結串列中兩兩一組進行 reverse,即為reverseK, k=2 的情況,因此可以重用q_reverseK
q_sort
q_sort
參考你所不知道的 C 語言: linked list 和非連續記憶體的快慢指標,找出鏈結串列的中點,將未排序的佇列分為左、右兩個子佇列,然後遞歸地對這兩個子佇列進行排序,最後將這兩個有序的子佇列合併成一個有序的佇列。
mergeTwoList
實作方法:利用 cmp
紀錄 L1
第一個節點 E1
和 L2
第一個節點 E2
的大小關係(大於零表示 E1
較大,反之亦然),若 descend=true
就將 cmp
的值變號。
假設descend = false
,若 cmp > 0
表示 E1
較大,因此將 E2
加入暫時的鏈結串列中,並將 count++
。一直重複此比較動作直到 L1
或 L2
其中一個鏈結串列長度為 0 ,並將暫存的鏈結串列移回 L1
的前端,接著再將 L2
移至 L1
的前端,如此一來不論是 L1
或 L2
還有剩餘節點都可以保持正確排序。
q_merge
透過走訪 chain
將每個佇列都串到第一個佇列,同時更新 size
。在連接完成後,對合併後的佇列使用 q_sort 函數進行排序。
執行 make valgrind
,結果顯示無記憶體問題
執行 massif 分析:
並獲得 massif.out.<pid>
檔案
使用 massif-visualizer 將數據圖形化
在 qtest.c
的 queue_insert
和 queue_remove
函式中會判斷 simulation
是否等於 1 ,若為 1 , queue_insert
會使用is_insert_tail_const()
、is_insert_head_const()
; queue_remove
會使用 is_remove_tail_const()
、 is_remove_head_const()
來檢查函式是否能在常數時間內執行完畢。
這四個函式都是由 fixture.h
中 is_##op##_const
定義的。
fixture.h
再進一步看 fixture.c
,發現 is_##op##_const
會回傳 test_const
,而test_const
最後會呼叫 doit
函式。
fixture.c
比對 dudect.h
與 fixture.c
,可以發現 dudect_main
對應於 doit
。在 dudect 中作者將第一批次的測試丟棄,並執行 prepare_percentiles
作為暖身,而doit
中沒有判斷是否為第一筆。另外論文作者在 update_statistics
中,從第十筆後才開始進行統計,故更改程式碼。
update_statistics
doit
後續加入 fixture.c
中缺少的一些函式才能正確執行
fixture.h
更改後即可通過 trace-17-complexity
檢測。
list_sort.c
並引入專案將 list_sort.c
和 list_sort.h
加入專案。並且刪除不必要的 include 。
將 list_sort.c
中的 u8
改成 uint8_t
,而 uint8_t
是由 stdint.h 提供的。因此在 list_sort.h
中加入 #include <stdint.h>
。
list_sort.h
list_sort.c
接著編譯後發現 likely
和 unlikely
未被定義,參考 [Linux Kernel慢慢學]likely and unlikely macro
在Linux kernel 2.6之後提供了likely, unlikely 這兩個macro,他們被定義在[/include/linux/compiler.h]。list_sort.h(https://github.com/torvalds/linux/blob/f6cef5f8c37f58a3bc95b3754c3ae98e086631ca/include/linux/compiler.h#L19)中,幫助compiler做最佳化。
將 likely
, unlikely
這兩個巨集的定義加入 list_sort.h
另外,移除 list_sort.c
的最後一行
最後,因為 list_sort 需要傳入比較函式 cmp
,因此利用字串比較的方式在 qtest.c
中實作。
新增 shuffle 指令
在 qtest.c
中加入
參考 chiangk 的比較實驗,利用 perf 分析執行時間
trace-sort.cmd
Instructions | Cycles |
---|---|
2,469,368,053 | 2,577,816,050 |
Instructions | Cycles |
---|---|
2,338,003,862 | 2,508,507,272 |
可以發現 linux 核心的排序演算法比我自行實作的更有效率。
shuffle
指令q_size
函式獲取佇列的大小 len
。len - 1
) 中隨機選擇一個索引 random 。然後,將 old
指向從佇列前端開始數來第 random 個節點,將 new
指向尚未被選取的最後一個節點。將 old
和 new
指向的節點的值交換,然後將 len
減 1。len
減小,已經被選取並交換值到佇列後面的節點會逐漸增多,直到所有節點都已經被選取過,隨機洗牌操作結束。演算法範例
實作
commit 3d1fd05
因為無法更改 queue.h
的內容,因此需要另外新增 shuffle.c
,另外要加入註解 // cppcheck-suppress nullPointer
才能忽略警告。
新增 shuffle 指令
在 qtest.c
加入
do_shuffle()
console_init()
中新增命令結果呈現
使用 lab0-d 提供的程式碼測試亂度
圖表顯示 shuffle 的結果大致符合 uniform distribution