计数排序与稳定排序
我们常用的排序算法,有冒泡算法、快速算法等,它们都是基于元素之间的比较来进行排序,有一种特殊的算法不是基于元素比较,而是利用数组下标来确定元素在数组的位置,这种算法就是“计数排序”。
先来说一下实现的原理,假设有20个随机整数的数组array,他们值分别是:9,3,5,4,9,1,2,7,8,1,3,6,5,3,4,0,10,9,7,9.有没有发现这些整数的大小都是在[0,10]区间内,这时我们可以定义一个长度为11的数组countArray,每个元素的初始值都为0.当我遍历这20个随机整数时,每个值在countArray的下标位置对号入座。
注意:不是赋值,而是每有一个值,就在对应位置+1,譬如遍历array的第一位元素9,就会在array[9]位置+1,遍历array的第二位元素3时,就在countArray[3]位置+1,如此类推。
最终遍历整个array数组后,countArray数组变成以下情况:
value: | 1 | 2 | 1 | 3 | 2 | 2 | 1 | 2 | 1 | 4 | 1 |
index: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
每一个value值对应表示下标值出现的次数,有了这个统计结果,那么重新排序就显得简单粗暴。
直接依次输出排序后的数组结果:0,1,1,2,3,3,3,4,4,5,5,6,7,7,8,9,9,9,9,10
下面是代码实现:
public int[] countSort(int[] array){
//获取array数组中元素的最大值
int max = array[0];
for(int i=1; i<array.length; i++){
if(array[i]>max){
max = array[i];
}
}
//根据数组的最大值确定统计数组的长度
int[] countArray = new int[max+1];
//遍历array,填充countArray
for(int i=0; i<array.length; i++){
countArray[array[i]]++;
}
//遍历统计数组,输出结果
int index = 0;
int[] sortArray = new int[array.length];
for(int i=0; i<countArray.length; i++){
for(int j=0; j<countArray[i]; j++){
sortArray[index++] = i;
}
}
return sortArray;
}
好了,代码已经给出来了,必须注意一点是上面代码有不少局限性,首先获取array数组的最大值+1作为countArray数组的长度,不严谨,其次,这是在数组元素值为[0,10]区间的整数才可以。
下面给出改进栗子:
原始无序数组array:
value | 95 | 94 | 91 | 98 | 99 | 90 | 99 | 93 | 91 | 92 |
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
对于统计数组countArray,我们采用array数组的(最大值-最小值+1)作为countArray的长度,即(99-90+1)=10,用array数组的最小值90作为countArray数组的偏移量,譬如95,则(95-90)=5,会在countArray[5]的值加1,对于纯碎的整数排序,只要在上面代码稍微修改一下就ok了,但是对于现实业务排序,就显得很鸡肋,譬如学生的成绩排序,
姓名 | 成绩 |
---|---|
Sam | 90 |
Tom | 99 |
Jack | 95 |
Jane | 94 |
Kan | 95 |
按照原来的统计思路,可以得到下面的统计数组:
value | 1 | 0 | 0 | 0 | 1 | 2 | 0 | 0 | 0 | 1 |
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
这种情况下,如果成绩相同,应当按照固有顺序排序,但是,怎么确定原来的固有顺序对应的人呢?现在就要对统计数组进行变变形,统计数组从第二个元素开始,每个元素的值都加上前面所有元素的值的和。
value | 1 | 1 | 1 | 1 | 2 | 4 | 4 | 4 | 4 | 5 |
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
为什么要相加呢?目的是存储当前元素值的最后位置,譬如下标为9的元素,不管有多少个值为99的元素,反正该值的元素最后一个位置是5.
倒序遍历array数组,第一个被遍历的元素Kan的95,找到countArray数组下标为5的value值,是4,代表Kan是在输出数组sortArray中排的位置是第四位(即,是输出数组sortArray[3]的值,注意,这个下标值可以通过countArray对应的元素值减1得到,countArray对应的元素值又可以通过countArray[array[i]-min]得到),然后countArray数组下标为5的value值相应减去1,4—>3,依次遍历,过程中再遇到95,那么该95排的位置是第三位,如此类推。这样一来,就可以清楚的将同样是95分的Jack和Kan排出顺序来,这就是稳定排序。
代码实现:
public int[] countSort(int[] array){
//得到array数组的最大值和最小值,以便计算得到countArray的长度
int max = array[0];
int min = array[0];
for(int i=1; i<array.length; i++){
if(array[i]>max){
max = array[i];
}
if(array[i]<min){
min = array[i];
}
}
int[] countArray = new int[max-min+1];
//统计对应元素的个数
for(int i=0; i<array.length; i++){
countArray[array[i]-min]++;
}
//对统计数组进行变形
int sum = 0;
for(int i=0; i<countArray.length; i++){
sum += countArray[i];
countArray[i] = sum;
}
//倒序遍历array数组,确定各元素的固有位置
int[] sortArray = new int[array.length];
for(int i=array.length; i>0; i--){
sortArray[countArray[array[i]-min]-1] = array[i];//这里的sortArray下标值就是上面高亮标记提到过的
countArray[array[i]-min]--;
}
return sortArray;
}
至此,计数排序和稳定排序结束,稳定排序就是在计数排序的基础上进化的,另外还要注意它们的局限性,原始数组元素的值必须都是整数,还有当数组的最大值与最小值差距较大时,使用计数排序显得没有意义了,因为计数排序是以牺牲空间复杂度来换取时间复杂度的算法。