算法,是永恆的技能,今天繼續算法篇,將研究桶排序。
算法思想:
桶排序,其思想非常簡單易懂,就是是將一個數據表分割成許多小數據集,每個數據集對應於一個新的集合(也就是所謂的桶bucket),然后每個bucket各自排序,或用不同的排序算法,或者遞歸的使用bucket sort算法,往往采用快速排序。是一個典型的divide-and-conquer分而治之的策略。
其中核心思想在於如何將原始待排序的數據划分到不同的桶中,也就是數據映射過程f(x)的定義,這個f(x)關乎桶數據的平衡性(各個桶內的數據盡量數量不要差異太大),也關乎桶排序能處理的數據類型(整形,浮點型;只能正數,或者正負數都可以)
另外,桶排序的具體實現,需要考慮實際的應用場景,因為很難找到一個通吃天下的f(x)。
基本實現步驟:
1. 根據數據類型,定義數據映射函數f(x)
2. 對數據進行分別規划進入桶內
3. 對桶做基於序號的排序
4. 對每個桶內的數據進行排序(快排或者其他排序算法)
5. 將排序后的數據映射到原始輸入數組中,作為輸出
桶排序,通常情況下速度非常快,比快速排序還要快,但是,依據我的理解,這個快,應該是建立在大數據量的排序。若待排序的數據元素個數比較少,桶排序的優勢就不是那么明顯了,因為桶排序就是基於分而治之的策略,可以將數據進行分布式排序,充分發揮並行計算的優勢。
特性說明:
1. 桶排序的時間復雜度通常是O(N+N*logM),其中,N表示桶的個數,M表示桶內元素的個數(這里,M取的是一個大概的平均數,這也說明,為何桶內的元素盡量不要出現有的很多,有的很少這種分布不均的事情,分布不均的話,算法的性能優勢就不能最大發揮)。
2. 桶排序是穩定的(是可以做到平衡排序的)。
3. 桶排序,在內存方面消耗是比較大的,可以說其時間性能優勢是由犧牲空間換來的。
下面,我們直接上代碼,我的實現過程中,考慮了數據的重復性,考慮到了數據有正有負的情況!
1 /** 2 * @author "shihuc" 3 * @date 2017年1月17日 4 */ 5 package bucketSort; 6 7 import java.io.File; 8 import java.io.FileNotFoundException; 9 import java.util.ArrayList; 10 import java.util.HashMap; 11 import java.util.Scanner; 12 13 /** 14 * @author shihuc 15 * 16 * 桶排序的實現過程,算法中考慮到了元素的重復性 17 */ 18 public class BucketSortDemo { 19 20 /** 21 * @param args 22 */ 23 public static void main(String[] args) { 24 File file = new File("./src/bucketSort/sample.txt"); 25 Scanner sc = null; 26 try { 27 sc = new Scanner(file); 28 //獲取測試例的個數 29 int T = sc.nextInt(); 30 for(int i=0; i<T; i++){ 31 //獲取每個測試例的元素個數 32 int N = sc.nextInt(); 33 //獲取桶的個數 34 int M = sc.nextInt(); 35 int A[] = new int[N]; 36 for(int j=0; j<N; j++){ 37 A[j] = sc.nextInt(); 38 } 39 bucketSort(A, M); 40 printResult(i, A); 41 } 42 } catch (FileNotFoundException e) { 43 e.printStackTrace(); 44 } finally { 45 if(sc != null){ 46 sc.close(); 47 } 48 } 49 } 50 51 /** 52 * 計算輸入元素經過桶的個數(M)求商運算后,存入那個桶中,得到桶的下標索引。 53 * 步驟1 54 * 注意: 55 * 這個方法,其實就是桶排序中的相對核心的部分,也就是常說的待排序數組與桶之間的映射規則f(x)的定義部分。 56 * 這個映射規則,對於桶排序算法的不同實現版本,規則函數不同。 57 * 58 * @param elem 原始輸入數組中的元素值 59 * @param m 桶的商數(影響桶的個數) 60 * @return 桶的索引號(編號) 61 */ 62 private static int getBucketIndex(int elem, int m){ 63 return elem / m; 64 } 65 66 private static void bucketSort(int src[], int m){ 67 //定義一個初步排序的桶與原始數據大小的映射關系 68 HashMap<Integer, ArrayList<Integer>> buckets = new HashMap<Integer, ArrayList<Integer>>(); 69 70 //規划數據入桶 【步驟2】 71 programBuckets(src, m, buckets); 72 73 //對桶基於桶的標號進行排序(序號可能是負數)【步驟3】 74 Integer bkIdx[] = new Integer[buckets.keySet().size()]; 75 buckets.keySet().toArray(bkIdx); 76 quickSort(bkIdx, 0, bkIdx.length - 1); 77 78 //計算每個桶對應於輸出數組空間的其實位置 79 HashMap<Integer, Integer> bucketIdxPosMap = new HashMap<Integer, Integer>(); 80 int startPos = 0; 81 for(Integer idx: bkIdx){ 82 bucketIdxPosMap.put(idx, startPos); 83 startPos += buckets.get(idx).size(); 84 } 85 86 //對桶內的數據采取快速排序,並將排序后的結果映射到原始數組中作為輸出 87 for(Integer bId : buckets.keySet()){ 88 ArrayList<Integer> bk = buckets.get(bId); 89 Integer[] org = new Integer[bk.size()]; 90 bk.toArray(org); 91 quickSort(org, 0, bk.size() - 1); //對桶內數據進行排序 【步驟4】 92 //將排序后的數據映射到原始數組中作為輸出 【步驟5】 93 int stPos = bucketIdxPosMap.get(bId); 94 for(int i=0; i<org.length; i++){ 95 src[stPos++] = org[i]; 96 } 97 } 98 } 99 100 /** 101 * 基於原始數據和桶的個數,對數據進行入桶規划。 102 * 103 * 這個過程,就體現了divide-and-conquer的思想 104 * 105 * @param src 106 * @param m 107 * @param buckets 108 */ 109 private static void programBuckets(int[] src, int m, HashMap<Integer, ArrayList<Integer>> buckets) { 110 for(int i=0; i<src.length; i++){ 111 int bucketIdx = getBucketIndex(src[i], m); 112 113 ArrayList<Integer> bucket = buckets.get(bucketIdx); 114 if(bucket == null){ 115 //定義桶,用來存放初步划分好的原始數據 116 bucket = new ArrayList<Integer>(); 117 buckets.put(bucketIdx, bucket); 118 } 119 bucket.add(src[i]); 120 } 121 } 122 123 /** 124 * 采用類似兩邊夾逼的方式,向輸入數組的中間某個位置夾逼,將原輸入數組進行分割成兩部分,左邊的部分全都小於某個值, 125 * 右邊的部分全都大於某個值。 126 * 127 * 快排算法的核心部分。 128 * 129 * @param src 待排序數組 130 * @param start 數組的起點索引 131 * @param end 數組的終點索引 132 * @return 中值索引 133 */ 134 private static int middle(Integer src[], int start, int end){ 135 int middleValue = src[start]; 136 while(start < end){ 137 //找到右半部分都比middleValue大的分界點 138 while(src[end] >= middleValue && start < end){ 139 end--; 140 } 141 //當遇到比middleValue小的時候或者start不再小於end,將比較的起點值替換為新的最小值起點 142 src[start] = src[end]; 143 //找到左半部分都比middleValue小的分界點 144 while(src[start] <= middleValue && start < end){ 145 start++; 146 } 147 //當遇到比middleValue大的時候或者start不再小於end,將比較的起點值替換為新的終值起點 148 src[end] = src[start]; 149 } 150 //當找到了分界點后,將比較的中值進行交換,將中值放在start與end之間的分界點上,完成一次對原數組分解,左邊都小於middleValue,右邊都大於middleValue 151 src[start] = middleValue; 152 return start; 153 } 154 155 /** 156 * 通過遞歸的方式,對原始輸入數組,進行快速排序。 157 * 158 * @param src 待排序的數組 159 * @param st 數組的起點索引 160 * @param nd 數組的終點索引 161 */ 162 public static void quickSort(Integer src[], int st, int nd){ 163 164 if(st > nd){ 165 return; 166 } 167 int middleIdx = middle(src, st, nd); 168 //將分隔后的數組左邊部分進行快排 169 quickSort(src, st, middleIdx - 1); 170 //將分隔后的數組右半部分進行快排 171 quickSort(src, middleIdx + 1, nd); 172 } 173 174 /** 175 * 打印最終的輸出結果 176 * 177 * @param idx 測試例的編號 178 * @param B 待輸出數組 179 */ 180 private static void printResult(int idx, int B[]){ 181 System.out.print(idx + "--> "); 182 for(int i=0; i<B.length; i++){ 183 System.out.print(B[i] + " "); 184 } 185 System.out.println(); 186 } 187 }
下面附上測試用到的數據:
1 3 2 9 2 3 2 3 1 4 6 -10 8 11 -21 4 15 5 5 2 6 3 4 5 10 9 21 17 31 1 2 21 11 18 6 9 4 7 2 3 1 4 6 -10 8 11 -21
上面第1行表示有幾個測試案例,第二行表示第一個測試案例的熟悉數據,15表示案例元素個數,5表示桶商數(對參與排序的桶的個數有影響)。第3行表示第一個測試案例的待排序數據,第4第5行參照第2和第3行理解。
運行的結果如下:
1 0--> -21 -10 1 2 3 4 6 8 11 2 1--> 1 2 2 3 4 5 6 9 10 11 17 18 21 21 31 3 2--> -21 -10 1 2 3 4 6 8 11
下面附上一個上述測試案例中的一個,通過圖示展示算法邏輯
上述算法實現過程中,桶的個數沒有直接指定,是有桶的商數決定的。當然,也可以根據實際場景,指定桶的個數,與此同時,算法的實現過程就要做相應的修改,但是整體的思想是沒有什么本質差別的。
桶排序,其優勢在於處理大數據量的排序場景,數據相對比較集中,這樣性能優勢很明顯。