or
or
By clicking below, you agree to our terms of service.
New to HackMD? Sign up
Syntax | Example | Reference | |
---|---|---|---|
# Header | Header | 基本排版 | |
- Unordered List |
|
||
1. Ordered List |
|
||
- [ ] Todo List |
|
||
> Blockquote | Blockquote |
||
**Bold font** | Bold font | ||
*Italics font* | Italics font | ||
~~Strikethrough~~ | |||
19^th^ | 19th | ||
H~2~O | H2O | ||
++Inserted text++ | Inserted text | ||
==Marked text== | Marked text | ||
[link text](https:// "title") | Link | ||
 | Image | ||
`Code` | Code |
在筆記中貼入程式碼 | |
```javascript var i = 0; ``` |
|
||
:smile: | ![]() |
Emoji list | |
{%youtube youtube_id %} | Externals | ||
$L^aT_eX$ | LaTeX | ||
:::info This is a alert area. ::: |
This is a alert area. |
On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?
Please give us some advice and help us improve HackMD.
Syncing
xxxxxxxxxx
2024q1 Homework1 (lab0)
contributed by < LambertWSJ >
作業說明
實作 Queue 介面
q_insert_head
/q_insert_tail
將節點插入到 queue 的頭尾。可以利用 doubly linked list 的特性簡化成傳入 queue 頭尾的節點,之後再用 list_add 插入新節點。
list_add(node, head)
: node 插入到 head 後面假設 queue 有 aa, bb, cc 三個節點,要新增的節點為 dd,如下所示
插入開頭
list_add(dd, head)
,將 dd 插入到 head 後方,效果相當於list_add_tail(dd, aa)
由於大部分的操作相同,所以只需要
q_insert_node
來處裡插入頭尾的函式即可。q_remove_head
/q_remove_tail
從 queue 的頭尾移除節點也能夠簡化成只使用一個函式來處理,利用雙向串列的特性傳入頭跟尾的 list_head 即可,從串列拿掉
node
後,再把el->value
複製到 sp 上用來比對答案。q_reverse
/q_reverseK
q_reverse
串列反轉的操作較為簡單,只需要將每個節點移動到 head 前面即可達成。這裡要使用 safe 版的 for 迴圈,確保節點在移動後, node 可以取得下一個節點,否則永遠會停留在第一個跟第二個節點,不斷的在循環。
如果在 safe 版的 for 迴圈中使用 list_move_tail 會發生什麼事? 那只會不斷的把當前的節點,往後面移動,那麼 node 將永遠都到不了 head,一樣會遇到無窮迴圈。
上述的迴圈可以輕鬆改寫成遞迴,不過反轉的節點太多會導致 stack overflow
除了單向的實做,亦可以利用雙向鍊結串列的特性,頭尾兩兩交換,概念類似一樣的技巧用在陣列反轉。
list_for_each_safe 由上到下的判斷式,依序考慮下面兩種 case
節點數量為偶數個時,則必然會遇到 cur 的下一個節點就會是 last,表示其它節點已經兩兩交換完畢,剩下彼此還沒交換位置。
節點數量為奇數個時,則 cur 與 last 必然會重疊,重疊時表示所有節點都已交換完畢,可以結束迴圈。
其中
list_swap
的實做如下,考慮到可能是前後節點交換,所以 e2 和 e1 交換完後,可以檢查 prev 是不是 e1,如果是的話表示 e1 與 e2 是鄰近節點,e2 搬動完後也就完成交換了,不必再做一次list_move
。三種不同的實做要拿來測試的話,可以在加入 option 變數來決定使用哪個實做來進行測試
有了 option 後,就能夠透過 rev_opt 來查表選擇實做,這裡使用 typedef 簡化函式指標,用來提昇程式碼的可讀性
q_reverseK
題目大意為每 k 個節點就將串列反轉。
實做時,卡在 reverse 後,怎麼指向下個 head? 去年參考 komark06 的寫法後,才知道指向 safe 的前一個節點當做 head。
直觀的做法就是
anchor
來記錄要反轉的子串列的頭,再用另一個變數去紀錄迴圈是不是走了 k 次,是的話就將走過的這一段串列反轉,反轉的作法是將走過 k 的一段串列切割出來給q_reverse
反轉,完畢後再放回去。anchor
接上反轉後的子串列,要將anchor
更新到safe
的前一個節點,當作下一輪反轉的子串列的list_head
。有了不同的反轉實做,就能夠將 reverse_op 帶入到 q_reverseK。
q_ascend
/q_descend
題目大意為刪掉串列的解點,使串列的值從右手邊觀察是嚴格遞增。單向串列的方法是透過遞迴讓 head 走訪到最後一個節點再比大小,因為題目要嚴格遞增,從最後一個節點開始記錄最大值,如果節點的值比最大值還要小就刪掉,比最大值還大就紀錄節點的內涵值,遞迴結束後就可以得到題目要求的串列。
我們可以利用雙向串列的特點,反過來走訪串列,並重複上述比大小的過程,就不用寫成遞迴了,也避開了 stack overflow 的風險。
由於是從從右手邊觀察,又要刪掉節點,所以用 macro 把迴圈簡化成
list_for_each_safe_reverse
與
q_descend
解法一樣,差別在於改成從左手邊觀察,所以只要把迴圈換成list_for_each_safe
即可。q_swap
將串列的節點兩兩交換,使用 for each 走訪串列,將下個節點移動到當前節點的尾,如果下個節點是 head 就停止,head 不是用來交換的而是用來找到串列的本體。
呼叫
list_move(node->next, node)
,node2 跟 node1 交換下一輪迴圈 node 指向 node3,依此類推將 node3 跟 node4 交換,直到 node 的下個節點是 head 就停下。
實做 q_reverse 的過程中,也順手實做了
list_swap
,可以用來替代list_move_tail
,暫時沒想到好的函式名稱,先命名為q_swap2
q_delete_mid
刪除 queue 中間的節點,可以使用快慢指標的技巧來尋找中間的節點再刪除即可。
q_delete_dup
從單向串列的解法改寫過來,如果遇到是重複的就刪除掉節點,並將 dup 設為 true,如果當前節點是連續節點的最後一個,就用 dup 來確認是不是最後一個連續節點,是的話就刪除,由於刪除會拿掉節點,所以要用 safe 版的 for each 迴圈。
q_sort
按照註解描述會有遞增與遞減排序的 list,因此要加入對應的 compare function 到排序演算法。
排序的實做選擇合併排序,除了實做上較容易外,合併排序屬於穩定排序,在最差的情況下的時間複雜度扔有 \(O(logN)\) 的表現。
參考 2021 的測驗題 修改的遞迴版 merge sort。
5 ~ 10 行:divide 階段,將串列分割成左右兩半。用快慢指標的技巧找出找出左半邊串列的結尾
slow
。11 ~ 13 行:將
head
到slow
的串列用list_cut_position
切來並用left
作為開頭的 list_head,此時 head 為右半邊的串列開頭。list_cut_position(&left, head, slow)
分割完後如下圖所示14 ~ 18:呼叫 q_sort 將串列分割成 sorted lists 後,用
merge_tow_lists
把兩個串列合併到sorted
上。merge_two_lists
merge_two_lists
則參考過去使用間接指標的實做,改成 list API 的版本。只要l1
和l2
還有節點,就用 compare function 決定下個要合併的節點移動到sorted
後面,當其中一個串列合併完了就把令一個串列拼接在 sorted 後面。依據
descend
判斷遞增與遞減,可以建表來紀錄兩者對應的比較函式,用以取代三元表達式(?:
)的分支判斷。為什麼最後要使用
list_splic_tail_init
而不是list_splic_tail
?list_splice_tail(list, head)
的註解表示list
開頭的 head 會被加入到head
這個串列,所以後面要使用到 list 的話要初始化它的 head。假設最後要和 sorted 合併的串列為 l2,而 l2 在 q_sort 的傳入的開頭為 head
qsort
\(\to\)merge_two_lists(&left, head, &sorted, descend)
呼叫
list_splice_tail(head, sorted)
,head
並沒有被拿掉,而是留在sorted
串列裡面。這導致之後呼叫
merge_two_lists
的時候,會陷入無窮迴圈,迴圈判斷式list_empty(head)
永遠為 false,因為head->next
永遠指不到自己。比較函式使用 strcmp 來比較字串,依據手冊的 strcmp 提到回傳值的三種情況
若 descend 為 true,字串要由大到小排序,也就是 s2 會優先加入到
sorted
串列裡面,因此 l2_val 比 l1_val 大時要回傳 1,反之則回傳 0,ascend 也依此類推。觀察上述比較函式的實,最後扔然要做分支判斷,該如何消除?
判斷式以 0 為基準,且
strcmp
為回傳有號數,可以利用 sign bit 即 MSB 來判斷數值是否為負數,在使用 not operator(!
)轉成 0 和 1,以減少 merge sort 的分支數量。為了之後引入 kernel 的 list_sort,顯然不能使用只有兩個參數的比較函式,合併時也不能僅用 true 和 false 來決定下個要合併哪個節點,而是要和 0 做比較,應此將 compare 函式修改成如下。
手冊 strcmp(3) 提到下方描述
回傳值與兩個數值相減來判斷誰大誰小的概念一樣,所以
strcmp(s1, s2)
回傳 -5,那麼 s1 與 s2 調過來strcmp(s2, s1)
就會回傳 5,可依此特性簡化升序與降序排列,合併函式就會依照正負數對調合併的節點。合併函式的函式指標型態改成和 kernel 的 list_sort 的 cmp 一樣,不過傳入到比較函式會變成 next,如果拿原本的比較函式,除了結果不正確,用在 list_sort 還會造成 Segementation fault。
q_merge
改寫自 yanjiew1 的解法,利用雙向 linked list 的特性,每次迭代的時候,將頭跟尾串接到 sorted 串列上,如果頭跟尾是同一個,表示已經走完串列了,就把頭拼接到 sorted 上(選擇尾巴也可以),合併完之後在透過
q_sort
合併成一條 sorted list。最後把 sorted 的串列接到 head 上。make test
有時後會 95/100 有時候滿分,要開始讀論文才能把統計執行時間的函式修正確。引入 list_sort.c
教材已寫清楚程式碼大部分的運作原理,搭配原始碼可以有效率的看懂程式碼,所以這裡改成做實驗還有驗證 paper
簡化程式碼,觀察輸出的指令
待整理
merge
原本的 merge 函式可以透過 indirect pointer 簡化原本 if-else 區塊相同操作的部份,其實就像在做 merge two sorted lists 一樣
merge_final
使用 GDB 觀察排序過程
lab0 -
Image Not Showing
Possible Reasons
Linux 核心的鏈結串列排序 何時合併的表格整理變數 count 在各階段時 pending 上的節點數量,如果是自己做實驗該如何得知 pending 每個子串列的數量,還有其節點的內含值?
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
Learn More →首先想到的就是 codegen 生成 graphviz script,但是這麼做要在 list_sort.c 加入很多程式碼又要額外處裡字串問題,寫完後除了驗證正確性外,還要用
#ifdef
覆蓋額外的程式碼,導致原本乾淨有品味的程式碼被我弄得一團亂。要解決上述問題,可以使用強大的 GDB 搭配 Python API 在執行時期拿到 pending 串列,並用 Python 完成 codegen、存檔和畫圖,既避免弄髒程式碼又能夠練習 GDB。
靈感來自手写红黑树, 再用gdb画给你看
新增 dump_sort 命令到 GDB
dump_sort 用來做這次實驗的命令,格式如下
list
(必填) : 傳入要觀察的串列開頭,預設是當作都傳入 pending 串列,也就是 prev 指向下個子串列開頭,next 指向子串列的下個節點,另一種則是原本的雙向鍊結串列-type
(可忽略): pending 和 origin 兩種類型,如上所述-o
(可忽略): 輸出的檔案名稱,如果有包含資料夾的路徑在裡面,會自動建立資料夾。只會輸出 png 檔,所以無論傳入的副檔名是 jpg 還是 raw 都會被改為 png完整程式碼請參考 dump_sort.py。
要添加 gdb command 大致流程如下
這次 demo 只用到 gdb 函式庫的
parse_and_eval
和execute
,兩者都會讓 GDB 執行傳入的字串,但是前者會回傳結果,後者一般情況下不會有回傳值,除非to_string=True
才會回傳結果。考量到程式的可讀性,要取值時
parse_and_eval
,其它都用execute
,具體使用方法如下程式碼的第 21 行走訪 pending 串列開始。先看 26 行,從 pend 拿到子串列 list 的開頭後走訪子串列,節點的內含值使用包裝過
container_of
的 node_val 取得,31 行執行完後回傳的是element_t
的開頭,裡面的欄位用字典保存,因此要存取資料的話要傳入欄位名稱的字串,例val['value']
,回傳的結果放到 div_list,走訪完子串列,再將 div_list 加入到 sort_lists 以便後後續做 codegen 跟畫圖。要換到下個子串列的話就要存取
pending->prev
,也就是第 34 行的操作,如果沒有子串列就退出,有的話就重複上述過程,就能在 Python 的環境中使用整個 pending 串列。設置斷點
在 list_sort 的分割與合併階段下斷點,用來觀察兩階段執行完的 pending 串列。
分割與合併階段斷點的參考位置如下程式碼 highlight 的位置,使用這兩處的 pending 串列傳入到
dump_sort
可以印出這兩階段的結果。原本想再 list_sort 的 merge_final 用 dump_sort 印出最終的排序好串列,但是這時候的 head 還沒有完全接好,要等到 merge_final 執行完,否則會存取到
0xdeadbeaf
,所以改成執行完 list_sort 的下一行下斷點並傳入current->q
印出結果。GDB 測試 command
測資:建立 01~16 個節點,打亂後交由 list_sort 排序。篇幅關係不放完整 cmd 上來
為了避免每次執行到斷點就要停下來敲命令,這裡使用 gdb 的 command 命令規定執行到斷點時要執行的操作。檔名使用
$cnt
產生流水號後面加上階段,dump_sort 做完後更新流水號並繼續執行下去(c, continue)。假設 shuffle 完的串列入下
執行完成後,圖片和 dot 檔會存入到 lab0/sort_step/,暫時還不知道怎麼用 convert 命令把一系列的圖片大小不一 png 檔轉成每個步驟階段的串列置中顯示的 gif 檔,這樣會更生動
上述測資分割完的 pending list 如下圖所示,圖形呈現參考〈Queue-Mergesort〉。
Note: 藍色箭頭為 prev,紅色箭頭為 next。
pending 每個排序好的子串列的數量都是 2 的冪,充分利用雙向鍊結串列的特性不需要用額外的空間配置 Queue,也符合教材的表格最終結果。
最後一次合併前的 pending 串列如下, 兩個子串列長度一樣。
遞增排序完的串列如下圖所。