传统的比较型排序算法(如快速排序、归并排序等)虽然应用广泛,但在面对特定类型的数据时,其性能往往受到一定限制。这时,非比较型排序算法,如计数排序,便展现出了其独特的优势。 本文旨在深入剖析计数排序的奥秘与魅力,从基本原理到实现步骤,从优缺点分析到实际应用场景,全面展现这一排序算法的独特之处。通过本文的阅读,读者将能够深刻理解计数排序的工作原理,掌握其实现方法,并学会在合适的场景下灵活运用这一算法,以提升数据处理的效率和质量。
计数排序(Counting Sort)是一种非比较型排序算法,它的基本原理是通过统计每个元素的出现次数,然后利用这些信息来重建排序后的数组。 其核心思想在于:通过统计每个元素在数组中出现的次数,来确定该元素在排序后数组中的位置。这种方法在处理具有明显范围限制且分布相对均匀的整数数据时,尤为高效。 作为一种线性时间复杂度的排序,它要求输入的数据必须是有确定范围的整数。
请看下图动图演示
首先,代码通过遍历待排序数组 a
,找出其中的最大值 max
和最小值 min
。这两个值用于确定计数数组 count
的大小,因为计数数组需要覆盖待排序数组中所有可能出现的值(在最小值和最大值之间)。
int min = INT_MAX;
int max = INT_MIN;
for (int i = 0; i < n; i++)//求得最大值和最小值
{
if (a[i] > max)
max = a[i];
if (a[i] < min)
min = a[i];
}
根据最大值和最小值计算出的范围(max - min + 1
),代码使用 calloc
分配了一个足够大的整数数组 count
,并将所有元素初始化为 0。这个数组用于统计待排序数组中每个值出现的次数。使用 calloc
而不是 malloc
加初始化是为了确保所有元素都初始化为 0,因为计数排序需要这些初始值来正确统计。
int size = max - min + 1;
int* count = (int*)calloc(size, sizeof(int));//根据数据范围申请空间
if (count == NULL)
{
perror("calloc fail\n");
return;
}
接下来,代码再次遍历待排序数组 a
,这次是为了统计每个元素出现的次数。对于数组 a
中的每个元素 a[i]
,代码通过 count[a[i] - min]++
将 a[i]
的出现次数记录在 count
数组的相应位置上。这里减去 min
是为了将 a
中的值映射到 count
数组的有效索引范围内。
for (int i = 0; i < n; i++)//遍历原数组,将数据出现的次数写入count数组对应的位置
{
count[a[i] - min]++;//核心代码1
}
代码遍历 count
数组,并根据每个值出现的次数,将对应的值依次放回原数组 a
中。这里使用了一个双层循环,外层循环遍历 count
数组的每个索引(即待排序数组中的每个可能值),内层循环(通过 while
循环实现)则根据 count[j]
的值(即该值出现的次数)将 j + min
(即原始值)放回原数组 a
中。每次放回一个值后,count[j]
递减,直到该值的所有出现都被放回原数组。
int i = 0;
for (int j = 0; j < size; j++)//遍历count数组,根据数据出现次数,将数据写入原数组对应的位置
{
while (count[j]--)//每写入一次,次数--
{
a[i++] = j + min;//核心代码2
}
}
最后,代码释放了 count
数组占用的内存,并将其指针设置为 NULL
,以避免野指针问题。
free(count);
count = NULL;
void CountSort1(int* a, int n)//计数排序升序
{
int min = INT_MAX;
int max = INT_MIN;
for (int i = 0; i < n; i++)//求得最大值和最小值
{
if (a[i] > max)
max = a[i];
if (a[i] < min)
min = a[i];
}
int size = max - min + 1;
int* count = (int*)calloc(size, sizeof(int));//根据数据范围申请空间
if (count == NULL)
{
perror("calloc fail\n");
return;
}
for (int i = 0; i < n; i++)//遍历原数组,将数据出现的次数写入count数组对应的位置
{
count[a[i] - min]++;//核心代码1
}
int i = 0;
for (int j = 0; j < size; j++)//遍历count数组,根据数据出现次数,将数据写入原数组对应的位置
{
while (count[j]--)//每写入一次,次数--
{
a[i++] = j + min;//核心代码2
}
}
free(count);
count = NULL;
}
void CountSort2(int* a, int n)//计数排序降序
{
int min = INT_MAX;
int max = INT_MIN;
for (int i = 0; i < n; i++)//求得最大值和最小值
{
if (a[i] > max)
max = a[i];
if (a[i] < min)
min = a[i];
}
int size = max - min + 1;
int* count = (int*)calloc(size, sizeof(int));//根据数据范围申请空间
if (count == NULL)
{
perror("calloc fail\n");
return;
}
for (int i = 0; i < n; i++)//遍历原数组,将数据出现的次数写入count数组对应的位置
{
count[a[i] - min]++;//核心代码1
}
int i = 0;
for (int j = size-1; j >=0; j--)//遍历count数组,根据数据出现次数,将数据写入原数组对应的位置
{
while (count[j]--)//每写入一次,次数--
{
a[i++] = j + min;//核心代码2
}
}
free(count);
count = NULL;
}
🍃时间复杂度:O(n + k),近似于O(n) 计数排序的时间复杂度主要由以下几个部分组成:
综上所述,计数排序的总时间复杂度是O(n + k)。然而,在实际应用中,由于k通常远小于n(例如,当排序的是一定范围内的整数时),计数排序的性能可以近似地看作是线性的,即O(n)。
🍃空间复杂度:O(k) 计数排序的空间复杂度主要取决于计数数组的大小,即O(k),其中k是数据范围的大小。如果k与n相比很大,那么计数排序可能会消耗大量的内存。因此,计数排序只适用于数据范围不是很大的情况。
🍃稳定性:稳定 计数排序能够保持相等元素的相对顺序不变,即它是稳定的排序算法。 计数排序通过统计每个元素的出现次数,并根据这些统计信息来重建排序后的数组。在这个过程中,如果两个元素的值相等,它们会被放入计数数组的同一个位置(或者更准确地说,是相邻的位置,因为计数数组会记录每个值出现的次数),并且在重建排序后的数组时,这些相等的元素会按照它们在原数组中的顺序被依次放回。
优点
缺点
计数排序是一种高效的排序算法,特别适用于一定范围内的整数排序。它的稳定性和高效性使得它在处理特定类型的数据时非常有用。然而,计数排序的空间复杂度较高,且对数据范围有一定的限制,这限制了它的应用范围。在选择排序算法时,需要根据具体的应用场景和数据特性来决定是否使用计数排序。如果数据范围明确且分布相对均匀,且内存空间足够,那么计数排序是一个很好的选择。