时间复杂度为O(n)的排序(JAVA)
時間復雜度為O(n)的排序
以下排序算法都是針對特定場景才有優勢的排序算法
桶排序
特定場景描述
以上場景一般具有以下特點:
-
要排序的數據有明顯的范圍。比如高考分數一般介于0~900,公民身高一般介于0~3000,公民體重一般介于0~150,訂單金額一般介于0~max(當年),而某一年的時間,如果把時間以時間戳形式計算,一定是介于某個固定區間的
-
既然有了明顯的范圍區間,就很方便劃分區間段(類似統計中的柱狀圖),而且區間段的數據滿足單調性,比如區間段f(x)和區間段f(x+1)一定滿足如下關系:max(f(x)[i]) < min(f(x+1)[i]),即上一個區間段內的最大值一定比下一個區間段的最小值要小(這里說的是升序排列)。比如高考分數可劃分的區間段是0~100、101~200、201~300、301~400、401~500、501~600、601~700、701~800、801~900,在區間段201~300中的最大值很明顯會小于區間段301~400中的最小值。
-
劃分好了區間段,還要滿足各區間段內的數據量相對均勻。還拿高考分數為例,意思是區間段201~300中的考生數和區間段301~400中的考生數和…幾乎相當。這個其實挺難的。因為很多類似的分布都會符合正態分布的,兩頭少,中間多,當然也有特定的情況,這完全是根據數據的業務屬性而來的。但各區間段內的數據量相對均勻是桶排序很關鍵的一點。
為了滿足各區間段內的數據量相對均勻,就需要根據實際業務場景,重新劃分區間段(桶)。
比如考生的分數,公民的身高和體重,一般來說符合正態分布,仍以分數為例,這需要參考以往經驗了,假設得到的經驗是,一般會在500~800之間扎堆兒,該區間考生數占據了所有的大約80%,那么根據這條經驗,可以做出新的桶劃分。比如:0~500、501~550、551~600、600~650、651~700、701~750、751~800、801~900,或許500~800還可以分的更細致些的。
比如微信的后臺日志時間戳,以天為單位的話,假設得到的經驗是,春節前后使用量很大,五一、十一也會很大。相應的調整新的桶劃分。
以上的目的就是盡量滿足各桶中的數據量均勻。
思路
以空間換時間
盡可能減少數據間的比較
設置好若干個桶之后,首先遍歷待排序數據,放入特定的桶中,時間復雜度是O(n),假設嚴絲合縫,也要開辟空間復雜度為O(n)的多個桶做臨時存儲
接著將各桶中的數據分別排序,如果需要穩定性(好像沒在排序里說明穩定性的意思,后邊單說吧),使用歸并排序,如果不需要穩定性,使用快速排序,總之單個桶的時間復雜度是O(klogk)。假設設置的桶足夠多,可以讓一個桶里的元素值相等,那就不需要桶內排序了。但這需要開辟的桶空間可就更大了(這也算是把“以空間換時間”發揮到極致了吧)。
最后按序遍歷各桶以及各桶中元素,依次放入原數組,完成排序。
實現代碼
比較糾結這個排序算法要怎么寫合適,網上也沒找到一個很通用的,畢竟設定幾個桶,桶的深度都要結合實際情況來看的。這里只能放一個簡單的例子,以及代碼實現。
全班10人,身高分別是[150,163,158,166,170,169,158,175,181,175],按身高升序排序
在執行排序前,得知身高最大值max=181,最小值min=150,設置m=3個桶,桶區間依次是150~160,161~170,171~181,每個桶的深度為5
下邊是偽代碼
計數排序
特定場景描述
場景和通排序的場景類似,只不過有了更苛刻的要求
-
要排序的數據不僅有明顯的范圍,而且數值范圍不大。比如桶排序列出的諸多場景中,分數、身高、體重都是很好的;金額可能有點邪乎,主要是如果土豪太多,雙十一一單花了好幾個億那種,區間就太大了,不過據說單個訂單有金額上限,如果這樣的話就很好了;按時間排序,這可能就有點頭大了,但如果不是按納秒,而是粗略一些忽略到毫秒(也難),忽略到秒(一年31,536,000秒,也難),忽略到分鐘(一年525,600分鐘,挺好),忽略到小時(一年8,760小時,挺好),再忽略估計這個排序的意義可能就喪失了。
-
要將被排序的所有元素按照某個一對一映射的函數,先計算成非負整數。比如身高和體重,身高169.55可以換算為16955,換算公式是f(x) = 100 * x;而x = f(x) / 100;兩個函數的計算結果都是唯一的。再比如時間排序,換算成時間戳,再減去一個待排序最小時間的時間戳,在允許的情況下忽略納秒、毫秒、甚至秒(這個逆向的映射有些麻煩,但既然忽略了一些精度,就相當于按更粗略的單位來排序了)。
思路
計數排序是特殊的桶排序,實現思路如下:
因為對數據值的范圍做了限定:一方面不大,另一方面都可轉為非負整數,那么就可以進行如下操作了。
假設數據值的最大值是max,接著設定max+1個桶,而這里的桶不用來存儲元素的集合,而用來存儲元素的個數,所以每個桶的長度為1。
所以可創建一個長度為max+1的數組充當這max+1個桶
接著遍歷原數組array,統計每個值的個數,并將結果保存到桶中
for(int i = 0; i < array.length; i++) {int item = array[i];temp[item] = temp[item] + 1; }舉個例子,數組array = {3,5,7,2,4,2,7,3,1}
其中最大元素是7,那么桶就是temp = new int[8]
遍歷array
最終得到temp = {0,1,2,2,1,1,0,2}
從數組temp可以看出,數組array中元素i的個數是temp[i]
而如果希望數組中元素值所表達如下含義呢?
從數組temp可以看出,數組array中<= i的元素的個數是temp[i]
數組temp要做如下的元素疊加處理
最終得到temp = {0,1,3,5,6,7,7,9}
從數組temp可以看出,數組array中<= i的元素個數是temp[i]
原array = {3,5,7,2,4,2,7,3,1},設置一個等長的臨時數組result = new int[array.length]
第一個元素3,得到temp[3] = 5,說明<= 3的元素個數是5,那么經過排序后的array中index = 4(第5個元素)的位置上放置的一定就是3,所以result[4] = 3。既然3的位置已經確定,<= 3的元素個數就是4了,需要執行temp[3] = temp[3] - 1
此時result = {0,0,0,0,3,0,0,0,0}; temp = {0,1,3,4,6,7,7,9};
第二個元素5,得到temp[5] = 7,說明<= 5的元素個數是7,那么經過排序后的array中index = 6(第7個元素)的位置上放置的一定就是5,所以result[6] = 5。同樣,temp[5] = temp[5] - 1
此時result = {0,0,0,0,3,0,5,0,0}; temp = {0,1,3,4,6,6,7,9};
第三個元素7
此時result = {0,0,0,0,3,0,5,0,7}; temp = {0,1,3,4,6,6,7,8};
第四個元素2
此時result = {0,0,2,0,3,0,5,0,7}; temp = {0,1,2,4,6,6,7,8};
第五個元素4
此時result = {0,0,2,0,3,4,5,0,7}; temp = {0,1,2,4,5,6,7,8};
第六個元素2
此時result = {0,2,2,0,3,4,5,0,7}; temp = {0,1,1,4,5,6,7,8};
第七個元素7
此時result = {0,2,2,0,3,4,5,7,7}; temp = {0,1,1,4,5,6,7,7};
第八個元素3
此時result = {0,2,2,3,3,4,5,7,7}; temp = {0,1,1,3,5,6,7,7};
第九個元素1
此時result = {1,2,2,3,3,4,5,7,7}; temp = {0,0,1,3,5,6,7,7};
最終得到數組result = {1,2,2,3,3,4,5,7,7}就是排序結果
再將其逐個復制到原數組array中即可
最終代碼
public void countingSort(int[] array, int length) {// 找最大值int max = array[0];for (int i = 1; i < length; i++) {if (max < array[i]) {max = array[i];}}// 創建統計元素個數的數組int[] temp = new int[max + 1];for (int i : array) {temp[i] = temp[i] + 1;}// 疊加元素個數for (int i = 1; i < temp.length; i++) {temp[i] = temp[i - 1] + temp[i];}// 創建存儲結果的臨時數組int[] result = new int[length];for (int i : array) {result[temp[i] - 1] = i;temp[i] = temp[i] - 1;}// 將result拷貝到array中去for (int i = 0; i < array.length; i++) {array[i] = result[i];} }@Test public void countingSort() {int[] array = {2,6,7,3,1,5,3,2,3};System.out.println(Arrays.toString(array));countingSort(array,array.length);System.out.println(Arrays.toString(array)); }采坑啦!
其實是在寫基數排序時,使用了上述計數排序,就踩到坑了
坑的位置在
針對重復元素,因為temp[i]要減一,所以后插入的元素是在先插入的前邊
而這段代碼中是按array從前向后遍歷的,這和上述很巧妙的方式正好反向,導致這里的排序不是穩定的排序
修改方案就是改為逆向遍歷
完整代碼如下:
public void countingSort(int[] array, int length) {// 找最大值int max = array[0];for (int i = 1; i < length; i++) {if (max < array[i]) {max = array[i];}}// 創建統計元素個數的數組int[] temp = new int[max + 1];for (int i : array) {temp[i] = temp[i] + 1;}// 疊加元素個數for (int i = 1; i < temp.length; i++) {temp[i] = temp[i - 1] + temp[i];}// 創建存儲結果的臨時數組int[] result = new int[length];for (int i = length - 1; i >= 0; i--) {int item = array[i];result[temp[item] - 1] = item;temp[item] = temp[item] - 1;}// 將result拷貝到array中去for (int i = 0; i < array.length; i++) {array[i] = result[i];} }@Test public void countingSort() {int[] array = {2, 6, 7, 3, 1, 5, 3, 2, 3};System.out.println(Arrays.toString(array));countingSort(array, array.length);System.out.println(Arrays.toString(array)); }基數排序
特定場景
這里涉及到的都是字符串排序,而且滿足如下條件:
第一個字符小的排在前
第一個字符相同時,第二個字符小的排在前
第一第二個字符相同時,第三個字符小的排在前
…
思路
思路其實是沒有的,只是覺得很巧吧
主要是執行固定次數的時間復雜度為O(n)的穩定排序
比如如下5個字符串
["113","217","121","212","221"]
首先按第三位排序得
["121","221","212","113","217"]注意這里是穩定排序(當然,在這里還用不到穩定排序的優勢),121和221順序不變
接著按第二位排序得
["212","113","217","121","221"]再次注意這里的穩定排序
最后按首位排序得
["113","121","212","217","221"]
最終代碼
private void countingSort(String[] array, int length, int index) {char max = array[0].charAt(index);for (int i = 1; i < length; i++) {char c = array[i].charAt(index);if (max < c) {max = c;}}int maxValue = charToInt(max);int[] temp = new int[maxValue + 1];for (int i = 0; i < length; i++) {int item = charToInt(array[i].charAt(index));temp[item] = temp[item] + 1;}for (int i = 1; i < temp.length; i++) {temp[i] = temp[i - 1] + temp[i];}String[] result = new String[length];for (int i = length - 1; i >= 0; i--) {int item = charToInt(array[i].charAt(index));result[temp[item] - 1] = array[i];temp[item] = temp[item] - 1;}for (int i = 0; i < array.length; i++) {array[i] = result[i];}}private int charToInt(char c) {return c - '0'; }public void radixSort(String[] array, int length) {int stringLength = array[0].length();for (int i = stringLength - 1; i >= 0; i--) {countingSort(array, length, i);} }@Test public void radixSort() {String[] array = {"12345678901", "16789012342", "17890123453", "13456789014", "11234567895","15678901236", "13456789017", "12345678908", "13456789019"};System.out.println(Arrays.toString(array));radixSort(array, array.length);System.out.println(Arrays.toString(array)); }這里附上一段經某位同學點撥后寫出的代碼,適用于億級別int類型整數的高速排序
public void radixSort(int[] array, int length) {for (int i = 0; i < 32; i++) {radixSort(array, length, i);} }private void radixSort(int[] array, int length, int index) {int[] tempArray = new int[2];for (int i = 0; i < array.length; i++) {int item = (array[i] >> index) & 1;tempArray[item] = tempArray[item] + 1;}for (int i = 1; i < tempArray.length; i++) {tempArray[i] = tempArray[i] + tempArray[i - 1];}int[] result = new int[length];for (int i = length - 1; i >= 0; i--) {int item = (array[i] >> index) & 1;result[tempArray[item] - 1] = array[i];tempArray[item] = tempArray[item] - 1;}for (int i = 0; i < length; i++) {array[i] = result[i];}}說明一下:
這相當于把一個int類型值先轉為二進制,再按二進制排序。int最多32位
上萬級別的數據,某位上的最大值為0的概率極低,所以最大值為1,存儲0和1的個數數組就是int[2]了。
其余操作不變,但對于取某位上的數值,可使用如下公式:
取二進制數的倒數第i位的值 x >> (i - 1) & 1
總結
以上是生活随笔為你收集整理的时间复杂度为O(n)的排序(JAVA)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: windows7共享打印机无法连接0x0
- 下一篇: python九九乘法表代码