為什麼公平。每條跑道拿到的陣列完全一樣:場次號碼、長條數、排列方式都相同。每一拍,還沒跑完的跑道都會重播同樣數量的真實操作,所以名次就是總操作數的高低。跑道上方的時鐘算的是操作次數,不是秒數,因為用 JavaScript 計時,量到的是你的瀏覽器,不是演算法。
什麼算一次操作。兩個元素比一次大小,或往陣列寫入一次,就算一次操作。交換等於寫兩次。合併排序會先把值複製到另一塊緩衝區,這些複製不算,但每個寫回陣列的值都算,就算它落回原本的位置也一樣。插入排序會先把要插入的值拿在手上,跟它比較算一次比較,放回去只有真的移動過才算一次寫入。打亂本身不計。
為什麼「慢」的排序也會贏。Big-O 講的是資料變多時工作量怎麼成長,不代表每一副牌都是這樣。這個氣泡排序實作只要有一趟沒交換就會提早結束,所以排好的資料只要 n − 1 次比較、零次寫入(64 根就是 63 次操作,和插入排序並列第一,選「已排序」就看得到);插入排序遇到每根長條都快到定位時,也幾乎不用做事。這裡的「幾乎排好」是:長條一開始依序排好,再把 max(1, n/10) 對相鄰的長條互換,所以最多只有 n/10 對順序是錯的,也沒有任何一根離定位超過 n/10 格。在這種資料上,我們試過的每一場(種子 1 到 200、16 到 256 根),插入排序都贏過另外五種,比較次數只比 n 多一點。隨機長條上,n² 的演算法一次都沒贏過。試試「幾乎排好」,再試「反向」。
為什麼這個快速排序實作會變慢。它拿最後一個元素當基準(Lomuto 分割法)。遇到已排序或反向的資料,每次分割都只切掉一個元素,比較次數剛好是 n(n − 1)/2,64 根長條就是 2,016 次,跟選擇排序一樣多。「只有幾種值」也會拖慢它,但沒那麼嚴重:值只有五種,一長串相同的長條會切得很歪,128 根時快速排序的比較次數是合併排序的 2.5 到 3.4 倍(種子 1 到 200)。實際的函式庫會把基準挑得更仔細,或在分割失衡時換演算法。
Big-O 一覽,以及這場賽跑沒量的東西。
| 演算法 | 工作量 |
| 氣泡排序 | O(n²);有提早結束的設計,最好情況 O(n) |
| 插入排序 | O(n²);最好情況 O(n) |
| 選擇排序 | 一律 O(n²),比較次數剛好是 n(n − 1)/2 |
| 合併排序 | 一律 O(n log n) |
| 快速排序 | 平均 O(n log n),最壞 O(n²) |
| 堆積排序 | O(n log n) |
這場賽跑不量實際花的時間、快取的影響,也不量記憶體。實務上的排序大多是混合式:V8 的 Array.prototype.sort 用 TimSort,C++ 的 std::sort 通常是 introsort。