你好,我是小趙,最近在系統的整理算法方面的知識,當你度過了新手階段,想要成爲牛逼的技術達人,算法是必需要掌握的東西,而算法中的排序,是每一個程序員都繞不開的基本功,重要性就不必多說了。程序員
在工做之餘堅持學習老是很是辛苦,常常不知不覺熬夜到四五點纔去睡,文中的每一張圖(除了最後一張,哈哈 ^_^)都是親手所畫,每一份實現代碼,都是仔細測試並添加註釋,固然在這個紛雜的信息世界中, 沒有百分之一百的原創,免不了有的東西有所借鑑與參考。算法
若是你還處在新手階段無憂無慮,請必定要儘可能學一學,若是你是前輩,歡迎提一些寶貴意見。數組
若是你以爲喜歡,請留個評論,或者給個關注,有粉必回哈,毫不會讓你有來無回,很是感謝。網絡
本文篇幅過大,字符總數一萬八千餘個,配圖60餘張,放在書籍裏已經能夠寫一個大章節了,強烈推薦先收藏再閱讀。數據結構
冒泡排序無疑是最爲出名的排序算法之一,從序列的一端開始往另外一端冒泡(你能夠從左往右冒泡,也能夠從右往左冒泡,看心情),依次比較相鄰的兩個數的大小(究竟是比大仍是比小也看你心情)。學習
咱們以[8,2,5,9,7]這組數字來作示例,上圖來戰:測試
咱們從左往右依次冒泡,將小的往右移動優化
首先比較第一個數和第二個數的大小,咱們發現2比8要小,那麼保持原位,不作改動。位置仍是8,2,5,9,7。ui
指針往右移動一格,接着比較:spa
比較第二個數和第三個數的大小,發現2比5要小,因此位置交換,交換後數組更新爲:[8,5,2,9,7]。
指針再往右移動一格,繼續比較:
比較第三個數和第四個數的大小,發現2比9要小,因此位置交換,交換後數組更新爲:[8,5,9,2,7]
一樣,指針再往右移動,繼續比較:
比較第4個數和第5個數的大小,發現2比7要小,因此位置交換,交換後數組更新爲:[8,5,9,7,2]
下一步,指針再往右移動,發現已經到底了,則本輪冒泡結束,處於最右邊的2就是已經排好序的數字。
經過這一輪不斷的對比交換,數組中最小的數字移動到了最右邊。
接下來繼續第二輪冒泡:
因爲右邊的2已是排好序的數字,就再也不參與比較,因此本輪冒泡結束,本輪冒泡最終冒到頂部的數字5也歸於有序序列中,如今數組已經變化成了[8,9,7,5,2]。
讓咱們開始第三輪冒泡吧!
因爲8比7大,因此位置不變,此時第三輪冒泡也已經結束,第三輪冒泡的最後結果是[9,8,7,5,2]
緊接着第四輪冒泡:
9和8比,位置不變,即肯定了8進入有序序列,那麼最後只剩下一個數字9,放在末尾,自此排序結束。
public static void sort(int arr[]) { for(int i=0;i<arr.length-1;i++){ for(int j=0;j<arr.length-1-i;j++){ int temp = 0; if(arr[j] < arr[j+1]){ temp = arr[j]; arr[j] = arr[j+1]; arr[j+1] = temp; } } } }
冒泡的代碼仍是至關簡單的,兩層循環,外層冒泡輪數,裏層依次比較,江湖中人人盡皆知。
咱們看到嵌套循環,應該立馬就能夠得出這個算法的時間複雜度爲O(n^2)。
冒泡有一個最大的問題就是這種算法無論無論你有序仍是沒序,閉着眼睛把你循環比較了再說。
好比我舉個數組例子:[9,8,7,6,5],一個有序的數組,根本不須要排序,它仍然是雙層循環一個很多的把數據遍歷乾淨,這其實就是作了不必作的事情,屬於浪費資源。
針對這個問題,咱們能夠設定一個臨時遍從來標記該數組是否已經有序,若是有序了就不用遍歷了。
public static void sort(int arr[]) { for(int i=0;i<arr.length-1;i++){ boolean isSort = true; for(int j=0;j<arr.length-1-i;j++){ int temp = 0; if(arr[j] < arr[j+1]){ temp = arr[j]; arr[j] = arr[j+1]; arr[j+1] = temp; isSort = false; } } if(isSort){ break; } } }
選擇排序的思路是這樣的:首先,找到數組中最小的元素,拎出來,將它和數組的第一個元素交換位置,第二步,在剩下的元素中繼續尋找最小的元素,拎出來,和數組的第二個元素交換位置,如此循環,直到整個數組排序完成。
至於選大仍是選小,這個都無所謂,你也能夠每次選擇最大的拎出來排,也能夠每次選擇最小的拎出來的排,只要你的排序的手段是這種方式,都叫選擇排序。
咱們仍是以[8,2,5,9,7]這組數字作例子。
第一次選擇,先找到數組中最小的數字2,而後和第一個數字交換位置。(若是第一個數字就是最小值,那麼本身和本身交換位置,也能夠不作處理,就是一個if的事情)
第二次選擇,因爲數組第一個位置已是有序的,因此只須要查找剩餘位置,找到其中最小的數字5,而後和數組第二個位置的元素交換。
第三次選擇,找到最小值7,和第三個位置的元素交換位置。
第四次選擇,找到最小值8,和第四個位置的元素交換位置。
最後一個到達了數組末尾,沒有可對比的元素,結束選擇。
如此整個數組就排序完成了。
public static void sort(int arr[]) { for(int i=0;i<arr.length;i++){ int min = i;//最小元素的下標 for(int j=i+1;j<arr.length;j++){ if(arr[j] < arr[min]){ min = j;//找最小值 } } //交換位置 int temp = arr[i]; arr[i] = arr[min]; arr[min] = temp; } }
雙層循環,時間複雜度和冒泡如出一轍,都是O(n^2)
插入排序的思想和咱們打撲克摸牌的時候同樣,從牌堆裏一張一張摸起來的牌都是亂序的,咱們會把摸起來的牌插入到左手中合適的位置,讓左手中的牌時刻保持一個有序的狀態。
那若是咱們不是從牌堆裏摸牌,而是左手裏面初始化就是一堆亂牌呢? 同樣的道理,咱們把牌往手的右邊挪一挪,把手的左邊空出一點位置來,而後在亂牌中抽一張出來,插入到左邊,再抽一張出來,插入到左邊,再抽一張,插入到左邊,每次插入都插入到左邊合適的位置,時刻保持左邊的牌是有序的,直到右邊的牌抽完,則排序完畢。
數組初始化:[8,2,5,9,7],咱們把數組中的數據分紅兩個區域,已排序區域和未排序區域,初始化的時候全部的數據都處在未排序區域中,已排序區域是空。
第一輪,從未排序區域中隨機拿出一個數字,既然是隨機,那麼咱們就獲取第一個,而後插入到已排序區域中,已排序區域是空,那麼就不作比較,默認自身已是有序的了。(固然了,第一輪在代碼中是能夠省略的,從下標爲1的元素開始便可)
第二輪,繼續從未排序區域中拿出一個數,插入到已排序區域中,這個時候要遍歷已排序區域中的數字挨個作比較,比大比小取決於你是想升序排仍是想倒序排,這裏排升序:
第三輪,排5:
第四輪,排9:
第五輪,排7
排序結束。
咱們來看一下插入排序的代碼實現
public static void sort(int[] arr) { int n = arr.length; for (int i = 1; i < n; ++i) { int value = arr[i]; int j = 0;//插入的位置 for (j = i-1; j >= 0; j--) { if (arr[j] > value) { arr[j+1] = arr[j];//移動數據 } else { break; } } arr[j+1] = value; //插入數據 } }
從代碼裏咱們能夠看出,若是找到了合適的位置,就不會再進行比較了,就比如牌堆裏抽出的一張牌自己就比我手裏的牌都小,那麼我只須要直接放在末尾就好了,不用一個一個去移動數據騰出位置插入到中間。
因此說,最好狀況的時間複雜度是O(n),最壞狀況的時間複雜度是O(n^2),然而時間複雜度這個指標看的是最壞的狀況,而不是最好的狀況,因此插入排序的時間複雜度是O(n^2)。
希爾排序這個名字,來源於它的發明者希爾,也稱做「縮小增量排序」,是插入排序的一種更高效的改進版本。
咱們知道,插入排序對於大規模的亂序數組的時候效率是比較慢的,由於它每次只能將數據移動一位,希爾排序爲了加快插入的速度,讓數據移動的時候能夠實現跳躍移動,節省了一部分的時間開支。
待排序數組 10個數據:
假設計算出的排序區間爲4,那麼咱們第一次比較應該是用第5個數據與第1個數據相比較。
調換後的數據爲[7,2,5,9,8,10,1,15,12,3],而後指針右移,第6個數據與第2個數據相比較。
指針右移,繼續比較。
若是交換數據後,發現減去區間獲得的位置還存在數據,那麼繼續比較,好比下面這張圖,12和8相比較,原地不動後,指針從12跳到8身上,繼續減去區間發現前面還有一個下標爲0的數據7,那麼8和7相比較。
比較完以後的效果是7,8,12三個數爲有序排列。
當最後一個元素比較完以後,咱們會發現大部分值比較大的數據都彷佛調整到數組的中後部分了。
假設整個數組比較長的話,好比有100個數據,那麼咱們的區間確定是四五十,調整後區間再縮小成一二十還會從新調整一輪,直到最後區間縮小爲1,就是真正的排序來了。
指針右移,繼續比較:
重複步驟,便可完成排序,重複的圖就很少畫了。
咱們能夠發現,當區間爲1的時候,它使用的排序方式就是插入排序。
public static void sort(int[] arr) { int length = arr.length; //區間 int gap = 1; while (gap < length) { gap = gap * 3 + 1; } while (gap > 0) { for (int i = gap; i < length; i++) { int tmp = arr[i]; int j = i - gap; //跨區間排序 while (j >= 0 && arr[j] > tmp) { arr[j + gap] = arr[j]; j -= gap; } arr[j + gap] = tmp; } gap = gap / 3; } }
可能你會問爲何區間要以gap=gap*3+1去計算,其實最優的區間計算方法是沒有答案的,這是一個長期未解決的問題,不過差很少都會取在二分之一到三分之一附近。
歸併字面上的意思是合併,歸併算法的核心思想是分治法,就是將一個數組一刀切兩半,遞歸切,直到切成單個元素,而後從新組裝合併,單個元素合併成小數組,兩個小數組合併成大數組,直到最終合併完成,排序完畢。
咱們以[8,2,5,9,7]這組數字來舉例
首先,一刀切兩半:
再切:
再切
粒度切到最小的時候,就開始歸併
數據量設定的比較少,是爲了方便圖解,數據量爲單數,是爲了讓你看到細節,下面我畫了一張更直觀的圖可能你會更喜歡:
咱們上面講過,歸併排序的核心思想是分治,分而治之,將一個大問題分解成無數的小問題進行處理,處理以後再合併,這裏咱們採用遞歸來實現:
/** * 歸併排序 * @param arr */ public static void sort(int[] arr) { int[] tempArr = new int[arr.length]; sort(arr, tempArr, 0, arr.length-1); } /** * 歸併排序 * @param arr 排序數組 * @param tempArr 臨時存儲數組 * @param startIndex 排序起始位置 * @param endIndex 排序終止位置 */ private static void sort(int[] arr,int[] tempArr,int startIndex,int endIndex){ if(endIndex <= startIndex){ return; } //中部下標 int middleIndex = startIndex + (endIndex - startIndex) / 2; //分解 sort(arr,tempArr,startIndex,middleIndex); sort(arr,tempArr,middleIndex + 1,endIndex); //歸併 merge(arr,tempArr,startIndex,middleIndex,endIndex); } /** * 歸併 * @param arr 排序數組 * @param tempArr 臨時存儲數組 * @param startIndex 歸併起始位置 * @param middleIndex 歸併中間位置 * @param endIndex 歸併終止位置 */ private static void merge(int[] arr, int[] tempArr, int startIndex, int middleIndex, int endIndex) { //複製要合併的數據 for (int s = startIndex; s <= endIndex; s++) { tempArr[s] = arr[s]; } int left = startIndex;//左邊首位下標 int right = middleIndex + 1;//右邊首位下標 for (int k = startIndex; k <= endIndex; k++) { if(left > middleIndex){ //若是左邊的首位下標大於中部下標,證實左邊的數據已經排完了。 arr[k] = tempArr[right++]; } else if (right > endIndex){ //若是右邊的首位下標大於了數組長度,證實右邊的數據已經排完了。 arr[k] = tempArr[left++]; } else if (tempArr[right] < tempArr[left]){ arr[k] = tempArr[right++];//將右邊的首位排入,而後右邊的下標指針+1。 } else { arr[k] = tempArr[left++];//將左邊的首位排入,而後左邊的下標指針+1。 } } }
咱們能夠發現merge方法中只有一個for循環,直接就能夠得出每次合併的時間複雜度爲O(n),而分解數組每次對半切割,屬於對數時間O(log n),合起來等於O(log2n),也就是說,總的時間複雜度爲O(nlogn)。
關於空間複雜度,其實大部分人寫的歸併都是在merge方法裏面申請臨時數組,用臨時數組來輔助排序工做,空間複雜度爲O(n),而我這裏作的是原地歸併,只在最開始申請了一個臨時數組,因此空間複雜度爲O(1)。
快速排序的核心思想也是分治法,分而治之。它的實現方式是每次從序列中選出一個基準值,其餘數依次和基準值作比較,比基準值大的放右邊,比基準值小的放左邊,而後再對左邊和右邊的兩組數分別選出一個基準值,進行一樣的比較移動,重複步驟,直到最後都變成單個元素,整個數組就成了有序的序列。
咱們以[8,2,5,0,7,4,6,1]這組數字來進行演示
首先,咱們隨機選擇一個基準值:
與其餘元素依次比較,大的放右邊,小的放左邊:
而後咱們以一樣的方式排左邊的數據:
繼續排0和1:
因爲只剩下一個數,因此就不用排了,如今的數組序列是下圖這個樣子:
右邊以一樣的操做進行,便可排序完成。
快速排序的關鍵之處在於切分,切分的同時要進行比較和移動,這裏介紹一種叫作單邊掃描的作法。
咱們隨意抽取一個數做爲基準值,同時設定一個標記mark表明左邊序列最右側的下標位置,固然初始爲0,接下來遍歷數組,若是元素大於基準值,無操做,繼續遍歷,若是元素小於基準值,則把mark+1,再將mark所在位置的元素和遍歷到的元素交換位置,mark這個位置存儲的是比基準值小的數據,當遍歷結束後,將基準值與mark所在元素交換位置便可。
代碼實現:
public static void sort(int[] arr) { sort(arr, 0, arr.length - 1); } private static void sort(int[] arr, int startIndex, int endIndex) { if (endIndex <= startIndex) { return; } //切分 int pivotIndex = partitionV2(arr, startIndex, endIndex); sort(arr, startIndex, pivotIndex-1); sort(arr, pivotIndex+1, endIndex); } private static int partition(int[] arr, int startIndex, int endIndex) { int pivot = arr[startIndex];//取基準值 int mark = startIndex;//Mark初始化爲起始下標 for(int i=startIndex+1; i<=endIndex; i++){ if(arr[i]<pivot){ //小於基準值 則mark+1,並交換位置。 mark ++; int p = arr[mark]; arr[mark] = arr[i]; arr[i] = p; } } //基準值與mark對應元素調換位置 arr[startIndex] = arr[mark]; arr[mark] = pivot; return mark; }
另外還有一種雙邊掃描的作法,看起來比較直觀:咱們隨意抽取一個數做爲基準值,而後從數組左右兩邊進行掃描,先從左往右找到一個大於基準值的元素,將下標指針記錄下來,而後轉到從右往左掃描,找到一個小於基準值的元素,交換這兩個元素的位置,重複步驟,直到左右兩個指針相遇,再將基準值與左側最右邊的元素交換。
咱們來看一下實現代碼,不一樣之處只有partition方法:
public static void sort(int[] arr) { sort(arr, 0, arr.length - 1); } private static void sort(int[] arr, int startIndex, int endIndex) { if (endIndex <= startIndex) { return; } //切分 int pivotIndex = partition(arr, startIndex, endIndex); sort(arr, startIndex, pivotIndex-1); sort(arr, pivotIndex+1, endIndex); } private static int partition(int[] arr, int startIndex, int endIndex) { int left = startIndex; int right = endIndex; int pivot = arr[startIndex];//取第一個元素爲基準值 while (true) { //從左往右掃描 while (arr[left] <= pivot) { left++; if (left == right) { break; } } //從右往左掃描 while (pivot < arr[right]) { right--; if (left == right) { break; } } //左右指針相遇 if (left >= right) { break; } //交換左右數據 int temp = arr[left]; arr[left] = arr[right]; arr[right] = temp; } //將基準值插入序列 int temp = arr[startIndex]; arr[startIndex] = arr[right]; arr[right] = temp; return right; }
快速排序的時間複雜度和歸併排序同樣,O(n log n),但這是創建在每次切分都能把數組一刀切兩半差很少大的前提下,若是出現極端狀況,好比排一個有序的序列,如[9,8,7,6,5,4,3,2,1],選取基準值9,那麼須要切分n-1次才能完成整個快速排序的過程,這種狀況下,時間複雜度就退化成了O(n^2),固然極端狀況出現的機率也是比較低的。
因此說,快速排序的時間複雜度是O(nlogn),極端狀況下會退化成O(n^2),爲了不極端狀況的發生,選取基準值應該作到隨機選取,或者是打亂一下數組再選取。
另外,快速排序的空間複雜度爲O(1)。
堆排序顧名思義,是利用堆這種數據結構來進行排序的算法。
若是你瞭解堆這種數據結構,你應該知道堆是一種優先隊列,兩種實現,最大堆和最小堆,因爲咱們這裏排序按升序排,因此就直接以最大堆來講吧。
咱們徹底能夠把堆(如下全都默認爲最大堆)當作一棵徹底二叉樹,可是位於堆頂的元素老是整棵樹的最大值,每一個子節點的值都比父節點小,因爲堆要時刻保持這樣的規則特性,因此一旦堆裏面的數據發生變化,咱們必須對堆從新進行一次構建。
既然堆頂元素永遠都是整棵樹中的最大值,那麼咱們將數據構建成堆後,只須要從堆頂取元素不就行了嗎? 第一次取的元素,是否取的就是最大值?取完後把堆從新構建一下,而後再取堆頂的元素,是否取的就是第二大的值? 反覆的取,取出來的數據也就是有序的數據。
咱們以[8,2,5,9,7,3]這組數據來演示。
首先,將數組構建成堆。
既然構建成堆結構了,那麼接下來,咱們取出堆頂的數據,也就是數組第一個數,9,取法是將數組的第一位和最後一位調換,而後將數組的待排序範圍-1,
如今的待排序數據是[3,8,5,2,7],咱們繼續將待排序數據構建成堆。
取出堆頂數據,此次就是第一位和倒數第二位交換了,由於待排序的邊界已經減1。
繼續構建堆
從堆頂取出來的數據最終造成一個有序列表,重複的步驟就再也不贅述了,咱們來看一下代碼實現。
若是你對堆這個數據結構不熟悉,不太理解代碼,那麼我建議你打斷點,一行一行的觀察數據的變化,伴隨着註釋說明,你確定會豁然開朗。
/** * 堆排序 * @param arr 排序數組 */ public static void sort(int[] arr) { int length = arr.length; //構建堆 buildHeap(arr, length); for (int i = length - 1; i > 0; i--) { //將堆頂元素與末位元素調換 int temp = arr[0]; arr[0] = arr[i]; arr[i] = temp; //數組長度-1 隱藏堆尾元素 length--; //將堆頂元素下沉 目的是將最大的元素浮到堆頂來 sink(arr, 0, length); } } /** * 構建堆 * @param arr 數組 * @param length 數組範圍 */ private static void buildHeap(int[] arr, int length) { for (int i = length / 2; i >= 0; i--) { sink(arr, i, length); } } /** * 下沉調整 * @param arr 數組 * @param index 調整位置 * @param length 數組範圍 */ private static void sink(int[] arr, int index, int length) { int leftChild = 2 * index + 1;//左子節點下標 int rightChild = 2 * index + 2;//右子節點下標 int present = index;//要調整的節點下標 //下沉左邊 if (leftChild < length && arr[leftChild] > arr[present]) { present = leftChild; } //下沉右邊 if (rightChild < length && arr[rightChild] > arr[present]) { present = rightChild; } //若是下標不相等 證實調換過了 if (present != index) { //交換值 int temp = arr[index]; arr[index] = arr[present]; arr[present] = temp; //繼續下沉 sink(arr, present, length); } }
堆排序和快速排序的時間複雜度都同樣是O(nlogn)。
計數排序是一種非基於比較的排序算法,咱們以前介紹的各類排序算法幾乎都是基於元素之間的比較來進行排序的,計數排序的時間複雜度爲O(n+m),m指的是數據量,說的簡單點,計數排序算法的時間複雜度約等於O(n),快於任何比較型的排序算法。
如下以[3,5,8,2,5,4]這組數字來演示。
首先,咱們找到這組數字中最大的數,也就是8,建立一個最大下標爲8的空數組arr。
遍歷數據,將數據的出現次數填入arr中對應的下標位置中。
遍歷arr,將數據依次取出便可。
public static void sort(int[] arr) { //找出數組中的最大值 int max = arr[0]; for (int i = 1; i < arr.length; i++) { if (arr[i] > max) { max = arr[i]; } } //初始化計數數組 int[] countArr = new int[max + 1]; //計數 for (int i = 0; i < arr.length; i++) { countArr[arr[i]]++; arr[i] = 0; } //排序 int index = 0; for (int i = 0; i < countArr.length; i++) { if (countArr[i] > 0) { arr[index++] = i; } } }
有一個需求就是當對成績進行排名次的時候,如何在原來排前面的人,排序後仍是處於相同成績的人的前面。
解題的思路是對countArr計數數組進行一個變形,變來和名次掛鉤,咱們知道countArr存放的是分數的出現次數,那麼其實咱們能夠算出每一個分數的最大名次,就是將countArr中的每一個元素順序求和。
以下圖:
變形以後是什麼意思呢?
咱們把原數組[2,5,8,2,5,4]中的數據依次拿來去countArr去找,你會發現3這個數在countArr[3]中的值是2,表明着排名第二名,(由於第一名是最小的2,對吧?),5這個數在countArr[5]中的值是5,爲何是5呢?咱們來數數,排序後的數組應該是[2,3,4,5,5,8],5的排名是第五名,那4的排名是第幾名呢?對應countArr[4]的值是3,第三名,5的排名是第五名是由於5這個數有兩個,天然佔據了第4名和第5名。
因此咱們取排名的時候應該特別注意,原數組中的數據要從右往左取,從countArr取出排名後要把countArr中的排名減1,以便於再次取重複數據的時候排名往前一位。
啊哦,圖都畫完了才發現個人排名是按升序來排的,居然有這種排分數的邏輯,尷尬。
下面是代碼實現:
public static void sort(int[] arr) { //找出數組中的最大值 int max = arr[0]; for (int i = 1; i < arr.length; ++i) { if (arr[i] > max) { max = arr[i]; } } //初始化計數數組 int[] countArr = new int[max + 1]; //計數 for (int i = 0; i < arr.length; ++i) { countArr[arr[i]]++; } //順序累加 for (int i = 1; i < max + 1; ++i) { countArr[i] = countArr[i-1] + countArr[i]; } //排序後的數組 int[] sortedArr = new int[arr.length]; //排序 for (int i = arr.length - 1; i >= 0; --i) { sortedArr[countArr[arr[i]]-1] = arr[i]; countArr[arr[i]]--; } //將排序後的數據拷貝到原數組 for (int i = 0; i < arr.length; ++i) { arr[i] = sortedArr[i]; } }
計數排序的毛病不少,咱們來找找bug。
若是我要排的數據裏有0呢? int[]初始化內容全是0,排毛線。
若是我要排的數據範圍比較大呢?好比[1,9999],我排兩個數你要建立一個int[10000]的數組來計數?
對於第一個bug,咱們可使用偏移量來解決,好比我要排[-1,0,-3]這組數字,這個簡單,我全給大家加10來計數,變成[9,10,7]計完數後寫回原數組時再減10。不過有可能也會踩到坑,萬一你數組裏剛好有一個-10,你加上10後又變0了,排毛線。
對於第二個bug,確實解決不了,若是是[9998,9999]這種雖然值大可是相差範圍不大的數據咱們也可使用偏移量解決,好比這兩個數據,我減掉9997後只須要申請一個int[3]的數組就能夠進行計數。
因而可知,計數排序只適用於正整數而且取值範圍相差不大的數組排序使用,它的排序的速度是很是可觀的。
桶排序能夠當作是計數排序的升級版,它將要排的數據分到多個有序的桶裏,每一個桶裏的數據再單獨排序,再把每一個桶的數據依次取出,便可完成排序。
咱們拿一組計數排序啃不掉的數據[500,6123,1700,10,9999]來舉例。
第一步,咱們建立10個桶,分別來裝0-1000、1000-2000、2000-3000、3000-4000、4000-5000、5000-6000、6000-7000、7000-8000、8000-9000區間的數據。
第二步,遍歷原數組,對號入桶。
第三步,對桶中的數據進行單獨排序,只有第一個桶中的數量大於1,顯然只須要排第一個桶。
最後,依次將桶中的數據取出,排序完成。
這個桶排序乍一看好像挺簡單的,可是要敲代碼就須要考慮幾個問題了。
桶這個東西怎麼表示?
怎麼肯定桶的數量?
桶內排序用什麼方法排?
咱們先來看看個人實現:
public static void sort(int[] arr){ //最大最小值 int max = arr[0]; int min = arr[0]; int length = arr.length; for(int i=1; i<length; i++) { if(arr[i] > max) { max = arr[i]; } else if(arr[i] < min) { min = arr[i]; } } //最大值和最小值的差 int diff = max - min; //桶列表 ArrayList<ArrayList<Integer>> bucketList = new ArrayList<>(); for(int i = 0; i < length; i++){ bucketList.add(new ArrayList<>()); } //每一個桶的存數區間 float section = (float) diff / (float) (length - 1); //數據入桶 for(int i = 0; i < length; i++){ //當前數除以區間得出存放桶的位置 減1後得出桶的下標 int num = (int) (arr[i] / section) - 1; if(num < 0){ num = 0; } bucketList.get(num).add(arr[i]); } //桶內排序 for(int i = 0; i < bucketList.size(); i++){ //jdk的排序速度固然信得過 Collections.sort(bucketList.get(i)); } //寫入原數組 int index = 0; for(ArrayList<Integer> arrayList : bucketList){ for(int value : arrayList){ arr[index] = value; index++; } } }
桶固然是一個能夠存放數據的集合,我這裏使用arrayList,若是你使用LinkedList那其實也是沒有問題的。
桶的數量我認爲設置爲原數組的長度是合理的,由於理想狀況下每一個數據裝一個桶。
數據入桶的映射算法實際上是一個開放性問題,因爲難度緣由我找了一下網絡上的文章博客,目前我沒有發現什麼最優的答案,大部分對桶排序的實現都是遮遮掩掩的,少部分寫的代碼也是瞎整,我認可我這裏寫的方案並不佳,由於我測試過不一樣的數據集合來排序,若是你有什麼更好的方案或想法,歡迎留言討論。
桶內排序爲了方便起見使用了當前語言提供的排序方法,若是對於穩定排序有所要求,能夠選擇使用自定義的排序算法。
在額外空間充足的狀況下,儘可能增大桶的數量,極限狀況下每一個桶只有一個數據時,或者是每隻桶只裝一個值時,徹底避開了桶內排序的操做,桶排序的最好時間複雜度就可以達到O(n)
好比高考總分750分,全國幾百萬人,咱們只須要建立751個桶,循環一遍挨個扔進去,排序速度是毫秒級。
可是若是數據通過桶的劃分以後,桶與桶的數據分佈極不均勻,有些數據很是多,有些數據很是少,好比[8,2,9,10,1,23,53,22,12,9000]這十個數據,咱們分紅十個桶裝,結果發現第一個桶裝了9個數據,這是很是影響效率的狀況,會使時間複雜度降低到O(nlogn),解決辦法是咱們每次桶內排序時判斷一下數據量,若是桶裏的數據量過大,那麼應該在桶裏面回調自身再進行一次桶排序。
基數排序是一種非比較型整數排序算法,其原理是將數據按位數切割成不一樣的數字,而後按每一個位數分別比較。
假設說,咱們要對100萬個手機號碼進行排序,應該選擇什麼排序算法呢?排的快的有歸併、快排時間複雜度是O(nlogn),計數排序和桶排序雖然更快一些,可是手機號碼位數是11位,那得須要多少桶?內存條表示不服。
這個時候,咱們使用基數排序是最好的選擇。
咱們以[892, 846, 821, 199, 810,700]這組數字來作例子演示。
首先,建立十個桶,用來輔助排序。
先排個位數,根據個位數的值將數據放到對應下標值的桶中。
排完後,咱們將桶中的數據依次取出。
那麼接下來,咱們排十位數。
最後,排百位數。
排序完成。
基數排序能夠當作桶排序的擴展,也是用桶來輔助排序,咱們來看下代碼:
public static void sort(int[] arr) { int length = arr.length; //最大值 int max = arr[0]; for(int i=0;i<length;i++){ if(arr[i] > max){ max = arr[i]; } } //當前排序位置 int location = 1; //桶列表 ArrayList<ArrayList<Integer>> bucketList = new ArrayList<>(); //長度爲10 裝入餘數0-9的數據 for(int i = 0; i < 10; i++){ bucketList.add(new ArrayList()); } while(true) { //判斷是否排完 int dd = (int)Math.pow(10,(location - 1)); if(max < dd){ break; } //數據入桶 for(int i = 0; i < length; i++) { //計算餘數 放入相應的桶 int number = ((arr[i] / dd) % 10); bucketList.get(number).add(arr[i]); } //寫回數組 int nn = 0; for (int i=0;i<10;i++){ int size = bucketList.get(i).size(); for(int ii = 0;ii < size;ii ++){ arr[nn++] = bucketList.get(i).get(ii); } bucketList.get(i).clear(); } location++; } }
其實它的思想很簡單,無論你的數字有多大,我一位一位的排,0-9最多也就十個桶,先按權重小的位置排序,而後按權重大的位置排序。
固然,若是你有需求,也能夠選擇從高位往低位排。
最後,在菜鳥教程上扒了張圖,真的是太詳細了,非扒不可。
最後,感謝閱讀。