contributed by < LambertWSJ >
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 後,就能夠透過 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 到排序演算法。
排序的實做選擇合併排序,除了實做上較容易外,合併排序屬於穩定排序,在最差的情況下的時間複雜度扔有 的表現。
參考 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。
All nodes from @list are added to to the end of the list of @head.
It is similar to list_add_tail but for multiple nodes. The @list head is not
modified and has to be initialized to be used as a valid list head/node
again.
假設最後要和 sorted 合併的串列為 l2,而 l2 在 q_sort 的傳入的開頭為 head
qsort
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 提到回傳值的三種情況
• 0, if the s1 and s2 are equal;
• a negative value if s1 is less than s2;
• a positive value if s1 is greater than s2.
若 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() returns an integer indicating the result of the
comparison, as follows:
• 0, if the s1 and s2 are equal;
• a negative value if s1 is less than s2;
• a positive value if s1 is greater than s2.
回傳值與兩個數值相減來判斷誰大誰小的概念一樣,所以 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 有時候滿分,要開始讀論文才能把統計執行時間的函式修正確。
教材已寫清楚程式碼大部分的運作原理,搭配原始碼可以有效率的看懂程式碼,所以這裡改成做實驗還有驗證 paper
待整理
merge
原本的 merge 函式可以透過 indirect pointer 簡化原本 if-else 區塊相同操作的部份,其實就像在做 merge two sorted lists 一樣
merge_final
lab0 - Linux 核心的鏈結串列排序 何時合併的表格整理變數 count 在各階段時 pending 上的節點數量,如果是自己做實驗該如何得知 pending 每個子串列的數量,還有其節點的內含值?
首先想到的就是 codegen 生成 graphviz script,但是這麼做要在 list_sort.c 加入很多程式碼又要額外處裡字串問題,寫完後除了驗證正確性外,還要用 #ifdef
覆蓋額外的程式碼,導致原本乾淨有品味的程式碼被我弄得一團亂。
要解決上述問題,可以使用強大的 GDB 搭配 Python API 在執行時期拿到 pending 串列,並用 Python 完成 codegen、存檔和畫圖,既避免弄髒程式碼又能夠練習 GDB。
靈感來自手写红黑树, 再用gdb画给你看
dump_sort 用來做這次實驗的命令,格式如下
dump_sort list [-o png_path] [-type list_type]
list
(必填) : 傳入要觀察的串列開頭,預設是當作都傳入 pending 串列,也就是 prev 指向下個子串列開頭,next 指向子串列的下個節點,另一種則是原本的雙向鍊結串列
-type
(可忽略): pending 和 origin 兩種類型,如上所述
-o
(可忽略): 輸出的檔案名稱,如果有包含資料夾的路徑在裡面,會自動建立資料夾。只會輸出 png 檔,所以無論傳入的副檔名是 jpg 還是 raw 都會被改為 png
完整程式碼請參考 dump_sort.py。
要添加 gdb command 大致流程如下
This method is called by GDB when this command is invoked.
CLI Commands In Python (Debugging with GDB)
這次 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
可以印出這兩階段的結果。
0xdeadbeaf
,所以改成執行完 list_sort 的下一行下斷點並傳入 current->q
印出結果。測資:建立 01~16 個節點,打亂後交由 list_sort 排序。篇幅關係不放完整 cmd 上來
$cnt
產生流水號後面加上階段,dump_sort 做完後更新流水號並繼續執行下去(c, continue)。假設 shuffle 完的串列入下
執行完成後,圖片和 dot 檔會存入到 lab0/sort_step/,暫時還不知道怎麼用 convert 命令把一系列的圖片大小不一 png 檔轉成每個步驟階段的串列置中顯示的 gif 檔,這樣會更生動
上述測資分割完的 pending list 如下圖所示,圖形呈現參考〈Queue-Mergesort〉。
Note: 藍色箭頭為 prev,紅色箭頭為 next。
pending 每個排序好的子串列的數量都是 2 的冪,充分利用雙向鍊結串列的特性不需要用額外的空間配置 Queue,也符合教材的表格最終結果。
最後一次合併前的 pending 串列如下, 兩個子串列長度一樣。
遞增排序完的串列如下圖所。