contributed by < 56han
>
1
取得 list
尾端的 node_t
,先確認是否 left
且 left->next
不為 NULL
,若不為 NULL
則指標往下一個 node_t
指,即 left->next
,最後跳出 while 迴圈時剛好會指到 tail。
當 left
不為 NULL
時,持續往下一個 node_t
指,走完整個鏈結串列時,剛好可得鏈結串列長度。
while
迴圈持續往下一個指,將原本的鏈結串列分成 > pivot
和 < pivot
,因此 CCCC
可填入 n->next
或 p->next
。
接著判斷是否比 pivot
值還大,若有加入 right
鏈結串列,反之加入 left
鏈結串列。
使用 begin[i]
和 end[i]
紀錄 left
串列的頭、尾;
begin[i+1]
和 end[i+1]
紀錄 pivot
;
begin[i+2]
和 end[i+2]
紀錄 right
串列的頭、尾。
這裡可以使用 list_tail
子函式,取得尾端的節點。
使用鏈結串列實作 quick sort
,實際上的鏈結串列如下,引用 YangYeh-PD 的畫法。
觀察程式碼可知,並沒有使用到 left
和 right
,因此可設計鍵結串列如下。
在上面的例子可以發現,當 4 是 pivot 時,
大於 pivot : 5
小於 pivot : 3, 2, 1
經過以上程式碼後,就會變成下圖。
觀察整個 quick_sort()
函式,發現:
right
串列是否還有需要排序(因為i += 2;
),再去檢查 left
。以上面例子來說,
right
串列只有一個節點。因此將結果放入result
串列中,i--
再去檢查pivot
和left
串列。
begin
和 end
使用了 2 倍的 list_length
空間,去記錄 left
和 right
串列中的頭尾。pivot
都是取鏈結串列的第一個節點。begin
和 end
是否真的需要 2 倍的 list_length
空間?count
= 10
~ 100000
,觀察 begin
和 end
需要使用多大的空間。在 begin[i+2]
和 end[i+2]
後面加入
最後發現 begin
和 end
只需要 10x( 計算 size 是10的幾次方 ) 的空間,就足夠了。
pivot
,當遇到已排序成 ascending /descending order 的鏈結串列時,就會成為 worst case,如何以別的方式挑選 pivot
以避免 worst case 的發生?rand()
取一數,並和 head
做交換,取新的 head
當作 pivot
commit 9fdec7b
TODO:提出可避免最差狀況的快速排序實作,應設計效能評比的測試程式。
2
timsort
函式:
find_run()
找出下一個 run,並根據 run 的長度和目前的堆疊狀態,呼叫 merge_collapse()
觸發必要的合併。merge_force_collapse()
強制合併所有剩餘的 run。build_prev_link()
還是 merge_final()
來建立最終排序完成的雙向鏈結串列。merge
函式:合併兩個已經排序好的鏈結串列 a
和 b
。
它會比較兩個串列的元素,較小的元素會被放在合併後的串列前面。如果兩個元素相等,為了保持穩定性(stability),會優先選擇串列
a
的元素。
build_prev_link
函式:重建雙向鏈結串列的 prev
指標。
從
tail
開始,調整每個節點的prev
,讓它指向前一個節點。最後再將頭尾兩端的prev
和next
指標連接起來,構成一個環狀的雙向鏈結串列。
merge_final
函式:執行最後一次的合併,並建立完整的雙向鏈結串列。
函式會不斷比較
a
和b
的元素,較小的會被連接到tail
之後,調整對應的prev
和next
指標。一旦a
或b
的元素取完, 迴圈就會結束。最後,函式會呼叫build_prev_link()
,將b
剩餘的元素全部接到tail
之後,完成雙向鏈結串列的建立。
find_run
函式:在未排序的鏈結串列中找出一段遞增或遞減的子串列(run)。
它會從
list
開始,比較相鄰元素的大小,決定目前的 run 是遞增還是遞減。對於遞減的 run, 函式會同時進行反轉,讓 run 變成遞增。函式會持續比較元素,直到不再滿足遞增/遞減的條件為止。
最後回傳一個pair
,其中head
指向 run 的起點,next
指向 run 結束後的下一個節點。函式還會在 run 的第二個節點的prev
欄位中暫存 run 的長度。
merge_at
函式:合併指定位置 at 後面的兩個鏈結串列。
首先計算要合併的兩個段的長度總和,然後呼叫
merge()
將它們合併。最後,它更新合併後的鏈結串列的長度訊息,將前一個節點的 next 指針指向合併後的鏈結串列,並減少堆疊的大小。
merge_force_collapse
函式:在堆疊的 size >= 3 時,持續合併鏈結串列。
它比較 tp 前面兩個鏈結串列的長度,合併較小的那一個。這個過程會一直持續,直到堆疊的 size < 3。
merge_collapse
函式:盡可能地將相鄰的較短鏈結串列合併,從而減少鏈結串列中節點的數量,提高逐一走訪的效率。
確保堆疊上的 run 長度保持平衡。該函式主要檢查堆疊頂端的 3 個 run 是否滿足以下原則:
主要是根據堆疊中當前的鏈結串列的長度情況,來決定如何進行合併操作。它會根據以下條件來判斷:
條件一:
stack 的 size >= 3
且tp->prev->prev
的長度 <=tp->prev
的長度 +tp
的長度。
條件二:
stack 的 size >= 4
且tp->prev->prev->prev
的長度 <=tp->prev->prev
的長度 +tp->prev
的長度。
如果滿足條件一或條件二:
如果tp->prev->prev
較短,則呼叫merge_at(priv, cmp, tp->prev)
合併tp->prev
和tp->prev->prev
。
如果tp
較短,則呼叫merge_at(priv, cmp, tp)
合併tp
和tp->prev
。
如果上述兩個條件都不滿足:
如果tp->prev
較短,則呼叫merge_at(priv, cmp, tp)
合併tp
和tp->prev
。
如果tp
較短,終止合併過程,返回tp
。
TODO:實作 Galloping mode
TODO:將改進過的 Timsort 實作整合進 sysprog21/lab0-c,作為另一個有效的排序命令,應設計效能評比的測試程式,確保測試案例符合 Timsort 設想的資料分布樣式。
1
將新節點插入到雙向鏈結串列的 head 時,新節點的 next 指針應該指向的位置。因為我們是將新節點插入到 head,所以它的 next 應該指向原來的 head 節點。
hlist_for_each
用於逐一走訪 hash table 中特定鏈結串列的 head 指標。heads
是一個 hash table 的 heads array,每個元素對應一個鏈結串列,而 hash
是根據給定值計算出的 hash values,用於確定逐一走訪哪個鏈結串列。
獲取 hash table 中特定鏈結串列的 head 指標,用於將新節點插入到對應鏈結串列的 head。
此程式碼的用意是透過構建一個 hash table,有效地儲存和查詢 inorder 序列中的節點,從而加速以 preorder 和 inorder 序列重建二元樹的過程。
hlist_node
以圖像表示。
很多個 hlist_node
連接,則會形成下圖。
hlist_head
以圖像表示。
透過 node_add
函式:將 node 一個一個加入 hash table 中。
示意圖如下:引用 ShchangAnderson 同學的圖。
建立完成 hash table 之後,呼叫 dfs
函式以深度優先搜尋往下尋找 root ,利用 find
函式找到 root 並且回傳 idx
值,即為 root 的位置。以 root 又可以切成左、右子樹,遞迴呼叫 dfs
函式。
引用 ShchangAnderson 同學的圖。
TODO:
2
目的是在從 hash table 中刪除一個節點時,更新下一個節點的 pprev 指標,使其指向上一個節點的地址。
閱讀 Linux 核心的 hash table 實作 學到為何是 *next
和 **pprev
。
釋放 LRU 快取實例所占用的記憶體。
從快取中獲取給定 key 對應的值,如果存在則返回該值,並將該節點移動到鏈結串列的頭部 (表示最近使用);如果不存在則返回 -1。
將給定的 鍵-值對 (key–value pair) 插入快取中。
Case 1:如果 key 已存在,則更新對應的值
Case 2:如果 key 不存在且容量已滿,則刪除鏈結串列尾部的節點(最久未使用),然後插入新的鍵-值對。
Case 3:如果 key 不存在且容量未滿,則直接插入新的鍵-值對在鏈結串列的頭部。
不管 key 是新的還是已存在,對應的節點都會被移動到鏈結串列的頭部。
不理解第 20、21 行,實際上的意思是刪除第一個節點和加入新節點,但為甚麼 hlist_del
和 hlist_add_head
輸入參數都是 &cache->node
?
利用 lRUCachePut
及 lRUCacheGet
等函式實作 LRU (Least Recently Used) 快取的資料結構。LRU 快取是一種常用的快取演算法,它會根據資料的最近使用情況,自動釋放最久未使用的資料,以確保快取空間的有效利用。
LRUCache 結構體
引用 vax-r 同學的圖
capacity
:紀錄此 cache 最多能記錄幾筆資料count
:cache 當前紀錄的資料筆數dhead
:用來串接所有資料的雙向鍵結串列hhead
:處理碰撞使用的 hlist
結構體陣列hlist_node
以圖像表示。
很多個 hlist_node
連接,則會形成下圖。
在 Linus Torvalds 的 GitHub 中可見,例如:linux/mm/list_lru.c
。
以下程式碼主要是在處理 LRU list 時,將一個項目添加到特定節點的 LRU list中。
TODO:指出其中可改進之處並實作
3
目的是找出 word
參數中最低位的1所在的位置(從0開始計算)。
計算出 bitmask,然後找到該位所在的字,最後使用 *p &= ~mask;
來清除該位。
用於尋找第 n
個 bit 為1的索引。
此篇程式碼目的是在指定的記憶體空間找出第 N 個設定的位元。
不理解執行 find_nth_bit() 時,甚麼時候會需要 return FIND_NTH_BIT(addr[idx], size, n);
用來檢查一個數字 nbits
是否 <= BITS_PER_LONG
,並且> 0 的常數。
__builtin_constant_p(nbits)
是 GCC 提供的一個內建函數,用於在編譯時檢查 nbits 是否是一個已知的常數。
用於生成一個 bitmask ,其中從第 l
位到第 h
位(含)的所有位都是1,其餘的位都是0。
(~0UL)
: 這表示一個 unsigned long 的值,其中所有位都是1。~
是反運算符,所以取反就是全部 bit 都為1。
(1UL << (l))
: 這將數值1(也是一個 unsigned long) 左移l
位。左移操作會在右側補0,因此這個表達式生成了一個數,其中只有第l
位是1,其餘位都是0。
fns
函數的目的是找出 word
中第 n
個設定的位元。
不斷呼叫
__ffs
來找到最低位元的1,如果那不是我們要找的位元,它就使用__clear_bit
來清除這個位,然後繼續搜索下一個設置位。如果n
等於0,意思是我們找到了想要的位元,所以它返回當前的bit
。
如果word
為0(即沒有更多設置位),則返回BITS_PER_LONG
,表示沒有找到。
計算出 bitmask,然後找到該位所在的字,最後使用 *p &= ~mask;
來清除該位。
位於 include/linux/cpumask.h
的 cpumask_nth
可以用來取得 cpumask 當中第 n 個 CPU。
使用
cpumask_bits(srcp)
獲取 cpumask 以 bit 表示,然後使用small_cpumask_bits
作為搜尋範圍的大小,最後通過cpumask_check(cpu)
確保傳入的 CPU 編號是有效的。