contributed by < v0103
>
1
因為這裡使用的是單向鏈結串列,因此需要確保傳入的指標在該函式執行完依舊指向串列的頭,參考你所不知道的 C 語言: linked list 和非連續記憶體提到的 indirect pointer,該函式用 node_t **left
,而非 node_t *left
。由於 left
是指向 list
的指標,*left
是 list
,*left->next
則是 node1
,left = &(*left->next)
就會將 left
更新為 node1
,經過while迴圈不斷往後找就可以找到串列的尾。
list_length
函式要計算串列總長,跟上面的 list_tail
一樣,使用 while 迴圈不斷的找下一個 node
,解題思路跟上面一樣,所以 BBBB
會是 *left->next
。
quick_sort
原方法 Optimized QuickSort — C Implementation (Non-Recursive) 首先選定最左邊的數字為 pivot
,分別使用 L
由左往右移動,R
由右往左移動,在 LR 移動過程會把小於 pivot
的數字放到左側,大於的則放到右側,最後 LR 相遇後再把 pivot
放到該點,以此完成排序。再來就是對 pivot
右側做排序,右側排序完了再對 pivot
左側做排序。
有了原方法的排序概念接下來看看題目 quick_sort
和原方法有何異同。
這裡一樣使用 begin
和 end
作為堆疊,去儲存尚未排列好的序列。
一樣選擇 L
作為 pivot
,value
則是 pivot
的值。這邊多了一個 node_t *p
指向 pivot->next
目前還不知道其作用。
可以看到這是一個 while 迴圈,運行到 p==NULL
才會結束,每次會用新指標 n
來存取 p
所指向的節點,n->value > value ? &right : &left
這邊判斷該次迴圈所指到的節點是否大於 pivot
,大於則會執行 list_add(&right, n)
將該節點放到 right
這個串列,反之,則放到left
串列。看起來這個迴圈的作用就是走訪串列,並將小於 pivot
的節點放左側,大於 pivot
的節點放右側。因此可以判斷出 CCCC
為 p->next
。
n
指向 p
的位置後 p
指向 p->next
。
n->value
< pivot->value
,所以 n
要放到 left
串列。
下一輪迴圈 n
指到 3,因此將 3 也放到 left
。
最後會將原串列拆成三組串列。
有了 left
right
兩個串列,再來要將他們存起來。begin
end
會做為下一輪迴圈的 L
R
,因此如果 begin[i]
是 left
可以得知 DDDD
是 list_tail(&left)
,同理 EEEE
是 list_tail(&right)
。最後 i += 2
所以跟原方法一樣是先排序右側。
TODO : 用list API改寫,提出可避免最差狀況的快速排序實作
2
首先在閱讀題目時講到 minrun 的選擇策略時,有個令我疑惑的點。
顯然,選擇 minrun 的策略是 Timsort 的關鍵,其中一個實務上的方法是,取資料長度 N 的前 6 位,若剩餘位中任何一位被設置,則加 1。這主要是為了在處理隨機數列時,使得 N 能夠被 minrun 切分成 2 的冪的 run,以便在合併時達到最佳平衡。
其中,取資料長度 N 的前 6 位我一直看不懂是甚麼意思,google 後才知道是將資料長度 N 轉換為二進位表示,並取其前 6 位的意思。
以題目的例子 N = 2112為例,2112 換成二進位是 100001000000 ,取其前六位也就是 100001 = 33,剩餘的六位都是 0,因此選擇 33 作為 minrun。
我解這題的方法是先大略理解了 Timsort 的原理,然後按照前後的結構來處理問題,先填補空缺的部分,最後再完全理解每個函式的作用。
首先,我看到了 timsort
函式,首先將原本的雙向鏈結串列轉換為單向的,然後使用 find_run
函式將原始串列劃分為多個 run。在這個拆分的過程中,同時使用 merge_collapse
函式來確保堆疊上的 run 長度保持平衡。接著,使用 merge_force_collapse
函式確保 run 的數量少於 3。如果 run 的數量剩餘 2,則執行 merge_final
函式進行最後的合併。同時,裡面包含一個 build_prev_link
函式,將原本的單向鏈結串列轉換為雙向的。如果 run 的數量剩餘 1,則直接執行 build_prev_link
函式。
我覺得一個有趣的地方是,在 find_run
函式中,將 run 的長度存儲在 head->next->prev
中。這樣做的好處是,在進行合併時可以直接讀取兩個 run 的長度,而不需要額外記錄或者重新掃描一次。
AAAA
BBBB
CCCC
都在 merge
函式。
在 merge
函式中 tail
負責指向串列的尾部,讓下一次迴圈比較完的較小值可以知道要接在哪,所以 tail
初始值應該指向 head
,AAAA
為 &head
。
在這段程式碼中,當 a
小於 b
時,在第一輪迴圈中,會將 *tail
,也就是 head
,設定為 a
。接著,為了確保下一輪迴圈中較小的值能夠接在 a
後面,我們需要更改 tail
的指向位置。因此,BBBB
為 &a->next
。
同理,CCCC
為 &b->next
。
前面有提到build_prev_link
函式的作用是將原本的單向鏈結串列轉換為雙向的。根據註解可以得知這兩行的作用是執行最後一步,讓串列的頭尾相接,因此 DDDD
為 tail->next
, EEEE
為 head->prev
。
TODO : 提出改進方案,整合進lab0-c
1
2
3
要找出第 N 個設定的位元,要先能找出第 1 個設定的位元,所以看到 __ffs
函式,使用多個 mask 逐步找出 word
中第 1 個設定的位元。我挑其中一個來講解。
下面條件若成立,表示 word
第 1 個設定的位元不在後 16 位,所以 num
計數要加 16 並且把 word
右移 16 位,檢查更高位元。
知道怎麼找第 1 個設定的位元,要找到第 N 個也就沒問題了。
fns
函式使用 while 迴圈,找到 word
第 1 個設定的位元後,再使用 __clear_bit
清除該位元,下一輪迴圈找到的就會是第 2 個設定的位元,以此循環便能找到第 N 個。
前面運行原理舉的例子是檢查後 16 位,mask 是 0xffff
,所以這個檢查後 32 位的 mask AAAA
是 0xffffffff
。
從 fns
函式可以知道,nr
是第 1 個設定的位元的位置,BIT_MASK
的作用是把 nr
的位元設為 1 其他設為 0,所以要將 p
的第 nr
個位元位元清除,BBBB
會是 ~mask
。
如果 size
> BITS_PER_LONG
時,會執行此巨集。