算法练习(5)-计数排序法及优化
日常开发中,会遇到一些特定的排序场景:“待排序的值”范围很明细,比如:基金的星级排名,客服的好评星级排名,一般星级排名也就从1星到5星。这种情况下,有一个经典的“下标计数排序法”,可以用O(n)的时间复杂度完成排序:
static void sort1(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
int[] rates = new int[5];
//统计每个元素出现的次数
for (int i = 0; i < arr.length; i++) {
rates[arr[i] - 1]++;
}
//辅助调试用
System.out.println(Arrays.toString(rates));
//根据计数结果,重新填充到原数组
int cur = 0;
for (int i = 0; i < rates.length; i++) {
//如果该位置的统计值>0,说明出现了多个,依次填充即可
//加上cur<arr.length是用于优化所有元素都是最小值的情况,后面的位置就不用看了
for (int j = 0; j < rates[i] && cur < arr.length; j++) {
arr[cur++] = i + 1;
}
}
}
public static void main(String[] args) {
int[] arr = new int[]{5, 5, 4, 1, 1, 1};
System.out.println(Arrays.toString(arr));
sort1(arr);
System.out.println(Arrays.toString(arr));
}
输出:
[5, 5, 4, 1, 1, 1] [3, 0, 0, 1, 2] [1, 1, 1, 4, 5, 5]
但这是一个不稳定的排序算法,如果想改进稳定的算法,有一种比较巧妙的做法:
static int[] sort2(int[] arr) {
if (arr == null || arr.length < 2) {
return arr;
}
int[] buckets = new int[5];
for (int i = 0; i < arr.length; i++) {
buckets[arr[i] - 1]++;
}
//辅助调试用
System.out.println("\tdebug=> " + Arrays.toString(buckets));
//处理成"前缀和"
for (int i = 1; i < buckets.length; i++) {
buckets[i] += buckets[i - 1];
}
//辅助调试用
System.out.println("\tdebug=> " + Arrays.toString(buckets));
//根据计数结果,生成有序数组
int[] result = new int[arr.length];
for (int i = arr.length - 1; i >= 0; i--) {
int t = arr[i];
result[buckets[t - 1] - 1] = t;
buckets[t - 1]--;
}
//辅助调试用
System.out.println("\tdebug=> " + Arrays.toString(buckets));
return result;
}
写段测试,跑一下:
int[] arr = new int[]{1, 4, 5, 2, 3, 4, 5, 5};
System.out.println(Arrays.toString(arr));
sort1(arr);
System.out.println(Arrays.toString(arr));
System.out.println("------------------------");
arr = new int[]{1, 4, 5, 2, 3, 4, 5, 5};
System.out.println(Arrays.toString(arr));
int[] result = sort2(arr);
System.out.println(Arrays.toString(result));
输出 :
[1, 4, 5, 2, 3, 4, 5, 5] debug=> [1, 1, 1, 2, 3] [1, 2, 3, 4, 4, 5, 5, 5] ------------------------ [1, 4, 5, 2, 3, 4, 5, 5] debug=> [1, 1, 1, 2, 3] debug=> [1, 2, 3, 5, 8] debug=> [0, 1, 2, 3, 5] [1, 2, 3, 4, 4, 5, 5, 5]
方法2,为啥能保证稳定呢? 关于在于"前缀和"这一步处理, 相当于把"计数+位置"这二种信息合成在一起了, 可能有点难理解.
debug=> [1, 1, 1, 2, 3] debug=> [1, 2, 3, 5, 8]
输出的这2行调试信息里:
第1行的[1, 1, 1, 2, 3]表示1出现1次, 2出现1次, 3次出1次, 4次出2次, 5出现3次. 然后把每1项,处理成前2项求和后, 变成
第2行的[1, 2, 3, 5, 8]表示<=1的元素个数为1, <=2的元素个数为2, <=3的元素个数为3, <=4的元素个数为5, <=5的元素个数为8,相同的元素越在后面出现,这样累加的值就越大,所以相当于变相的用计数的大小,蕴含了元素的相对位置信息。建议初次接触此解法的同学,多断点调试几次,观察每1步各变量的情况。
这个方法看似巧妙,但个人觉得有点鸡肋,原因二点:
1、需要引入1个与原数组规模相同的额外数组存放最后的有序结果,额外空间又扩大了一倍
2、思路不易于理解
既然都是空间换时间,还不如搞简单点,在每个槽位放一个list,相同元素依次放进该槽位的list(这样用list.size代替计数),由于list天然保证元素放入的顺序不变,所以能保证最终排序结果的稳定性:
static void sort2_2(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
List<List<Integer>> buckets = new ArrayList<>(5);
for (int i = 0; i < 5; i++) {
buckets.add(new ArrayList<>(arr.length));
}
//每个元素放入指定的槽位
for (int i = 0; i < arr.length; i++) {
buckets.get(arr[i] - 1).add(arr[i]);
}
//辅助调试用
System.out.println("\tdebug=> " + buckets);
//根据计数结果,重新填充到原数组
int cur = 0;
for (int i = 0; i < buckets.size(); i++) {
//如果该位置的list非空,说明出现了多个,依次填充即可
//加上cur<arr.length是用于优化所有元素都是最小值的情况,后面的位置就不用看了
for (int j = 0; j < buckets.get(i).size() && cur < arr.length; j++) {
arr[cur++] = buckets.get(i).get(j);
}
}
}
与刚才看似精妙的方法结果一样,但理解起来容易多了。跑测试的话,输出结果如下:
[1, 4, 5, 2, 3, 4, 5, 5] debug=> [[1], [2], [3], [4, 4], [5, 5, 5]] [1, 2, 3, 4, 4, 5, 5, 5]
简单int[]类型的数组, 可能看不出稳定排序与非稳定排序的区别, 我们可以把数据类型弄复杂点:
[客服A:1, 客服B:4, 客服C:5, 客服D:2, 客服E:3, 客服F:4, 客服G:5, 客服H:5]
static class Score {
public String name;
public int val;
public Score(String name, int val) {
this.name = name;
this.val = val;
}
@Override
public String toString() {
return this.name + ":" + this.val;
}
}
按刚才的思路,要实现稳定的计数排序, 有2种写法:
static void sort3(Score[] arr) {
if (arr == null || arr.length < 2) {
return;
}
List<List<Score>> buckets = new ArrayList<>(5);
for (int i = 0; i < 5; i++) {
buckets.add(new ArrayList<>());
}
for (int i = 0; i < arr.length; i++) {
buckets.get(arr[i].val - 1).add(arr[i]);
}
//辅助调试用
System.out.println("\tdebug=> " + buckets);
//根据计数结果,重新填充到原数组
int cur = 0;
for (int i = 0; i < buckets.size(); i++) {
//如果该位置的统计值>0,说明出现了多个,依次填充即可
//加上cur<arr.length是用于优化所有元素都是最小值的情况,后面的位置就不用看了
for (int j = 0; j < buckets.get(i).size() && cur < arr.length; j++) {
arr[cur++] = buckets.get(i).get(j);
}
}
}
static Score[] sort4(Score[] arr) {
if (arr == null || arr.length < 2) {
return arr;
}
int[] buckets = new int[5];
for (int i = 0; i < arr.length; i++) {
buckets[arr[i].val - 1]++;
}
//辅助调试用
System.out.println("\tdebug=> " + Arrays.toString(buckets));
//处理成前缀合
for (int i = 1; i < buckets.length; i++) {
buckets[i] += buckets[i - 1];
}
//辅助调试用
System.out.println("\tdebug=> " + Arrays.toString(buckets));
//根据计数结果,生成有序数组
Score[] result = new Score[arr.length];
for (int i = arr.length - 1; i >= 0; i--) {
Score t = arr[i];
result[buckets[t.val - 1] - 1] = t;
buckets[t.val - 1]--;
}
//辅助调试用
System.out.println("\tdebug=> " + Arrays.toString(buckets));
return result;
}
测试代码:
public static void main(String[] args) {
Score[] arr3 = new Score[]{
new Score("客服A", 1),
new Score("客服B", 4),
new Score("客服C", 5),
new Score("客服D", 2),
new Score("客服E", 3),
new Score("客服F", 4),
new Score("客服G", 5),
new Score("客服H", 5)
};
System.out.println(Arrays.toString(arr3));
sort3(arr3);
System.out.println(Arrays.toString(arr3));
System.out.println("------------------------");
Score[] arr4 = new Score[]{
new Score("客服A", 1),
new Score("客服B", 4),
new Score("客服C", 5),
new Score("客服D", 2),
new Score("客服E", 3),
new Score("客服F", 4),
new Score("客服G", 5),
new Score("客服H", 5)
};
System.out.println(Arrays.toString(arr4));
Score[] result = sort4(arr4);
System.out.println(Arrays.toString(result));
}
输出:
[客服A:1, 客服B:4, 客服C:5, 客服D:2, 客服E:3, 客服F:4, 客服G:5, 客服H:5] debug=> [[客服A:1], [客服D:2], [客服E:3], [客服B:4, 客服F:4], [客服C:5, 客服G:5, 客服H:5]] [客服A:1, 客服D:2, 客服E:3, 客服B:4, 客服F:4, 客服C:5, 客服G:5, 客服H:5] ------------------------ [客服A:1, 客服B:4, 客服C:5, 客服D:2, 客服E:3, 客服F:4, 客服G:5, 客服H:5] debug=> [1, 1, 1, 2, 3] debug=> [1, 2, 3, 5, 8] debug=> [0, 1, 2, 3, 5] [客服A:1, 客服D:2, 客服E:3, 客服B:4, 客服F:4, 客服C:5, 客服G:5, 客服H:5]
出处:http://yjmyzz.cnblogs.com
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
浙公网安备 33010602011771号