內容介紹
計數排序簡介
咱們知道目前排序速度最快的是時間複雜度爲O(nlogn)的排序算法,如快速排序、歸併排序和堆排序。其中O(logn)是利用了分治思想進行數據二分遠距離比較和交換元素的位置。以前的算法都是基於元素比較的,有沒有一種算法,它的時間複雜度小於O(nlogn)呢?這樣的算法是存在的。計數排序就是一個非基於比較的排序算法,該算法於1954年由 Harold H. Seward提出。它的優點在於在對必定範圍內的整數排序時,它的複雜度爲Ο(n+k)(其中k是整數的範圍),快於任何比較排序算法。 固然這是一種犧牲空間換取時間的作法。java
計數排序適用於數據量很大,可是數據的範圍比較小的狀況。好比對一個公司20萬人的年齡進行排序,要排序的數據量很大,可是年齡分佈在0 ~ 134歲之間(最大年齡數據來源吉尼斯世界記錄)。算法
計數排序的思想
使用一個輔助數組,遍歷待排序的數據,待排序數據的值就是輔助數組的索引,輔助數組索引對應的位置保存這個待排序數據出現的次數。最後從輔助數組中取出待排序的數據,放到排序後的數組中。編程
計數排序動畫演示
通常沒有特殊要求排序算法都是升序排序,小的在前,大的在後。數組由{6, 8, 9, 5, 3, 2, 1, 7, 8, 5} 這10個無序元素組成。 數組
計數排序分析
經過上面的動畫演示,咱們能夠將計數排序分爲兩個過程:微信
- 統計過程
- 排序過程。
假設咱們要排序的數據是10個0到9的隨機數字,例如{6, 8, 9, 5, 3, 2, 1, 7, 8, 5} 這10個數據,以下圖所示: 優化
- 統計過程 取出元素6,放到輔助數組索引6的地方,輔助數組記錄數據6出現1次,效果以下圖:
取出元素8,放到輔助數組索引8的地方,輔助數組記錄數據8出現1次,效果以下圖: 動畫
取出元素9,放到輔助數組索引9的地方,輔助數組記錄數據9出現1次,效果以下圖: 3d
依次類推。中間省略一部分。code
再次取出元素8,放到輔助數組索引8的地方,輔助數組記錄數據8出現2次,效果以下圖: blog
最終輔助數組效果以下:
這個輔助數組統計了每一個數據出現的次數,最後遍歷這個輔助數組,輔助數組中的索引就是元素的值,輔助數組中索引對應的值就是這個數據出現的次數,放到排序後的數組中。
- 排序過程 輔助數組索引1的對應的數據1放到排序後數組中,效果以下:
輔助數組索引2的對應的數據2放到排序後數組中,效果以下:
依次類推,取出統計數組中的數據放到排序後的數組中,中間省略部分。
輔助數組索引5的對應的數據5放到排序後數組中,效果以下:
再次將輔助數組索引5的對應的數據5放到排序後數組中,效果以下:
依次類推,中間省略部分。
最終排序後的效果以下:
計數排序代碼編寫
public class CountSortTest { public static void main(String[] args) { int[] arr = new int[] {6, 8, 9, 5, 3, 2, 1, 7, 8, 5}; countSort(arr); System.out.println("排序後:" + Arrays.toString(arr)); } // 假設咱們要排序的數據是10個0到9的隨機數字 public static void countSort(int[] arr) { // 1.獲得待排序數據的最大值 int max = arr[0]; for (int i = 1; i < arr.length; i++) { int num = arr[i]; if (num > max) max = num; } // 2.建立一個輔助數組長度是最大值+1 int[] countArr = new int[max+1]; // 3.遍歷待排序的數據,放到輔助數組中進行統計 for (int i = 0; i < arr.length; i++) { // arr[i]取出待排序的數據,假設是5 // countArr[arr[i]] 就是 countArr[5] // countArr[5]++; countArr[arr[i]]++; // 取出待排序的數據,找到數據在輔助數組對應的索引,數量加1 } // 4.遍歷輔助數據,將統計到的待排序數據放到已排序的數組中 int index = 0; // 用於記錄當前數據放到已排序數組的哪一個位置 // 已排序數組和待排序數組同樣長 int[] sortedArr = new int[arr.length]; for (int i = 0; i < countArr.length; i++) { while (countArr[i] > 0) { sortedArr[index++] = i; countArr[i]--; } } // 5.將已排序的數據放到待排序的數組中 for (int i = 0; i < sortedArr.length; i++) { arr[i] = sortedArr[i]; } } }
計數排序優化1
對任意指定範圍內的數字進行排序。
剛纔咱們的計數排序規定數據是 0 ~ 9這十個範圍內的數字。有可能排序的數據不是從0開始,例以下面這個數{68, 65, 72, 74, 73, 72, 70, 71, 69, 70, 67, 70, 66}是65 ~ 74這十個範圍內的數字。咱們按照剛纔的代碼輔助數組的長度須要爲75,其實這是沒有必要的,咱們能夠看到0 ~ 64這個範圍內根本沒有數字。浪費了數組上0 ~ 63索引位置上的存儲空間。咱們能夠把65放到索引0,66放到索引1,依次類推,效果以下圖:
經過上圖能夠看到,咱們須要找到待排數據中的最小值和最大值,使用(最大值-最小值+1)來做爲輔助數組的長度。最小值放到輔助數組0索引的位置,依次日後推。
輔助數組的長度=10 (74-65+1)
69元素在輔助數組的位置=4 (69-65)
優化後代碼:
public class CountSortTest2 { public static void main(String[] args) { int[] arr = new int[] {68, 65, 72, 74, 73, 72, 70, 71, 69, 70, 67, 70, 66}; countSort(arr); System.out.println("排序後:" + Arrays.toString(arr)); } // 假設咱們要排序的數據是10個65到74的隨機數字 public static void countSort(int[] arr) { // 1.獲得待排序數據的最大/最小值 int max = arr[0]; int min = arr[0]; for (int i = 1; i < arr.length; i++) { int num = arr[i]; if (num > max) max = num; else if (num < min) min = num; } // 2.建立一個輔助數組長度是最大值+1 int[] countArr = new int[max-min+1]; // 3.遍歷待排序的數據,放到輔助數組中進行統計 for (int i = 0; i < arr.length; i++) { // arr[i]取出待排序的數據,假設是69 // countArr[arr[i]-min] 就是 countArr[69-65] // countArr[4]++; countArr[arr[i]-min]++; // 取出待排序的數據,找到數據在輔助數組對應的索引,數量加1 } // 4.遍歷輔助數據,將統計到的待排序數據放到已排序的數組中 int index = 0; // 用於記錄當前數據放到已排序數組的哪一個位置 // 已排序數組和待排序數組同樣長 int[] sortedArr = new int[arr.length]; for (int i = 0; i < countArr.length; i++) { while (countArr[i] > 0) { sortedArr[index++] = min + i; countArr[i]--; } } // 5.將已排序的數據放到待排序的數組中 for (int i = 0; i < sortedArr.length; i++) { arr[i] = sortedArr[i]; } } }
計數排序優化2
保證計數排序的穩定性。
到目前爲止,咱們的計數排序能夠實現必定範圍內的排序,可是還存在一個問題,相同的數據咱們排序時沒有保證順序,也就是說如今的計數排序時不穩定的排序,以下圖所示:
從上圖能夠看到待排序中有3個相同的數據70,經過計數排序後,這3個70的位置改變了。那麼如何保證計數排序的穩定性呢?我要先分析一下爲何會形成不穩定,上面說過計數排序分紅兩個過程:1.統計過程
,2.排序過程
。問題就出如今這兩個過程當中。
咱們先來看一下統計的過程:
第一次統計紫色數字70,效果以下:
第二次統紅色計數字70,效果以下:
第三次統計灰色數字70,效果以下:
咱們再來看一下排序的過程:
第一次排序,排序的是第三個灰色數字70,效果以下:
第二次排序,排序的是第二個紅色數字70,效果以下:
第三次排序,排序的是第一個紫色色數字70,效果以下:
經過上面的分析咱們就知道致使計數排序是不穩定排序的緣由了,統計時最後一個灰色的數字70在排序時被第一個取出排序,第一個紫色的70被最後一次取出排序。總結就是統計時的順序和排序時的順序不對應。知道緣由瞭解決就好辦了。
要讓計數排序是穩定排序,只要保證統計時和排序時操做相同數字的順序是對應的(後統計的先參與排序)。以下圖所示:
統計時第三個灰色的數字70第一次取出放到合適的地方,以下圖:
統計時第二個紅色的數字70第二次取出放到合適的地方,以下圖:
統計時第一個紫色的數字70第三次取出放到合適的地方,以下圖:
如何作到上圖中的相同數據後統計的先參與排序
,這個地方有點繞,要注意啦!咱們須要保證兩點:
- 統計時,計算相同數據具體保存的位置。
- 排序時,從待排序數組倒序遍歷,從後往前獲取數據。
咱們先看第一點:統計時,計算相同數據具體保存的位置
。 輔助數組在統計當前元素數量
時加上以前元素的數量
,就能夠肯定當前元素所在的位置,效果以下:
咱們再看第二點:排序時,從待排序數組倒序遍歷
,從後往前獲取數據,能夠保證後統計的數據先參與排序。動畫效果以下:
優化後代碼以下:
public class CountSortTest3 { public static void main(String[] args) { int[] arr = new int[] {68, 65, 72, 74, 73, 72, 70, 71, 69, 70, 67, 70, 66}; countSort(arr); System.out.println("排序後:" + Arrays.toString(arr)); } // 假設咱們要排序的數據是10個65到74的隨機數字 public static void countSort(int[] arr) { // 1.獲得待排序數據的最大/最小值 int max = arr[0]; int min = arr[0]; for (int i = 1; i < arr.length; i++) { int num = arr[i]; if (num > max) max = num; else if (num < min) min = num; } // 2.建立一個輔助數組長度是最大值+1 int[] countArr = new int[max-min+1]; // 3.遍歷待排序的數據,放到輔助數組中進行統計 for (int i = 0; i < arr.length; i++) { // arr[i]取出待排序的數據,假設是69 // countArr[arr[i]-min] 就是 countArr[69-65] // countArr[4]++; countArr[arr[i]-min]++; // 取出待排序的數據,找到數據在輔助數組對應的索引,數量加1 } // 4.對輔助數組進行加工處理 for (int i = 1; i < countArr.length; i++) { countArr[i] += countArr[i-1]; } System.out.println("Arrays.toString() = " + Arrays.toString(countArr)); // 5.倒序遍歷源數組 // 已排序數組和待排序數組同樣長 int[] sortedArr = new int[arr.length]; for (int i = arr.length-1; i >= 0; i--) { // 獲得這個待排序的數據`arr[i]`,去輔助數組中找到合適的位置`arr[i]-min`,放到已排序數組中`countArr[arr[i]-min]` // arr[i]: 待排序的數據 // arr[i]-min: 待排序的數據在輔助數組中的位置 // countArr[arr[i]-min-1]: 待排序數據再已排序數組的位置 sortedArr[countArr[arr[i]-min]-1] = arr[i]; // 輔助數組中該數據的數量減一,也就是後續相同數據放到前面一個位置 countArr[arr[i]-min]--; } // 6.將已排序的數據放到待排序的數組中 for (int i = 0; i < sortedArr.length; i++) { arr[i] = sortedArr[i]; } } }
計數排序的複雜度
假設數據規模爲n,數據範圍爲k。
計數排序的空間複雜度:輔助數組須要m個空間,排序後的數組和待排序數組是同樣長的,因此總的空間複雜度是O(n+m)
。
計數排序的時間複雜度:1.獲得待排序數據的最大/最小值遍歷一次源數組操做次數爲n,3.遍歷待排序的數據,放到輔助數組中進行統計操做次數爲n,4.對輔助數組進行加工處理操做次數爲k,5.倒序遍歷源數組操做次數爲n,6.將已排序的數據放到待排序的數組中操做次數爲n,總操做次數爲:4n+m。因此總的時間複雜度爲O(n+k)
。
計數排序的侷限性
-
待排序數據範圍過大不適用於計數排序。假設有100個整數,他們的範圍是0到兩千萬,若是使用計數排序須要一個長度爲一千萬零一的數組,其中只有100位置存儲了數據,剩餘都是沒有存儲數據,浪費空間。若是有負整數,能夠加上一個固定的常數使得待排序列的最小值爲0。
-
待排序數據不是整數不適用於計數排序。假設有100個小數,他們的範圍是0到1,可是0到1之間的小數有無數個,沒法使用計數排序進行排序,由於連開闢多大的輔助數組都不能肯定。
總結
計數排序就是一個非基於比較的排序算法,它的優點在於在對必定範圍內的整數排序時,它的複雜度爲Ο(n+k)(其中k是整數的範圍),快於任何比較排序算法。
計數排序適用於數據量很大,可是數據的範圍比較小的狀況。好比對一個公司20萬人的年齡進行排序,要排序的數據量很大,可是年齡分佈在0 ~ 134歲之間(最大年齡數據來源吉尼斯世界記錄)。
計數排序的思想:使用一個輔助數組,遍歷待排序的數據,待排序數據的值就是輔助數組的索引,輔助數組索引對應的位置保存這個待排序數據出現的次數。最後從輔助數組中取出待排序的數據,放到排序後的數組中。
原創文章和動畫製做真心不易,您的點贊就是最大的支持! 想了解更多文章請關注微信公衆號:表哥動畫學編程