简介
排序是用于解决问题的基本技术之一,特别是在那些与编写和实现高效算法有关的问题中。
通常情况下,排序是与搜索结合在一起的--这意味着我们首先对给定集合中的元素进行排序,然后在其中搜索一些东西,因为在一个已排序的集合中搜索一些东西通常比在一个未排序的集合中搜索更容易,因为我们可以做出有根据的猜测并对数据进行假设。
有许多算法可以有效地对元素进行排序,但在本指南中,我们将看一下如何实现 计数排序 在Java中实现。
Java中的计数排序
计数排序是一种稳定的、非比较性的排序算法,它的主要用途是对非负整数的数组进行排序。
计数排序对具有不同键值的对象的数量进行计数,然后在这些计数上应用前缀和来确定每个键在输出中的位置。像所有其他非比较性排序算法一样,计数排序也是在不对要排序的元素进行任何比较的情况下进行排序。同时,作为一种稳定的排序算法,计数排序保留了输出数组中键值相同的元素在原始数组中的排序顺序。
这种操作的结果,本质上是一个整数出现的列表,我们通常将其命名为计数数组。计数排序使用辅助的计数数组来确定元素的位置。

计数数组中的每个索引代表输入数组中的一个元素。与该索引相关的值是该元素在输入数组中出现的次数(计数)。
了解计数排序工作原理的最好方法是通过一个例子。假设我们有一个数组。
int[] arr = {0, 8, 4, 7, 9, 1, 1, 7};
为了简单起见,数组中的元素只能是个位数,即从0 到9 的数字。由于最大的数值是9 ,让我们把最大值标记为max = 9 。
这很重要,因为我们需要指定一个新的计数数组,由max + 1 元素组成。这个数组将用于计算我们给定的原始数组内每个数字的出现次数,所以我们需要将整个数组初始化为0 ,也就是。
int[] countArray = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
因为我们的数组可能有10个元素,所以每个数字都有10个0。
既然我们已经定义了我们要处理的数组,而且我们也定义了我们的计数数组来记录每个数字的出现,我们需要通过以下步骤来实现计数排序。
第一步
通过在一个单一的for 循环中浏览我们的整个数组arr ,对于从0 到n-1 的每一个i ,其中n 是arr 的元素数量,我们将通过增加countArray 中的位置arr[i] 的值来计算每个数字的出现。让我们在代码中看到这一点。
for(int i = 0; i < arr.length; i++)
countArray[arr[i]]++;
在第一步之后,我们的countArray 看起来像这样。[1, 2, 0, 0, 1, 0, 0, 2, 1, 1].
第二步
由于我们现在有了countArray ,我们继续下一步--将前缀和应用到countArray 。前缀和基本上是在我们将数组中的每一个前缀数字累加到下一个数字上时形成的,形成所有尚未看到的前缀之和。
for(int i=1; i < countArray.length; i++)
countArray[i] += countArray[i-1];
而应用这一步后,我们得到以下countArray :[1, 3, 3, 3, 4, 4, 4, 6, 7, 8]。
第三步
第三步,也是最后一步,是根据countArray 中的值计算出排序输出中的元素位置。为此,我们需要一个新的数组,我们称之为outputArray 。outputArray 的大小与我们原来的arr 相同,我们再一次将这个数组初始化为所有的零。
int[] outputArray = {0, 0, 0, 0, 0, 0, 0, 0};
正如我们前面提到的,计数排序是一种稳定的排序。如果我们把我们的arr 数组从0 迭代到n-1 ,我们最终可能会把元素换来换去,破坏了这种排序算法的稳定性,所以我们以相反的顺序迭代数组。
我们将在我们的countArray ,找到与当前元素arr[i] 的值相等的索引。然后,在countArray[arr[i]] - 1 的位置,我们将放置元素arr[i] 。这可以保证我们保持这个排序的稳定性。之后,我们将值countArray[i] 递减1,并继续这样做,直到i >= 0 。
for(int i = arr.length-1; i >= 0; i--){
outputArray[countArray[arr[i]] - 1] = arr[i];
countArray[arr[i]]--;
}
在算法结束时,我们可以直接将outputArr 中的值复制到我们的起始数组arr ,并将排序后的数组打印出来。
for(int i = 0; i < arr.length; i++){
arr[i] = outputArray[i];
System.out.print(arr[i] + " ");
}
当然,运行后,我们得到的排序数组的稳定性(相对顺序)得到了保证,相等的元素。
0 1 1 4 7 7 8 9
计算排序的复杂性
让我们讨论一下计数排序的时间和空间复杂性。
假设n 是arr 阵列中的元素数,k 是n 那些元素的允许值范围,从1...n 。由于我们只处理简单的for 循环,没有任何递归调用,我们可以用以下方式分析时间的复杂性。
- 计算输入范围内每个元素的出现次数需要
O(n)。 - 计算前缀的总和需要
O(k)时间。 - 而根据前两者计算
outputArray,则需要O(n)。
考虑到这些单独步骤的复杂性,计数排序的时间复杂度为O(n+k) ,使得计数排序的平均情况为线性,这比大多数基于比较的排序算法要好。然而,如果k 的范围是1...n² ,那么Counting Sorts的最坏情况很快就会恶化到O(n²) ,这真的很糟糕。
值得庆幸的是,这种情况并不经常发生,而且有一种方法可以确保它永远不会发生。这就是Radix Sort的由来--它通常在排序时使用Counting Sort作为其主要子程序。
通过对多个有界子数采用计数排序,时间复杂度绝不会恶化到O(n²) 。此外,Radix Sort可以使用任何稳定的、非比较性的算法来代替Counting Sort,但它是最常用的一种。
另一方面,空间复杂度问题要简单得多。由于我们的countArray ,大小为k ,比我们的起始数组n 的元素要大,所以那里的主导复杂性将是O(k) 。需要注意的是,给定数组中元素的范围越大,计数排序的空间复杂度就越大。
总结
在这篇文章中,我们描述了什么是计数排序,它是如何工作的以及如何在Java中实现它。