计数排序
计数排序由 HaroldH.Seward 于1954年提出,它是一种非基于比较的排序算法,通过辅助数组来确定各元素的最终位置。因为在排序过程中不存在元素之间的比较和交换操作,所以当待排序数组为整数且数组内数据的范围较小时,其优势是十分明显的。
1. 算法思想
对待排序数组中的每一个元素分别进行计数,确定整个数组中小于当前元素的个数,计数完成后便可以按照各计数值将各元素直接存放在已排序的数组中。
注意:当存在多个相同的值时,不可以把它们放在同一位置上。需要在代码中进行适当的修改。
2. 算法步骤
(1)根据待排序序列中的最大元素和最小元素确定计数数组c的空间大小。
(2) 统计待排序序列中每个元素i出现的次数并存入c[i-1]中。
(3)对c中所有的值进行累加,后者为其前面所有的值的总和。
(4)将每个元素放入已排序序列的第c[i]项,每放入一个元素,c[i]便减1。
(5)所有的元素都放入或者c[i]均为0之后,排序结束。
3. 算法分析
第一个循环用于记录每个元素出现的次数,复杂度为 O(n);第二个循环用于对计数数组进行累加,复杂度为O(k),k为申请空间的大小(即差值范围):第三个循环用于反向填充已排序序列,复杂度为O(n)。总结可得,计数排序的时间复杂度为O(n+k)。
正如前文所说的“以空间换时间”——虽然十分消耗空间,但只要 O ( k ) < O ( n l o g 2 n ) O(k)<O(nlog_2n) O(k)<O(nlog2n),计数排序便可快于任何比较排序算法。因比较排序算法的时间复杂度存在一个理论边界—— O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)。
在比较排序算法中,如果存在n个元素,则会形成n!个排列个数,也就是说这个序列构成的决策树叶子节点个数为n!。由叶子节点的个数又可以知道,这棵决策树的高度为 l o g 2 ( n ! ) log_2(n!) log2(n!),也就是说从这棵树的根节点到叶子节点需要比较 l o g 2 ( n ! ) log_2(n!) log2(n!)次。根据斯特灵公式,已知:
n ! ≈ 2 π n { n e } n n!\approx\sqrt{2\pi n}\{\frac{n}{e}\}^n n!≈2πn{en}n
所以:
O { l o g 2 ( n ! ) } ≈ O { l o g 2 { 2 π n { n e n } } ≈ O ( n l o g 2 n ) O\{log_2(n!)\}\approx O\{log_2\{\sqrt{2\pi n}\{\frac{n}{e}^n\}\}\approx O(nlog_2n) O{log2(n!)}≈O{log2{2πn{enn}}≈O(nlog2n)
注意:如果 O ( k ) < O ( n l o g 2 n ) O(k)<O(nlog_2 n) O(k)<O(nlog2n),那么其反而不如基于比较的排序算法(如快速排序、堆排序和归并排序)快。
由于我们额外申请了一个大小为k的计数数组和一个与待排序数组同样大小的排序空间,所以空间复杂度为 O(n+k),而且计数排序属于稳定排序。
结合时间复杂度和空间复杂度不难发现计数排序存在的弊端:待排序数组中的最大值和最小值之差不可太大,否则会使开销增大,性能变差;因为我们是以元素值为下标,所以在待排序数组中最好不要存在浮点数或其他形式的元素,否则还需要对元素值和元素差值进行相应的转换。因此,一定要谨慎使用计数排序。
4. 算法代码
算法代码如下:
Python
# -*- coding: utf-8 -*-# 计数排序
def count_sort(arr):# 计算序列的最大值和最小值maximum, minimum = max(arr),min(arr)# 申请额外的空间作为计数数组,大小为最大值减最小值+1countArr = [0 for i in range(maximum - minimum + 1)]# 申请一个存放已排序序列的等长空间finalArr =[0 for i in range(len(arr))]# 统计各元素出现的次数,并将统计结果存入计数数组中for i in arr:# 出现一次就加 1countArr[i-minimum]+= 1 # 注意下标 index = data - min# 对计数数组进行累加for i in range(1, len(countArr)):# 从第二个位置开始,每个位置的值等于其本身加上前身的值countArr[i]+= countArr[i-1]# 开始把元素反向填充至最终的数组中for i in arr:finalArr[countArr [i-minimum]-1] = i#填充了一个就减一countArr[i-minimum] -= 1return finalArr
#调用count_sort 函数
print(count_sort([34,21,-2, 13,2,5,1,55,-3,3,1,8]))
Java
public static List<Integer> countSort(List<Integer> arr) {// 计算序列的最大值和最小值int maxPositive = Integer.MIN_VALUE, minNegative = Integer.MAX_VALUE;for (int i : arr) {if (i > 0) {maxPositive = Math.max(maxPositive, i);} else if (i < 0) {minNegative = Math.min(minNegative, i);}}// 分别申请额外的空间作为正数计数数组、负数计数数组和临时存储列表int positiveRange = maxPositive + 1;int[] positiveCountArr = new int[positiveRange];Arrays.fill(positiveCountArr, 0);int negativeRange = Math.abs(minNegative) + 1;int[] negativeCountArr = new int[negativeRange];Arrays.fill(negativeCountArr, 0);// 统计各元素出现的次数,并将统计结果存入相应的计数数组中for (int num : arr) {if (num >= 0) {positiveCountArr[num]++;} else {negativeCountArr[Math.abs(num)]++;}}// 开始把元素反向填充至最终的数组中List<Integer> finalArr = new ArrayList<>(arr.size());// 先添加负数for (int i = negativeCountArr.length - 1; i >= 1; i--) {while (negativeCountArr[i]-- > 0) {finalArr.add(-i);}}// 添加零(如果有的话)if (positiveCountArr[0] > 0) {while (positiveCountArr[0]-- > 0) {finalArr.add(0);}}// 添加正数for (int i = 1; i < positiveCountArr.length; i++) {while (positiveCountArr[i]-- > 0) {finalArr.add(i);}}return finalArr;}@Testvoid contextLoads () {List<Integer> arr = List.of(34, 21, -2, 13, 2, 5, 1, 55, -3, 3, 1, 8);List<Integer> sortedArr = countSort(arr);System.out.println(sortedArr);}
5. 输出结果
代码输出结果如下:
6. 算法过程分解
待排序序列[-2,2,5,1,-3,3,1]
最大值 5,最小值为-3
所以,需申请的计数数组空间为9
计数数组如下:
累加更新后,计数数组如下;