從無重複大陣列找TOP N元素的最優解說起

藍貓163發表於2016-06-15

有一類面試題,既可以考察工程師演算法、也可以兼顧實踐應用、甚至創新思維,這些題目便是好的題目,有區分度表現為可以有一般解,也可以有最優解。最近就發現了一個這樣的好題目,拿出來曬一曬。

1 題目 原文:

There is an array of 10000000 different int numbers. Find out its largest 100 elements. The implementation should be optimized for executing speed.

翻譯:

有一個長度為1000萬的int陣列,各元素互不重複。如何以最快的速度找出其中最大的100個元素?

2 分析與解 (接下來的演算法均以Java語言實現。)

首先,第一個冒出來的想法是——排序。各種排序演算法對陣列進行一次sort,然後limit出max的100個即可,時間複雜度為O(nLogN)。

2.1 堆排序思路 我以堆排序來實現這個題目,這樣可以使用非常少的記憶體空間,始終維護一個100個元素大小的最小堆,堆頂int[0]即是100個元素中最小的,插入一個新的元素的時候,將這個元素和堆頂int[0]進行交換,也就是淘汰掉堆頂,然後再維護一個最小堆,使int[0]再次儲存最小的元素,迴圈往復,不斷迭代,最終剩下的100個元素就是結果,該演算法時間複雜度仍然是O(nLogN),優點在於節省記憶體空間,演算法時間複雜度比較理想,平均耗時400ms。

程式碼實現如下,

那麼挖掘下題目,兩個點是我們的優化線索:

1、元素互不重複

2、最快的速度,沒有提及對於系統資源以及空間的要求

2.2 多執行緒分而治之策略 順著#2條線索,可以給出一個多執行緒的優化版本,使用分而治之的策略,將1000萬大小的陣列分割為1000個元素組成的若干小陣列,利用JDK自帶的高效排序演算法void java.util.Arrays.sort(int[] a)來進行排序,多執行緒處理,主執行緒彙總結果後取出各個小陣列的top 100,歸併後再進行一次排序得出結果。

程式碼實現如下,

分析看來,這個解的優勢在於充分利用了系統資源,使用了分而治之的思想,將時間複雜度平均分配到了每個子執行緒中,但是程式碼中大量用到了System.arraycopy進行陣列拷貝,佔用記憶體過於多,甚至需要指定JVM的記憶體-Xmx才可以正常執行起來,平均耗時250ms。

2.3 點陣圖陣列思路 這個思路屬於比較創新的方式,考慮到優化線索#1提到的無重複元素,那麼可以使用點陣圖陣列儲存元素,一個int佔用4個位元組,32個bit,也就是說1個int可以表示32個數字的位置。 維護一個陣列長度/32+1的點陣圖陣列x,遍歷給定的陣列,將數字安插進入這個點陣圖陣列x中,例如int[0]=62,那麼

那麼就置點陣圖陣列的x[1]=x[1] | 30,採用“位或”是為了不丟掉以前處理過的數字。

程式碼實現如下,

這個演算法的時間複雜度是O(N),非常理想,平均耗時可以減少到50ms作用,效能比排序演算法提升了10倍以上,不足在於點陣圖陣列的長度取決於給定陣列的最大值,如果分佈比較平均,並且最大值比較小,那麼佔用記憶體空間就可以得到有效的控制。

3 總結

綜上給出的題目,可以看出解決一個實際問題,既可以用純演算法的思路來解決,我們甚至可以自己動手實現,例如自己寫的堆排序,非常節省空間,如果用JDK自帶的快速排序,那麼無疑這一點不會好於我們的實現。 現今,處理大資料問題一個傾向的思路就是充分利用系統資源,充分發揮多核、大記憶體計算型伺服器的能力,為我們提高效率,多執行緒是在JAVA中以及有了非常好用的API以及concurrent包下的工具類,能否有效利用這些工具提速我們的程式也很關鍵。同時,問題總有一些點可以讓我們找到最適合的場景來解決,例如點陣圖陣列的思路,在效能上達到了最佳,同時多消耗的記憶體對於現代的伺服器來說完全在可控範圍內,因此不失為一種創新的好思路。

相關文章