排序算法 【桶排序】


算法,是永恆的技能,今天繼續算法篇,將研究桶排序。

 

算法思想:

桶排序,其思想非常簡單易懂,就是是將一個數據表分割成許多小數據集,每個數據集對應於一個新的集合(也就是所謂的桶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

 

下面附上一個上述測試案例中的一個,通過圖示展示算法邏輯

 

上述算法實現過程中,桶的個數沒有直接指定,是有桶的商數決定的。當然,也可以根據實際場景,指定桶的個數,與此同時,算法的實現過程就要做相應的修改,但是整體的思想是沒有什么本質差別的。

桶排序,其優勢在於處理大數據量的排序場景,數據相對比較集中,這樣性能優勢很明顯。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM