算法练习(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
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。