一、计数排序概述
计数排序是一种非比较排序算法,它与其他基于比较的排序算法有着明显的不同。这种算法主要适用于整数排序,特别是当数值范围不是特别大的时候,它能够展现出非常高的效率。
计数排序的时间复杂度为 Ο(n+k),其中 n 是数组长度,k 是数值范围。这意味着当数值范围相对较小时,计数排序的效率会非常高。例如,当处理一个包含 100 个整数的数组,且这些整数的范围在 0 到 99 之间时,计数排序可以快速地完成排序任务。
然而,计数排序也有其局限性。它需要额外的空间来存储计数数组。这个计数数组的大小取决于数值范围 k。如果数值范围很大,那么所需的额外空间也会相应地增大。例如,对于一个包含 1000 个整数的数组,且这些整数的范围在 0 到 1000000 之间,计数排序需要创建一个大小为 1000001 的计数数组,这会占用大量的内存空间。
计数排序的步骤主要包括确定数值范围、创建计数数组、统计每个数值出现的次数、累计计数、构建输出数组和复制排序结果等。首先,需要知道数组中的最大值和最小值,这有助于确定计数数组的大小。然后,创建一个大小等于数值范围的计数数组,并初始化为 0。接着,遍历待排序数组,对于数组中的每一个元素,在 count [] 中相应位置的计数加 1。这样 count [] 就记录了每个数值出现的次数。之后,遍历 count [],将前一个元素的值累加到当前元素上,得到的结果就是该数值在排序后数组中的正确位置。再创建一个新的数组 output [],并从后往前遍历待排序数组,根据 count [] 中记录的位置,将数值放入 output [] 中正确的索引位置。最后,将 output [] 中的数据复制回原数组。
总之,计数排序是一种在特定情况下非常高效的排序算法,但也需要考虑其额外空间的占用问题。
二、计数排序原理与步骤
(一)确定数值范围
在计数排序中,确定数值范围是非常关键的一步。通过遍历待排序数组,找出其中的最大值和最小值,可以确定数值的范围。例如,假设有一个待排序数组为 {4, 2, 2, 8, 3, 3, 1},通过遍历可以确定最大值为 8,最小值为 1,那么数值范围就是 [1, 8]。如果数据范围已知,就可以直接使用该范围,无需再进行遍历查找最大值和最小值的操作。确定数值范围有助于确定计数数组的大小,为后续的排序步骤提供基础。
(二)创建计数数组
创建计数数组的大小等于数值范围。以刚才确定的 [1, 8] 范围为例,创建一个大小为 8 的数组 count [],初始值为 0。这样,每个位置对应一个可能的数值,初始时都没有出现过,所以计数为 0。这个计数数组将用于记录每个数值出现的次数。
(三)统计数值出现次数
遍历待排序数组,对于数组中的每一个元素,在计数数组相应位置计数加 1。例如,当遇到第一个元素 4 时,count [4] 加 1;遇到第二个元素 2 时,count [2] 加 1,以此类推。继续以 {4, 2, 2, 8, 3, 3, 1} 这个数组为例,最后 count [] 变为 [0, 1, 2, 2, 1, 0, 0, 1]。通过这个步骤,计数数组记录了每个数值出现的次数。
(四)累计计数
遍历计数数组,将前一个元素的值累加到当前元素上。比如,count [1] 不变;count [2] 变为 1 + 2 = 3;count [3] 变为 3 + 2 = 5;以此类推。最后 count [] 变为 [0, 1, 3, 5, 6, 6, 6, 7]。这个步骤的目的是得到每个数值在排序后数组中的正确位置。
(五)构建输出数组
创建一个新的数组 output [] 用于存放排序后的结果。从后往前遍历待排序数组,根据计数数组记录的位置,将数值放入输出数组正确索引位置。以 {4, 2, 2, 8, 3, 3, 1} 这个数组为例,最后一个元素 1 应该放在 output [1] 的位置,即 output [count [1] - 1] = 1,然后 count [1] 减 1。重复此过程直到所有元素被放置完毕。这样可以保持稳定性,即相同的元素保持原有的相对顺序。
(六)复制排序结果
最后将 output [] 中的数据复制回原数组。这样就完成了整个计数排序的过程。
三、代码实现与示例
(一)函数声明与随机数组生成
以下是 C 语言中计数排序的代码实现过程,首先是函数声明和随机数组生成部分。
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
// 函数声明
// 创建并生成指定大小和范围的随机数组,返回数组指针
int* create_and_generate_random_array(int size, int range);
// 打印数组元素的函数,接受一个常量整数数组指针和数组大小
void print_array(const int *array, int size);
// 计数排序函数,接受待排序数组、数组大小和数据范围作为参数
void counting_sort(int *array, int size, int range);
int main()
{
int size = 10; // 数组大小,这里设定为10个元素
int range = 100; // 数据范围,数组中的元素将在0到这个值 - 1之间随机生成
// 创建并填充随机数组
int *array = create_and_generate_random_array(size, range);
if (array == NULL)
{
// 如果内存分配失败(create_and_generate_random_array函数返回NULL)
printf("Memory allocation failed\n");
return 1;
}
// 打印原始数组
printf("原始数组: ");
print_array(array, size);
// 调用计数排序函数对数组进行排序
counting_sort(array, size, range);
// 打印排序后的数组
printf("排序后的数组: ");
print_array(array, size);
// 释放动态分配的数组内存
free(array);
return 0;
}
// 创建并生成随机数组
int* create_and_generate_random_array(int size, int range)
{
// 使用malloc动态分配足够的内存来存储size个整数
int *array = (int *)malloc(sizeof(int) * size);
if (array == NULL)
{
// 如果内存分配失败,返回NULL
return NULL;
}
// 设置随机数种子,使每次运行程序生成的随机数序列不同
srand(time(NULL));
// 循环生成随机数填充数组
for (int i = 0; i < size; i++)
{
// 生成0到range - 1之间的随机数并赋值给数组元素
array[i] = rand() % range;
}
return array;
}
// 打印数组的函数
void print_array(const int *array, int size)
{
for (int i = 0; i < size; i++)
{
// 逐个打印数组元素,元素之间用空格隔开
printf("%d ", array[i]);
}
// 换行
printf("\n");
}
// 计数排序函数(这里只是函数框架,需要进一步完善才能实现排序功能)
void counting_sort(int *array, int size, int range)
{
// 计数排序的基本思路是统计每个数出现的次数,然后根据次数将数重新放回原数组
// 1. 创建计数数组并初始化为0
int *count = (int *)malloc(sizeof(int) * range);
for (int i = 0; i < range; i++)
{
count[i] = 0;
}
// 2. 统计每个数在原数组中出现的次数
for (int i = 0; i < size; i++)
{
count[array[i]]++;
}
// 3. 根据计数数组重新构建原数组(这里还未完整实现)
int index = 0;
for (int i = 0; i < range; i++)
{
while (count[i] > 0)
{
array[index] = i;
index++;
count[i]--;
}
}
// 释放计数数组的内存
free(count);
}
(二)打印数组
接下来是打印数组的函数,用于输出数组的内容。
// 打印数组
void print_array(const int *array, int size)
{
for (int i = 0; i < size; i++)
{
printf("%d ", array[i]);
}
printf("\n");
}
(三)计数排序函数
最后是计数排序函数,实现计数排序的核心逻辑。
// 计数排序
void counting_sort(int *array, int size, int range)
{
int *count = (int *)malloc((range + 1) * sizeof(int));
int *output = (int *)malloc(size * sizeof(int));
// 初始化计数数组
for (int i = 0; i <= range; ++i)
{
count[i] = 0;
}
// 统计每个数值出现的次数
for (int i = 0; i < size; ++i)
{
count[array[i]]++;
}
// 累积计数
for (int i = 1; i <= range; ++i)
{
count[i] += count[i - 1];
}
// 构建输出数组
for (int i = size - 1; i >= 0; --i)
{
output[count[array[i]] - 1] = array[i];
count[array[i]]--;
}
// 复制排序结果到原数组
for (int i = 0; i < size; ++i)
{
array[i] = output[i];
}
// 释放临时数组
free(count);
free(output);
}
我们可以通过以下方式调用这些函数,对随机生成的数组进行计数排序并输出结果。
int main()
{
int size = 10; // 数组大小
int range = 100; // 数据范围
// 创建并填充随机数组
int *array = create_and_generate_random_array(size, range);
if (array == NULL)
{
printf("Memory allocation failed\n");
return 1;
}
// 打印原始数组
printf("Original array:\n");
print_array(array, size);
// 进行计数排序
counting_sort(array, size, range);
// 打印排序后的数组
printf("Sorted array:\n");
print_array(array, size);
// 释放内存
free(array);
return 0;
}
例如,对于一个随机生成的数组 {89, 45, 76, 23, 67, 54, 32, 98, 12, 78},经过计数排序后,将按照从小到大的顺序输出。计数排序在处理整数排序时,特别是当数值范围相对较小的情况下,能够高效地完成排序任务。
四、计数排序特点总结
(一)稳定性
计数排序是一种稳定的算法。这意味着在排序过程中,相等元素的相对顺序不会改变。例如,对于数组 {3, 2, 2, 1},在计数排序后,两个 2 的相对顺序依然保持不变。这种稳定性在某些特定的应用场景中非常重要,比如需要保留元素原始顺序信息的情况。
(二)时间复杂度
计数排序的时间复杂度为 ,其中 是待排序数组的长度, 是数值范围。当数值范围相对较小时,计数排序的效率非常高。例如,当处理一个包含 1000 个整数的数组,且这些整数的范围在 0 到 99 之间时, 相对 来说很小,此时计数排序可以快速地完成排序任务。但如果数值范围很大,比如对于一个包含 1000 个整数的数组,且这些整数的范围在 0 到 1000000 之间,虽然时间复杂度依然是 ,但由于 的值很大,实际运行时间可能会较长。
(三)空间复杂度
计数排序的空间复杂度为 ,即取决于数值范围 。如果数值范围很大,那么所需的额外空间也会相应地增大。例如,对于一个包含 1000 个整数的数组,且这些整数的范围在 0 到 1000000 之间,计数排序需要创建一个大小为 1000001 的计数数组,这会占用大量的内存空间。
(四)适用场景
计数排序适用于范围较为集中且重复数据较多的场景。例如,在统计学生考试成绩的分布情况时,成绩通常在一个相对较小的范围内,且可能有很多重复的分数。此时,计数排序可以高效地完成排序和统计任务。另外,计数排序只适合对整数进行排序,如果数据类型不是整数,就不能使用计数排序。