contributed by < yourui1017
>
首先先針對遮蔽的部分進行推敲。
可以看到函式的目的是要找出 list 的最後一個 node,因此在指標的指標 left 還沒指到 NULL 之前,left 都應該持續往下一個節點移動,故 AAAA = (*left)->next。
可以看到函式的目的是要找出 list 的長度,因此在指標的指標 left 還沒指到 NULL 之前,left 都應該持續往下一個節點移動,故 BBBB = (*left)->next。
該段程式碼推測是想要用 left 或 right 指標指向當前的節點(若比 pivot 小,則 left 會指向該節點,相反亦然。),且因為 list_add 會改變當前節點指向的位置,因此需要用 p 指標儲存下一個節點位置,故 CCCC = p->next
在 2024q1 第 1 週測驗題 中有提到,begin 和 end 這兩個矩陣是為了用堆疊模擬原本的遞迴呼叫。
其中,begin 依照順序會儲存 left、pivot 和 right 的首指標,而 end 應該要儲存 left、pivot 和 right 的尾指標,故 DDDD = list_tail(&left)
EEEE = list_tail(&right)
重複以上步驟,直到堆疊中沒有東西即可完成排序。
問題:
max_level
在單向鍊結串列的 quicksort
中,最差狀況下的 max_level 會是 2*n-1
,因為在堆疊中會儲存到 NULL,導致空間的浪費。
pivot
使用 Linux 核心風格的 List API 改寫
quick_sort
使用 List API 改寫,精簡程式碼並增加易讀性。實驗結果
使用 perf stat --repeat 5 -e cache-references,instructions,cycles ./main
指令計算效能
將 pivot 設為第一個節點
隨機選取 pivot
發現結果其實沒有變比較好,推測是因為鍊結串列每次在選取隨機點時需要走訪節點,且此走訪的時間會超過 worst case 搜尋的時間,導致效能下降。
延伸問題:
首先先針對遮蔽的部分進行推敲。
這邊宣告了 head 指標和 tail 為指標的指標,並考量到下面的程式碼是讓 tail 在該串列中進行走訪,故 AAAA = &head。
這邊的程式碼是要讓 tail 根據比較的結果指向 a 或 b 指標,並往下個指標移動,故 BBBB = CCCC = &(*tail)->next
build_prev_link 函式是要讓指標根據當前的排列順序指向正確的 link,故 DDDD = tail->next,EEEE = head->prev。
這邊是要判斷 stk_size 的大小,如果只有一個以下的 link 就不需要進行 build_prev_link,故 FFFF = 1。
因為篇幅的緣故所以又開了一個筆記 Timsort 筆記 紀錄學習過程。
進行這段的程式碼。
執行第一次的 while loop ,尋找下一個 run ,把該 run 的長度儲存在 run_head 的 next 的 prev 中,並且讓 run_head 的 prev 指向 NULL 。
除此之外,還宣告了 pair 的結構體儲存當前 run 的 head 和待尋找的節點位置。
進行第二次的 while loop ,尋找下一個 run ,並且因為 stack 的長度滿足 \(B>C\) ,所以不合併。
進行第三次的 while loop,尋找下一個 run 。
進行第三次的 while loop 。
因為堆疊的長度不滿足 \(A>B+C\) 所以將 stack[1] 和 stack[2] 合併,完成 merge_collapse
,跳出 while 。
因為已經搜尋完所有的節點,且堆疊只剩下 stack[0] 和 stack[1] ,故將它們合併。
執行 merge_force_collapse
。
完成合併,將 prev 指標接上,完成 timsort。
在上述的示意圖中可以發現在此程式碼中並沒有規定 Minrun 的大小,run 的大小都是根據升冪 / 降冪的數量決定,導致 run 的數量不固定,無法確保分組接近 2 的冪。
另外,合併的方式也只有使用到合併排序,因此如果輸入資料大量的連續升冪 / 降冪將會導致效能低落,故可以實作 galloping mode。
TODO:
首先,先計算該鍊結串列的 minrun ,其中 minrun 的大小不得超過 64 ,否則會使 insertion sort 的效能低落。
在 find_run
中加入 insertion sort ,使 run 的大小達到指定的 minrun 。
使用 perf stat --repeat 3 -e cache-misses,cache- references,instructions,cycles ./main
針對隨機資料進行測試。
未使用 minrun :
使用 minrun :
可以觀察到加入 minrun 後, run 的數量都會維持在略小於 2 的冪,這樣一來就會使合併的效能提昇。
實作 exponentioal search 並在 Timsort 中加入 galloping mode。
因為 exponentioal search 的效率取決於能夠隨機訪問元素,但對於鍊結串列來說,因為需要走訪所有的節點,導致隨機訪問的效率較低,因此判斷即使加入 exponentioal search 反而可能會讓整體 timsort 的效能降低。
將改進過的 Timsort 實作整合進 sysprog21/lab0-c,作為另一個有效的排序命令,應設計效能評比的測試程式,確保測試案例符合 Timsort 設想的資料分布樣式。
參考 csotaku0926 同學的方法進行效能測試的實驗。
詳細請看 commit40fb4 。
延伸問題:
首先先針對遮蔽的部分進行推敲。
讓新增的 node 指向原本 h->first 指向的位置。
針對對應的 heads[hash] 去找內部的 inorder tree 。
使用 container_of 找到包含此 node 的 order_node 。
針對對應的 heads[hash] 加入 node 。
Construct Binary Tree from Preorder and Inorder Traversal 原理解釋
本測驗的目的是藉由輸入樹的前序和中序的序列還原完整的樹。
前序遍歷:順序是根節點、左子節點、右子節點。
中序遍歷:順序是左子節點、根節點、右子節點。
後序遍歷:順序是左子節點、右子節點、根節點。
因此可以先透過前序序列找到根節點,並且找到根節點對應到中序序列的 index 。如此一來,就可以藉由中序序列找到根節點、左子樹和右子樹。
並且使用 dfs(Depth-first Search) ,依據順序找到根節點、左子樹和右子樹,在此程式碼中是使用遞迴實作 dfs 。
示意圖
因此可以找到根節點、左子樹和右子樹。
根節點:3
左子樹:9
右子樹:15、20、7
接下來就可以針對左右子樹找根節點、左子樹和右子樹。
最後就可以建立原始的樹如下圖:
雜湊表示意圖
另外,因為本測驗是基於鍊結串列做實作,因此為保證查閱時間為 O(1) ,使用 hash table 儲存中序序列,以便在 dfs 函式中可以使用前序序列查閱。
TODO
延伸問題:
LRU 原理解釋
Least Recently Used (LRU) 的核心觀念就是將比較常用到的資料放置在記憶體空間的前面,減少對磁盤的存取,讓讀取的速度可以更快。
LRU 使用的是先進先出的概念,所以如果佇列滿了,則會將 tail 的 Node 移除,並在 Head 加上新使用的 Node 。如此一來,佇列就會儲存最近被使用到的資料。
示意圖
其中 count
代表目前 Cache使用的大小、 capacity
代表 hhead 的大小。
除了使用雙向鍊結串列連接以外,還有使用到 hash table 儲存鍊結,這樣一來就可以加快存取鍊結的速度。
程式碼解釋
lRUCacheGet 會將 key 模除得到 hash值,並且根據 hash 值去找對應的 hash table 中的 CacheNode ,如果找到話就回傳該 key 對應的 value。
藉由存取 hash table 加快鍊結串列的存取速度。
lRUCachePut 則是會先判斷 Cache 的容量是否已經滿了。
此處的 hash function 都是使用模除的方式,但針對不知道 key 的範圍以及分佈情形,適用 Multiplication Method 。
TODO:
延伸問題:
原理解釋
參考自 yuyuan0625
__ffs()
是用來查詢一個 word 中最低位 1 的所在位置, 若 (word & 0xffffffff) == 0 即可知道較低的32位元內沒有任一位元為1, 因此將 word 右移 32 位元再進行檢查。
此段程式碼是透過 BIT_MASK(nr) 產生出只有第 nr 位為 1 ,其他位皆為 0 的遮罩,並利用此遮罩將該 addr 的第 nr 位清除。
此段程式碼目的為找到第 n 個被設為 1 的位置,它會不斷地呼叫 __ffs 找到最低被設為 1 的位元,若還不是第 n 個就會使用 __clear_bit 將目前的位元清除,再繼續找下一個被設為 1 的位元。
運作原理:
先利用 small_const_nbits
比較要找的位數是否超過限制的 size
, 並利用 GENMASK(h, l)
將 h 到 l 間的位元變為 1和 addr
做 & 運算 ,若 val
有值代表 addr
h 到 l 間有位元被設為1,因此再利用 fns 找到為 第 n 個被設為 1 的位元並回傳其位置。若不符合 small_const_nbits
就利用 FIND_NTH_BIT
來處理。
FIND_NTH_BIT
能夠搜尋超出單個 BITS_PER_LONG
長度的範圍。如果要查找的位元不在目前處理的字節中,能透過迴圈繼續在下一個 BITS_PER_LONG
長度的區塊中搜尋,直到找到該位為止。
因為 size
可能無法被 BITS_PER_LONG
整除,因此搜尋到最後一輪時應避免找到超出 size
範圍的字元,因此使用取餘數的方式找到最後一個字節所需要搜尋的字元數量。
TODO: