我是如何擊敗Java自帶排序演算法的

ImportNew發表於2015-09-05

Java 8 對自帶的排序演算法進行了很好的優化。對於整形和其他的基本型別, Arrays.sort() 綜合利用了雙樞軸快速排序、歸併排序和啟發式插入排序。這個演算法是很強大的,可以在很多情況下通用。針對大規模的陣列還支援更多變種。我拿自己倉促寫的排序演算法跟Java自帶的演算法進行了對比,看看能不能一較高下。這些實驗包含了對特殊情況的處理。

首先,我編寫了一個經典的快速排序演算法。這個演算法通過計算樣本的平均值來估計整個陣列的中心點,然後用作初始樞軸。

我借鑑了一些Java的思路來適當改進我的快速排序,修改後的演算法在對小陣列進行排序的時候直接呼叫了插入排序。在這種情況下,我的排序演算法和Java的排序演算法可以達到相同的執行時間量級。Wild & al 指出,如果排序陣列有很多的重複資料,標準的快速排序會比雙樞軸的快速排序要快。我沒有嘗試任何位元組或彙編級別的分析和優化。在大部分的問題中,我的版本的優化程式都遠遠不能跟Java系統程式相提並論。

我一直都想測試腦海裡的一個簡單的排序演算法,我稱之為Bleedsort。這是一個分散式演算法,它通過樣本抽樣方法對要排序的陣列進行分佈估計,根據估計結果把資料分配到相應的一個臨時的陣列裡(如圖 1 所示),並重寫這個初始的陣列。這是一個預處理過程,然後再應用其他的排序演算法分別進行排序。在我的測試中,我使用了我編寫的快速排序版本。如果使用合併排序應該會有更好的結果,因為合併排序被廣泛應用在高度結構化的陣列中。為了計算簡單,我只測試了分佈均勻的資料。

Bleedsort在遇到相同的資料的時候都會放到右邊,所以此演算法在排序相對一致(譯者注:會有很多重複資料)的陣列的時候表現很差。所以我需要對排序的陣列進行樣本估計,當重複數很多的情況下應避免使用Bleedsort演算法。

我很清楚,Bleedsort演算法在記憶體空間使用方面沒辦法跟歸併排序(快速排序)相提並論,臨時陣列也比原來的陣列要大四倍左右。同時其他的一些分佈排序演算法,比如Flashsort,在這方面也表現得要好很多。

擊敗Java排序演算法

圖1 Bleedsort舉例說明

我運用JMH來作為測試基準。為了簡單起見,我就用整形陣列進行測試。在1000.000 到10.000.0000 數量級的均勻分佈的陣列中,我的演算法表現的最好。儘管我寫的快速排序演算法在一定程度上比不過Java自帶的演算法,但是我的預處理過程很好的彌補了這些不足(呼叫了我的快速排序的Bleedsort 87ms vs Java 自帶演算法105ms; 938ms vs 1.144s)

Benchmark Mode Cnt Score Error Units Corrected

MyBenchmark._1e6U sample 8512 0.024 ± 0.001 s/op

MyBenchmark._1e7U sample 985 0.236 ± 0.001 s/op

我生成了下面這些正確的基準陣列

MyBench.int1e6UQuickSort sample 1641 0.131 ± 0.001 s/op 0.107 ± 0.002

MyBench.int1e6UBleedSort sample 2410 0.087 ± 0.001 s/op 0.063 ± 0.002

MyBench.int1e6UJavaSort sample 1978 0.105 ± 0.001 s/op 0.081 ± 0.002

MyBench.int1e7UQuickSort sample 200 1.483 ± 0.014 s/op 1.459 ± 0.015

MyBench.int1e7UBleedSort sample 373 0.938 ± 0.009 s/op 0.914 ± 0.010

MyBench.int1e7UJavaSort sample 200 1.144 ± 0.009 s/op 1.120 ± 0.010

所以,我的這個沒有特殊優化的演算法程式在這些資料集上要比Java自帶演算法快大概 10-15% 。

在1000.000資料級,包含 10% 或者 1% 的隨機重複資料的均勻增加資料集上,我的演算法表現的也不差。

Benchmark Mode Cnt Score Error Units Corrected

._1e6Iwf010 sample 20705 9.701 ± 0.033 ms/op

._1e6Iwf001 sample 148693 1.344 ± 0.003 ms/op

生成正確的基準陣列

.int1e6Iw010BleedSort sample 4159 49.377 ± 0.571 ms/op 39.68 ± 0.60

.int1e6Iw010JavaSort sample 3937 52.139 ± 0.229 ms/op 42.44 ± 0.25

.int1e6Iw010QuickSort sample 3899 52.457 ± 0.210 ms/op 42.76 ± 0.23

10% 重複資料

.int1e6Iw001BleedSort sample 6190 32.821 ± 0.219 ms/op 31.48 ± 0.22

.int1e6Iw001JavaSort sample 8113 24.910 ± 0.079 ms/op 23.57 ± 0.08

.int1e6Iw001QuickSort sample 8653 23.367 ± 0.056 ms/op 22.02 ± 0.06

^^ 1%

但是,這個演算法在只有10.000左右的小二項分佈的資料集 (~bin(100,0.5))(譯者加:考慮到括號裡面是公式程式碼,並沒有修改內部英文括號符號成中文符號)上表現的很差。 在這些陣列中,平均下來,出現50這個數字的次數是795.5,而出現40組重複陣列的次數是108.4。

同時,在排序1000.0000量級的大陣列的時候,這個演算法要比 Arrays.sort() 慢兩倍左右。這些陣列都有很多的重複資料(比如有的大小為1e6的陣列裡只有450個不同的數值)。

Benchmark Mode Cnt Score Error Units Corrected

._1e4bin100 sample 152004 1.316 ± 0.001 ms/op

^^ for correction

.int1e4bin100BleedSort sample 148681 1.345 ± 0.001 ms/op 0.029 ± 0.002

.int1e4bin100JavaSort sample 150864 1.326 ± 0.001 ms/op 0.010 ± 0.002

.int1e4bin100QuickSort sample 146852 1.362 ± 0.001 ms/op 0.046 ± 0.002

.int1e6bin1e4BleedSort sample 75344 2.654 ± 0.005 ms/op -

.int1e6bin1e4JavaSort sample 146801 1.361 ± 0.002 ms/op -

.int1e6bin1e4QuickSort sample 76467 2.615 ± 0.005 ms/op -

在排序小型的(10.000, 100.000)均勻隨機陣列下,這個演算法表現尚可,但是並不比系統演算法更好。

MyBench.int1e4UBleedSort sample 216492 0.924 ± 0.001 ms/op 0.683 ± 0.002

MyBench.int1e4UJavaSort sample 253489 0.789 ± 0.001 ms/op 0.548 ± 0.002

MyBench.int1e4UQuickSort sample 217394 0.920 ± 0.001 ms/op 0.679 ± 0.002

MyBench.int1e5UBleedSort sample 18752 0.011 ± 0.001 s/op 0.009 ± 0.002

MyBench.int1e5UJavaSort sample 22335 0.009 ± 0.001 s/op 0.007 ± 0.002

MyBench.int1e5UQuickSort sample 18748 0.011 ± 0.001 s/op 0.009 ± 0.002

總而言之,在記憶體不是很緊張的情況下,針對適當的大資料集,我會建議把分佈搜尋演算法做為一個有效的補充選項。

最後,讓大家來認識一下二項分佈的一些資料集 bin(100, 0.5) 和 bin(1000, 0.5),

這裡是兩個隨機抽樣了100個資料的資料集(使用R語言生成)。

> rbinom(100, 100, 0.5)

[1] 43 49 51 47 49 59 40 46 46 51 50 49 49 45 50 51 50 49 53 52 45 53 48 56 45

[26] 47 55 47 53 53 56 41 47 42 51 51 46 49 49 52 46 48 49 50 48 56 54 49 53 52

[51] 54 48 45 45 50 48 54 49 52 50 48 48 49 45 54 54 50 41 53 45 51 48 53 52 52

[76] 50 53 47 55 47 60 54 52 56 45 46 54 46 38 43 53 45 62 48 52 52 52 49 52 56

> rbinom(100, 1000, 0.5)

[1] 515 481 523 519 524 516 498 473 523 514 483 496 458 506 507 491 514 489

[19] 475 489 485 507 486 523 521 492 502 500 503 501 504 482 518 506 498 525

[37] 498 491 492 479 506 499 505 497 510 479 504 510 485 488 495 519 522 490

[55] 517 511 511 488 519 508 475 521 505 493 480 498 490 492 492 476 490 506

[73] 496 505 521 518 506 509 477 483 509 493 497 501 483 502 470 515 519 509

[91] 510 496 477 508 506 481 490 511 498 476

相關文章