内容介绍
计数排序简介
咱们知道目前排序速度最快的是时间复杂度为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岁之间(最大年龄数据来源吉尼斯世界记录)。
计数排序的思想:使用一个辅助数组,遍历待排序的数据,待排序数据的值就是辅助数组的索引,辅助数组索引对应的位置保存这个待排序数据出现的次数。最后从辅助数组中取出待排序的数据,放到排序后的数组中。
原创文章和动画制做真心不易,您的点赞就是最大的支持! 想了解更多文章请关注微信公众号:表哥动画学编程