hi~上期我们学习了堆排序,是对二叉堆的再次应用,比较片面。这种的方法的事假复杂度是O(nlogn)有没有这更快的算法呢?
有的,我们本期就是学习这个算法-计数排序,什么叫做计数排序呢?
首先我们回顾一下以前学习算法,不管是冒泡排序,还是快速排序,堆排序等等都是基于元素的之间的比较来进行排序的。但是有一种特殊的排序算法叫做计数排序,他是利用数组下标来确定元素的正确位置。
计数排序是如何进行排序的呢?
例:有一个无序的数组:9,3,5,4,9,1,2,7,8,1,3,6,5,3,4,0,10,9 ,7,9
然后我们根据数组的取值范围建立一个[最小值,最大值]的数组区间。如上所示,建立一个长度为11的数组,数组下标从0-10,初始值设置为0。
那么如何给这个无序的数组进行排序呢?
操作步骤:我们遍历这个无序的随机数列,每一个整数按照其值对号入座,对应数组下标的元素进行加一操作。比如第一个整数是9,那么数组下标为9的元素加1:
第二个整数是3,那么数组下标为3的元素加1:
继续遍历数列并修改数组后...
最终,数组的状态如下:
数组的每一个下标位置的值,代表了数列中对应整数的出现的次数。
有了这个“计数结果”,排序就很简单了,直接遍历数组,输出元素的下标值,元素的值是几就输出几次。如下所示:
0,1,1,2,3,3,3,4,4,5,5,6,7,7,8,9,9,9,9,10
显然这个输出的数列已经是有序的了。
这就是计数排序的基本过程,它适合一定范围的整数培训,再取汁的范围不是很大的情况下,他的性能是大于那些(nlogn)的排序算法。
我们先来看看这个代码是如何实现的。
public class CountSort {
public static int[] countSort(int[] array) {
//得到数列的最大值
int max = array[0];
for (int i : array) {
if (i > max) {
max = i;
}
}
//根据数列最大值确定统计数组的长度
int[] countArray = new int[max + 1];
//遍历数列,填充统计数组
for (int i = 0; i < array.length; i++) {
countArray[array[i]]++;
}
//遍历统计数组,输出结果
int index = 0;
int[] sortedArray = new int[array.length];
for (int i = 0; i < countArray.length; i++) {
for (int j = 0; j < countArray[i]; j++) {
sortedArray[index++] = i;
}
}
return sortedArray;
}
public static void main(String[] args) {
int[] array = {9, 3, 5, 4, 9, 1, 2, 7, 8, 1, 3, 6, 5, 3, 4, 0, 10, 9, 7, 9};
int[] ints = countSort(array);
System.out.println(Arrays.toString(ints));
}
}
从功能上,这段代码是实现了整数的排序。但是这段代码也存在一些问题,你有发现么?
是的,如果通过最大值来确定数组的长度的话就会出现问题,比如一组数据,900~1000,其实就100个数字,但是你通过长度来算的话就是1000笔数据,0~899其实是没有用的。那我们改怎么去优化呢?
解决方式:
我们不再以输入数列的最大值+1作为统计数组的长度,而是以数列的最大值和最小值的差+1作为数组的长度。同时,数列的最小值作为一个偏移量,用于统计数组的对号入座。
比如这个数列:95,94,91,98,99,90,99,93,91,92
- 统计数组长度为 99-90=10,偏移量等于数列的最小值90。
- 对于第一个整数95,对应的统计数组下标是95-90=5,如图所示:
那这个代码改如何实现呢?
public class CountSortPlus {
public static void countSort(int[] array){
//根据差值确定数组长度
int min=array[0]; int max=array[0];
for (int i = 1; i <array.length ; i++) {
if (max<array[i]){
max=array[i];
}
if (min>array[i]){
min=array[i];
}
}
//构建新的数组
int[] countArray = new int[max - min + 1];
//遍历无序数组,并根据无序数组的值来更改countArray
for (int i = 0; i <array.length ; i++) {
//以最小值为基准
int index = array[i] - min;
countArray[index]=countArray[index]+1;
}
//遍历countArray 值是多少就遍历几次
int flag=0;
for (int i = 0; i <countArray.length ; i++) {
for (int j = 0; j <countArray[i] ; j++) {
array[flag++]=i+min;
}
}
System.out.println(Arrays.toString(array));
}
public static void main(String[] args) {
int[] array = {95, 93, 95, 99, 99, 91, 92, 97, 98, 91, 93, 96, 95, 93, 94, 90, 100, 99, 97, 99};
countSort(array);
}
}
进阶版:
如果我们想要原始数组中的相同元素按照本来的顺序的排列,那该怎么处理呢?以前面代码里的数组为例
int[] array = {95, 93, 95, 99, 99, 91, 92, 97, 98, 91, 93, 96, 95, 93, 94, 90, 100, 99, 97, 99};
比如这里有四个99,那我想要他们保持原来顺序。那我就要知道他们的初始顺序。然后在排序的时候将他们顺序保持不变即可。
那么具体代码是如何实现呢?
public static int[] countSort(int[] array) {
int min=array[0]; int max=array[0];
for (int i = 1; i <array.length ; i++) {
min=Math.min(min,array[i]);
max=Math.max(max,array[i]);
}
//构建新的数组+1
int[] countArray = new int[max - min + 1+1];
//遍历无序数组,并根据无序数组的值来更改countArray 令countArray[0]永远为0
for (int i = 0; i <array.length ; i++) {
//以最小值为基准
int index = array[i] - min+1;
countArray[index]=countArray[index]+1;
}
//统计数组->变型,后面元素等于前面元素之和,元素的值表示这个数出现的次数,那么后面的元素就表示他在新的数组的位置
int sum=0;
for (int i = 1; i <countArray.length ; i++) {
sum+=countArray[i];
countArray[i]=sum;
}
//遍历
int[] ints = new int[array.length];
for (int i = 0; i <array.length ; i++) {
ints[countArray[array[i]-min]]=array[i];
countArray[array[i]-min]++;
}
return ints;
}
综上所诉,我们可以看出计数排序的时间复杂度为O(N+M)所以可以归于)O(N)。但是为什么我们在开发中使用频率并不高呢?因为计数排序的有两个前提:
- 一是需要排序的元素必须是整数
- 二是排序元素的取值要在一定范围内,并且比较集中。
只有这两个条件都满足,才能最大程度发挥计数排序的优势。但是对于这种局限性,还有另外一种排序算法做了弥补,那就是下期我们学习的桶排序算法。下期见~