线性排序

 我们都考一百分,就都是第一名了——用分组的方法来避免排序

之前介绍的几种排序算法都涉及到各元素间的比较、移动,能否避免或者尽量减少这些开销呢?接下来要介绍的几种“线性排序”方法或许能带给我们新思路。

一 桶排序

对数据进行分桶(分组),再按桶的顺序来处理数据。也有之前提到的“分治”的意思。有些特殊数据,例如0-100岁的人群、考试分数,都是范围相对固定,且远小于元素的数量。可以针对这个特性来设计特定的排序算法。

看个具体例子:如何按照学分来排序,代码如下

 1 import java.util.Arrays;
 2 
 3 public class BucketSort {
 4     public static void main(String[] args) {
 5         //10个学生 按学分排序 学分范围为0-5
 6         int[] numbers = {3, 2, 5, 1, 3, 5, 2, 0, 1, 2};
 7         System.out.println(Arrays.toString(numbers));
 8         //0-5学分对应6个桶(组)
 9         int bucketNum = 6;
10         int[] buckets = new int[bucketNum];
11         //统计每个桶里所属的学分的出现次数
12         for (int number : numbers) {
13             buckets[number] += 1;
14         }
15         System.out.println(Arrays.toString(buckets));
16         //按照桶的大小顺序来显示数据
17         int index = 0;
18         for (int i = 0; i < bucketNum; i++) {
19             for (int j = 0; j < buckets[i]; j++) {
20                 numbers[index] = i;
21                 index++;
22             }
23         }
24         System.out.println(Arrays.toString(numbers));
25     }
26 }

 输出说明

[3, 2, 5, 1, 3, 5, 2, 0, 1, 2]
[1, 2, 3, 2, 0, 2] #按桶的顺序统计每个学分的出现次数,这里情况比较特殊,学分的位置就是桶的编号
[0, 1, 1, 2, 2, 2, 3, 3, 5, 5] #按桶的编号从小到大打印出来就排序好了

各个学分的统计人数如图示:

可以看到代码第20行“numbers[index] = i”直接把桶编号当作了元素,其实是忽略掉了原始数组中数出现的顺序,导致了“非原地”排序。例如[2 2 1],排序后为[1 2 2],其中第一个2有可能不是[2 2 1]中的第一个2,单独看这个2是否原地没影响,但实际开发中,可能是两个不同的对象的某个属性都是2。

例如两个用户对象{"uid":3,"type":2} {"uid":4,"type":2},我们按照type属性排序时不希望后出现的对象{"uid":4,"type":2}排到前面。如果需要原地排序,可以按元素出现顺序依次保存到对应桶里,之后再按照写入顺序读取即可。

二 计数排序

可以看作是桶排序的一种扩展,在统计每个元素出现次数的同时,计算出截至到某个桶时累计的元素个数,用于还原出每个元素的具体位置。

代码如下

 1 import java.util.Arrays;
 2 
 3 public class CountingSort {
 4     public static void main(String[] args) {
 5         int[] numbers = {3, 5, 4, 1, 3, 0, 1};
 6         System.out.println(Arrays.toString(numbers));
 7         //找到最大值
 8         int max = numbers[0];
 9         for (int number : numbers) {
10             if (number > max) {
11                 max = number;
12             }
13         }
14         System.out.println("max " + max);
15 
16         int[] numbersCount = new int[max + 1];
17         for (int number : numbers) {
18             numbersCount[number] = numbersCount[number] + 1;
19         }
20         System.out.println("number count " + Arrays.toString(numbersCount));
21         //个数累计
22         int numberCount = 0;
23         for (int i = 0; i < numbersCount.length; i++) {
24             numberCount += numbersCount[i];
25             numbersCount[i] = numberCount;
26         }
27         System.out.println("number sum   " + Arrays.toString(numbersCount));
28 
29         int[] sortedNumbers = new int[numbers.length];
30         //从数组右侧开始处理
31         for (int i = numbers.length - 1; i > -1; i--) {
32             System.out.print("pos " + i + " value " + numbers[i] + " numberCount" + numbersCount[numbers[i]]);
33             int pos = numbersCount[numbers[i]] - 1;
34             numbersCount[numbers[i]] = pos;
35             sortedNumbers[pos] = numbers[i];
36             System.out.println("->" + pos + Arrays.toString(sortedNumbers));
37         }
38         System.out.println(Arrays.toString(sortedNumbers));
39     }
40 }

输出说明

[3, 5, 4, 1, 3, 0, 1]
max 5 #找到最大值
number count [1, 2, 0, 2, 1, 1] #每个分数的出现次数
number sum   [1, 3, 3, 5, 6, 7] #截至到每个桶时的分数出现次数累计
pos 6 value 1 numberCount3->2[0, 0, 1, 0, 0, 0, 0] #从右侧向左处理各个元素 取出最右侧的1,对应桶1,对应累计出现次数为3,所以元素1排序后的位置是3-1=2,【同时需要把次数-1后的值更新到桶累计次数中】
pos 5 value 0 numberCount1->0[0, 0, 1, 0, 0, 0, 0] #取元素0,对应桶0,次数为1,最终位置为1-1=0
pos 4 value 3 numberCount5->4[0, 0, 1, 0, 3, 0, 0] #取元素3,对应桶3,次数为5,最终位置为5-1=4
pos 3 value 1 numberCount2->1[0, 1, 1, 0, 3, 0, 0] #取元素1,对应桶2,次数为2,最终位置为2-1=1
pos 2 value 4 numberCount6->5[0, 1, 1, 0, 3, 4, 0] #取元素4,对应桶4,次数为6,最终位置为6-1=5
pos 1 value 5 numberCount7->6[0, 1, 1, 0, 3, 4, 5] #取元素5,对应桶5,次数为7,最终位置为7-1=6
pos 0 value 3 numberCount4->3[0, 1, 1, 3, 3, 4, 5] #取元素3,对应桶3,次数为4,最终位置为4-1=3
[0, 1, 1, 3, 3, 4, 5]

 各学分数量和累计如图:

 

排序过程如图:

三 基数排序

所谓基数排序,就是把数分成了多个分位,按照每个分位来分桶排序,例如123,分为百分位的1,十分位的2,个分位的3。

演示代码如下,注意我们用Map模拟了各个桶对应的存储文件,实际使用时可以用每个桶对应一个文件。

 1 import java.util.*;
 2 
 3 public class RadixSort {
 4     public static void main(String[] args) {
 5         String[] numbers = {"354", "130", "152", "251", "401", "210"};
 6         System.out.println(Arrays.toString(numbers));
 7         //假定数据都是3位长度(不够可以左侧补0)
 8         int numberLength = 3;
 9         //0-9对应10个桶
10         int bucketNum = 10;
11         int[] buckets = new int[bucketNum];
12 
13         //用Map模拟桶对应的10个文件,key是桶名称,value是该桶里的元素
14         Map<Integer, List<String>> numbersMap = new HashMap<>();
15 
16         //从右往左取,每次取一位 例如456,第一次取6,第二次取5,第三次取4
17         for (int i = numberLength; i > 0; i--) {
18             System.out.print("pos " + i + "\n\t");
19             for (String num : numbers) {
20                 int tmp = Integer.parseInt(num.substring(i - 1, i));
21                 System.out.print(tmp + " ");
22                 //该位出现的次数
23                 buckets[tmp] += 1;
24                 //初始化桶对应的元素列表
25                 numbersMap.putIfAbsent(tmp, new ArrayList<>());
26                 //追加对应的元素
27                 numbersMap.get(tmp).add(num);
28             }
29             System.out.println("\n\t" + Arrays.toString(buckets));
30             int numIndex = 0;
31             for (int j = 0; j < bucketNum; j++) {
32                 //跳过无数据的桶
33                 if (buckets[j] == 0) {
34                     continue;
35                 }
36                 //读取桶里的元素
37                 for (String e : numbersMap.get(j)) {
38                     //元素按“当前位”排序后的位置
39                     numbers[numIndex] = e;
40                     numIndex++;
41                 }
42                 //重置桶数量
43                 buckets[j] = 0;
44             }
45             System.out.println("\t" + Arrays.toString(numbers));
46             numbersMap.clear();
47         }
48     }
49 }

输出说明

[354, 130, 152, 251, 401, 210]
pos 3 #从右侧往左处理
	4 0 2 1 1 0 #各元素最右侧的位值
	[2, 2, 1, 0, 1, 0, 0, 0, 0, 0] #该位出现的次数统计
	[130, 210, 251, 401, 152, 354] #按从小到大的桶编号来直接取出桶的元素,就是按当前位排序后的元素,更新原有数组
pos 2 #元素右侧第二个
	3 1 5 0 5 5 #各元素的位值
	[1, 1, 0, 1, 0, 3, 0, 0, 0, 0] #次数统计
	[401, 210, 130, 251, 152, 354] #按桶排序,并更新原有数组
pos 1 #右侧第三个
	4 2 1 2 1 3 #各元素的位值 
	[0, 2, 2, 1, 1, 0, 0, 0, 0, 0] #次数统计
	[130, 152, 210, 251, 354, 401] #按桶排序,并更新原有数组

总结

三种排序方法都是对数据的分桶,按照桶自身的顺序来处理各个元素,不涉及元素间的比较,时间复杂度是“线性”的(参与排序的元素数量和算法的时间复杂度的关系是一次方函数关系,如x=2y、x=2y+1),所以统称为“线性排序”。

另外,使用时一定要根据数据特点,评估桶的数量是否可控,选择适合的算法。

 

扩展思考

1 桶数量和元素数量一样时会出现什么情况?

2 计数排序时为何从右侧开始处理元素?

3 基数排序时如果遇到小数应该怎么处理?

posted @ 2022-06-22 11:12  binary220615  阅读(109)  评论(0编辑  收藏  举报