contributed by < ccs100203 >
linux2021考慮一個單向 linked list,其結構定義為:
對該單向 linked list 進行 Quick sort,由小排序到大。
基於 Divide and Conquer 的方式進行,在每次的迭代中選定一個 pivot,將其餘數字與 pivot 比較,比 pivot 小的會放入 pivot 左邊,反之則在右邊。而兩段數列會進入下一次的迭代,一直到最後就能得到排序好的數列。
pivot 與其餘節點斷開,用 p 來保存後續的 list。left,大的放入 right,如此我們可以得到一個 pivot 以及兩條 list。由於 list_add_node_t 的特性,節點會新增在 list 的最前方,下面會說明。left 與 right 進入下一次迭代,一直到切出所有 pivot。list_concat 接上所有已排序好的節點。此函式的作用為將一個 node 加入 list 的前方。
node_t->next = *list;*list = node_t; 轉移 list 指向的位置,即可完成新增此函式的作用在於串接兩條 list。會先藉由一個 while 迴圈找到 *left 尾端的 NULL,將 right 放在其位置,就可以串接兩條 list,若 *left 一開始就是 NULL,則 right 會直接取代他。
right 放進 *left (也就是 tail->next) 的位置,就可以完成串接根據 random(3)
The random() function uses a nonlinear additive feedback random number generator employing a default table of size 31 long integers to return successive pseudo-random numbers in the range from 0 to RAND_MAX. The period of this random number generator is very large, approximately 16 * ((2^31) - 1).
又根據 Linear congruential generator
可以知道 random(3) 是透過此方式實做的,而 LCG 的公式為:
C 所使用的參數為下表
| modulus m | multiplier a | increment c | output bits of seed in rand() |
|---|---|---|---|
| 1103515245 | 12345 | bits 30..0 |
所以程式執行結果每次都相同的原因是因為每次都是從相同位置開始計算。所以最直觀的方式就是改變 seed 讓其計算的行為改變。所以可以在這邊引入 srand() 與 time
這可以將時間當作 random 的 seed,用來改變其亂數的規律。
但這會遇到一個問題,短時間內快速重複執行還是會得到相同的亂數,所以要設法解決這個問題。
後來找到 linux 中使用的 getrandom(2),會藉由 urandom 的 entropy 去產生亂數,使用這個方法解決了上面遇到的問題。
內文所提到的方法是以 swap 為主體,利用 L 與 R 去紀錄需交換的數量,再用 beg[] 與 end[] 作為 stack,用來紀錄比較的範圍。
由於原先給的程式已經能有效的劃分出:
leftright所以要做的就是如何進行後續第二層的 partition,以及要如何把 list 串接回來,這邊引入上述的 stack,得以儲存迭代進行的狀態。
stack 會用來儲存每次做完的 left 與 right,這可以用來取代原先需要遞迴的情況
由於 stack 的特性與存放順序的關聯,運作中的 stack 會如同下圖
通過這張圖可以看出,會先將 right 的部份優先做完,再去做 left 的部份,如同 DFS。那麼我就可以很直覺的在 node 走到 leaf 時 (也就是沒有連接下一個節點),開始將 node 放入 result,要注意到的是每次都要放在 result 的最前方,因為程式會從最大的值開始放,而我們需要將 list 從小排到大。
因為 pivot 加入 result 的時間點變了,所以必須要想辦法儲存,否則就會從 list 中消失
list_concat 將 pivot 接在 left 的最後面,將其保留下來。result,但因為原先的作法會將與 pivot 一樣大的節點放入 left,所以最後就會產生 left 裡面永遠有兩個相同大小的 ,程式就無法結束。right,就能夠將兩個 有效的分開。所以將 > 改成 >= 很重要。以下是完整實作:
TODO: 程式碼的 node_t *stack[MAX_LEVELS] 可換為類似 C++ STL vector 的實作,例如用 C 語言開發的 Vector,基本想法是:
這樣的話,可兼顧空間和執行效率。
另外,quick sort 本身尚可改進,Introsort 提出混合演算法,在資料量少的時候 (如 ),排序演算法改用插入排序,否則,使用快速排序。參考資訊:
MAX_LEVELS 開銷引用 Optimized QuickSort — C Implementation (Non-Recursive) 作者的說法
Since is greater than , and there are only about , fundamental particles in this universe from which a human-made computer can be built, no larger limit will ever be needed.
他所設立的 已經大於人造電腦可以建立出來的資料量,所以不會有觸碰到極限的問題。
我目前尚未能驗證他的說詞
也因為我是用一個 stack 去取代原先作者所使用的兩個 stack,所以我程式碼的 Max_LEVELS 在他的說法裡應設為 600。
compar透過 man qsort 得知其屬於 stdlib.h
TODO
引用 vector
將 stack 替代為下方的版本,在需要記憶體時執行 push, 並且使用完後就會 erase。
參考 IntroSort,顯示 C++ STL 的 sort 是基於 quicksort,並由 heapsort 和 insertion sort 組成。
加入 heapsort 的原因為,當 quicksort 迭代了 次之後還沒得到解,那就有高機率會得到 的時間複雜度。此時切換為 heapsort 能將其時間複雜度控制在 。
The reason behind this “fallback” is that if Quicksort was not able to get the solution after recursions for a branch, probably it hit its worst case and it is degrading to complexity .
加入 insertion sort 是為了優化此演算法,而作者表示這是經由經驗法則所得出的結論
The number 20 is an empiric number obtained observing the behaviour of InsertionSort with lists of this size.
看懂 introsort 後,在 quiz2 可挑戰實作 Timsort,說不定還可改進 Linux 核心的 list_sort.c
非遞迴版本的目前想不到方法把答案串起來,所以就變成實作遞迴版本的。
而 heapsort 還未實作。
2021/03/06
正在思考如何以最小的成本去實作 heapsort,單純的 linked list 效率很差,也不可能在不改動結構的情況下轉換為 Tree,目前想以新增一個 tree node struct 的方式進行。
2021/03/07
在想是不是改成混用 tree sort 會比較好,兩難。
參照 list.h,可以看到 linux 使用的 linked list,但是卻沒有定義所使用的
struct list_head,於是在 types.h 內找到了他的定義。
next 與 prev 都指向自己,配合後面插入節點的操作,就可以知道他屬於 circular doubly-linked list.void list_add() 為例:__list_add()__list_add_valid() 做一層驗證,再去進行 add 的操作。可以看出他是將新的節點插入在 prev 與 next 之間。值得一提的是,如果 CONFIG_DEBUG_LIST 為 False, __list_add_valid 會直接回傳 true
程式中所使用的 WRITE_ONCE(),其定義在 compiler.h 內
這巨集存在的原因,主要是 memory (re)ordering,你應該參照 Kernel documentation,摘錄對應的考量。
好的,研究中
根據 list.h
list_add()TODO
研讀 Ching-Kuang Shene (冼鏡光) 教授撰寫的 A Comparative Study of Linked List Sorting Algorithms,思考高效率的 linked list 排序演算法。
TODO