contributed by < Mike1117 >
random_shuffle_array
list_for_each_entry_safe
構建的迴圈中:若將 list_move_tail
換為 list_move
,為何無法滿足 stable sorting?qtest
perf
等效能分析工具:
list_sort
在內排序實作的效能表現sqrtf
sqrtf
, rsqrt
, reciprocal
在 Arm assembly 的實作int_sqrt
程式碼到 Linux 核心git log
以得知 Linux 核心開發者變更程式碼的考量因素解釋上方程式碼運作原理
返回鏈結串列的 size
依序設定每個節點的 value 為 i ,清除 next 的指向,清空 head 使鏈結串列為空。
依序測試將節點插入到鏈接串列的開頭、尾端。
執行測試 test_list
,若失敗會回傳錯誤訊息
主函式,負責執行測試、輸出測試結果以及執行的測試數量。
for (p = &l->head; *p != before; p = &(*p)->next)
此 for 迴圈的作用為找到指向 before
的指標
故初始條件應設為 p = &l->head
,即從鏈結串列的頭節點開始。
退出條件則設為 *p != before
,即 p
指向 before
的指標時退出迴圈。
每次迴圈執行時, p
都會被更新為下一個節點的 next
指標。
找到指向 before
的指標後,先將 *p
之值更新為 item
,即將指向 before
的指標更新為指向我們欲插入的 item
。
再將 (*p)->next
更新為 before
,將 item
的下一個節點設為 before
。
在現有的程式碼基礎上,撰寫合併排序操作
在撰寫此份作業時,我已使用合併排序完成 lab0 中的 q_sort
實作,所以此處之合併排序修改自 lab0
。
需要注意的是 lab0
中的鏈結串列為具有 Dummy head
之雙向鏈結串列,而此處觀察 list.h
中的串列結構,可以發現是具有 Dummy head
之單向鏈結串列,故需要在 lab0
的程式碼上再稍作修改,使其可以正常排序單向鏈結串列。
合併排序是一個採用 Divide and Conquer
的典型應用,故此實作的第一步便是要先實現 Divide
。我們需要將鏈結串列的長度在每次遞迴中縮短一半,較為有效的方法便是使用快慢指標來獲取位於串列中間之節點的位址,如此即可在不斷遞迴中將整個串列 divide 成單節點的串列。
第二步則是需要將已被「打散」的鏈結串列作合併,而合併的同時則是需要比較各節點值的大小,值較小的先合併。這樣每次遞迴回傳的鏈結串列都是 sorted 的。
實作中則將整個合併排序的流程分為兩個函式,merge_sort
會利用快慢指標將鏈結串列不斷分為長為目前一半的子串列,直至剩下單節點後,再透過呼叫 merge_two_list
來依序合併子串列。
而 merge_two_list
中則使用了教材 中提到的指向指標的指標這一方法,將兩個子串列中的節點依大小串接起來,直至其中一串列變空,最後再將剩餘的節點接至已合併串列的尾端。
可以看到填空部分上方的註解中提到 This is the rightmost node in the left subtree
。
即在下方這個 while 迴圈中,我們需要找的是目前此左子樹的最右節點。
故 while 的條件應設為 (*pred_ptr)->r
,當前節點有右節點時繼續迴圈。
而 while 內則是不斷將 pred_ptr
更新為 &(*pred_ptr)->r
,即右節點。
解釋上方程式碼運作的原理,並嘗試補完程式碼,使其可獨立執行,應提供相關的測試程式碼
此函式的 input 為 root
及欲尋找之節點 target
,由註解可知 The free tree is a binary search tree
,故尋找目標節點只需比較與當前節點之大小,當前節點較 target
大則向左,反之則向右,直至找到 target
的 pointer 再返回。
此函式的 input 為 root
及欲尋找 predecessor 之 node
。
而 Predecessor 的定義為:以中序 traverse 時, node 的前一個節點。
所以在 node
有左子樹時, node
的 predecessor 就是 node
的左子樹中最大的節點。
而我們看到在 remove_free_tree
中,呼叫 find_predecessor_free_tree
前,函式會先判斷 if ((*node_ptr)->l && (*node_ptr)->r)
。故在呼叫 find_predecessor_free_tree
時,該 tree 必定有左子樹,所以我們的函式可以簡單地寫成尋找左子樹的最大值,即左子樹之最右節點。
以下是給出的測試程式:
該程式先定義一個函式 insert_free_tree
,用以建立 BST。再定義一個函式 inorder_traversal
用以 print 出目前 BST 的 inorder traversal 。
而在 main 函式中則先建立一個 BST 後 print 出他的 inorder traversal ,以前述函式 remove_free_tree
移除一特定節點後再檢驗移除的節點是否正確。
以下是該測試函式的輸出:
可以看到
參閱 memory-allocators,針對你補完 (及後續改進) 的記憶體配置器,進行效能評比,並解讀其效能表現
解釋上述程式碼運作原理
加入 value 為 n 的新節點。
清空整個 list 及其記憶體。
檢驗目前的 list 是否有嚴格遞增,無則返回 false ,有則返回 true 。
Shuffle array,有實作上的缺陷。
主函式部分,負責建立一個 array 並依序填入 0 - 99999 後進行 shuffle ,再用此 array 建立一個新的 list ,呼叫 quicksort 對 list 進行 sort ,校驗 quicksort 是否實作正確後釋放 list 及 array 的記憶體。
會返回 list 最尾端之節點的指標。
返回 list 的長度。
確保雙向鏈結串列的每個節點的 next
以及 prev
鏈結正確,且頭尾相連保證該串列是個 circuit
。所以prev->next = head
及 GGGG
兩行的作用為確保串列頭尾相連,故應填入 head->prev = prev
,使 head
的 next
指向串列的尾端節點。
該函式是將 Optimized QuickSort — C Implementation (Non-Recursive)
實作於雙向鏈結串列。
先利用 list_length
取得鏈結串列的長度後,建立了兩倍於長度的 陣列 begin
,用以存放待處理的的子串列,模擬遞回的堆疊。
而在外層的 while
迴圈中,L
及 R
分別表示當前要處理之子序列之頭和尾。
當 L
及 R
未指向同一 node 時,會選定 L 作為 pivot ,並在內層的 while 迴圈中比較當前 node 與 pivot 之 value 的大小,較大則加入 right ,較小則加入 left 。
最後再將 left pivot 及 right 依序放入 begin 。
而當 L=R 時,代表目前待處理的串列僅有單個節點,此時就直接將此節點歸入 result ,同時呼叫 i--
,在進入下一迴圈時處理堆疊中的下一個串列。
迴圈結束後會將 list 的 next 指向已經排序好的 result ,並呼叫 rebuild_list_link
保證鏈結串列的正確性。至此,便可在不遞迴的情況下完成 quicksort 。
研讀〈A comparative study of linked list sorting algorithms〉,分析針對雙向鏈結串列的排序演算法考量,並運用 Linux 核心風格的 List API 予以實作
解釋上方程式碼運作原理,改進 random_shuffle_array
也探討 list_for_each_entry_safe
建構的迴圈中,若將 list_move_tail
換為 list_move
,為何會無法滿足 stable sorting
qsort 所需要的比較函式,比較 p1 及 p2 的大小,若 p1 較大則回傳正數,較小則回傳負數,相等則回傳 0 。
計算 Array 的大小。
宣告一個大小為 256 的陣列存放隨機生成的數。
生成一個 0 ~ 255 之間的隨機數。
呼叫 getnum
生成兩個 8-bit 的隨機數,組合成一個 16-bit 的隨機數。
打亂陣列,原實作有缺陷,需在後續修改為 Fisher-Yates Shuffle
主函式,先向陣列中依序填入 0 ~ 255 間的證書,再透過 random_shuffle_array
打亂,並以此 Array 建立一鏈結串列。
隨後以 qsort 及自定義的 list_quicksort 分別對陣列及鏈結串列進行 quick sort 。
再以兩者 sort 完的結果一一進行比較,檢查結果是否相同,若過程中有 error 或最後的結果不同,則會輸出錯誤資訊。
random_shuffle_array
原函式的實作會造成 Biased Shuffling
,故此處需修改為正確的 Fisher-Yates Shuffle
實作。
在上述程式中,呼叫 list_move_tail
的地方有兩處
而這是 list_move_tail
及 list_move
的函式內容:
可以看到兩函式的差異僅在於 list_move_tail
呼叫的函式為list_add_tail
,而 list_move
呼叫的函式為 list_add
。
再繼續看到 list_add_tail
及 list_add
這兩個函式:
可以看到 list_add_tail
是將新的node接入串列的尾端,而 list_add
則是將新的node接入head之後。
假設我們有兩個具有相同值之 node , A1 及 A2 ,在 compare 之後若使用 list_move_tail
,在新的串列中他們的相對位置仍然會是 A1 在前, A2 在後;若使用 list_move
,兩節點之相對位置則會變成 A2 在前, A1 在後,不符合 stable sort 之定義。
將程式碼整合進 lab0 提及的 qtest
,並探討環狀雙向鏈結串列的排序效能,並善用 perf 一類的效能分析工具,探討包含 Linux 核心 list_sort 在內排序實作的效能表現並充分解釋
qtest
以 Python 撰寫自動化測試程式並生成圖表,可以看到兩者在執行時間上幾乎沒有差異。
解釋上述程式碼,並探討擴充為 (向上取整數) 實作,該如何調整並保持只用整數加減及位元操作
該程式先定義了兩個全域整數陣列 mask 及magic。 mask 用以遮蔽高位元、提取低位元。
而 magic 則是在最後剩下二位元時直接獲得 leading zero 的數量。
而函式主體部分則是根據傳入值 c 判定當前的遞迴層數,用以切割高位元及低位元。若高位元非 0,則只需計算高位元的 leading zero 。若高位元皆為 0 ,則再計算低位元的 leading zero 並加上高位元的 bit 數。
當傳入值 c = 3 時,代表當前的高低位元會分別剩下 2 bit ,此時只需使用 magic 陣列即可直接獲得 leading zero 的數量。
參照計算機結構測驗題 C 及其注釋,以 C 語言 (搭配 GNU extension) 撰寫不依賴除法指令的 sqrtf
,確保符合 IEEE 754 規格。對照 sqrtf, rsqrt, reciprocal implementations in Arm assembly
參照從 √2 的存在談開平方根的快速運算提到的「藉由查表的平方根運算」,以 C 語言實作,並比較不同手法的整數開平方根效能
解釋上述程式碼運作原理,應提供測試程式碼
初始化 hash map ,傳入值 bits 為 map 的大小。
為 map 加入新的鍵值,會先檢查要加入的 key 是否已在 map 中,若無再作加入。
釋放 hash table 及其內部所有節點的記憶體。
建立一個大小為 10 的 hash table , traverse 陣列中每一個元素,檢查目標值與其相減後的值是否存在 hash table 中,若存在,則可直接返回自己與 table 中對應值的 index 。
若不存在,則將自身的值加入 hash table 中,以利其他元素查找。
首先需要加入標頭檔及定義缺失的巨集 MAP_HASH_SIZE
查閱 Two Sum 的 Description ,要求為 return indices of the two numbers such that they add up to target
。
故測試主函式如下:
首先定義一函式 printArray
用以 print 結果。
其次提供 4 測試例,預期輸出為[0, 1],[1, 2],[0, 1],[].
最終輸出如下:
與預期結果相符。
進行《The Art of Computer Programming》(vol 3) 一書section 6.4 的 exercise 8,也就是證明 Theorem S
探討 Linux 核心中使用 hash table 的應用案例,至少二項,應當列出原始程式碼並分析,斟酌搭配 git log 以得知 Linux 核心開發者變更程式碼的考量因素