一、算法描述
一个会计师负责对一个小饭店的账本进行审核。每天晚上饭店打洋时,饭店主人记录白 天的总销售额,然后打印出有总额和日期的收据。这些收据存放在一个大盒子里面.每 年年终,会计师审核盒子中的这些收据,检查是否有的已经丢失。你能想象,盒子中的 收据都是无序状态的。
会计师可以按照日期降序地排列所有的收据,然后审核。另一种方法是,他找到一个当 年的空白日历,从盒子中一条接一条取出收据来,然后在日历上相应日期用X标记.一 旦盒子为空之后,会计师仅仅需要检查一下日历上哪些日子没有标记。注意第二种方法中,从来没有两个收据会相互进行比较。如果饭店已经营业了60年,并且会计师有60年的日历,如果盒子中只有5张收据,那么这个方法将会非常低效,但是有20000张收据的话,这将是一个有效的方法,即收据的数量与总日子的比例决定了方法的效率。
在前面的教程中,我们证明了基于比较的排序算法不可能好于O( n l o g n nlogn nlogn)的时间排序元素。令人惊异的是,如果能知道这些元素的更多信息,那么就会有其他的排序方法。例如,假设你被指定排序几个元素,并且告知你每一个元素的值范围都在[0,k)之间,k比n小得 多。你就能够利用这个信息,使用一个线性的排序算法,叫做计数排序。
计数排序不需要一个比较函数,是对范围固定在[0,k)的整数排序的最佳选择。即使 k 个元素的全序能够被决定以及每一个元素的值都是唯一的,这个算法也可用。例如,如果排序一系列形如 1/p(p 为整数)的小数,p 的最大值为 k,那么每个小数 1/p 能够被分配一个唯一的值 k 一 p。
因为元素的k值形式的全序关系,所以计数排序能成功进行排序。
以下是计数排序的详细过程:
一、基本思想
计数排序的基本思想是对待排序的元素进行计数,并建立一个长度等于最大元素值加上1的辅助数组(计数数组),用来存储每个元素出现的次数。然后根据计数数组的信息,依次将元素放回原始数组中的正确位置,以实现排序。
二、具体步骤
1.确定待排序列中的最大元素和最小元素:
扫描整个待排序列,找到其中的最大值和最小值。
2.统计每个元素出现的次数:
创建一个计数数组,其长度等于最大值与最小值之差加1。
遍历待排序列,对每个元素出现的次数进行统计,并将结果存储在计数数组的相应位置。
3.计算每个元素在有序排列中的位置:
对计数数组进行前缀和操作,即对每个位置的值进行累加,使其变为该位置及之前所有元素出现的总次数。这样,计数数组中的每个值就表示了对应元素在有序排列中的最后一个位置的索引。
4.根据计数数组的信息,将元素放回原始数组中的正确位置:
创建一个临时数组,用于存储排序后的结果。
从待排序列的最后一个元素开始,根据计数数组的信息,将每个元素放入临时数组的正确位置,并更新计数数组中对应元素的值(通常通过递减来实现)。
重复此过程,直到所有元素都被放入临时数组中。这个步骤是排序的核心步骤,下面是对这个步骤的详解:
详细步骤
1.从待排序列的最后一个元素开始:
我们从 ar[n-1] 开始遍历待排序列,直到 ar[0]。这样做是为了在 countArray 中从后往前减少计数,从而避免覆盖之前已经放置在 tempArray 中的元素。
2.根据 countArray 的信息:
对于当前遍历到的元素 ar[i],我们查看 countArray[ar[i]] 的值,它表示元素 ar[i] 在排序后的序列中应该出现的次数。
3.将元素放入 tempArray 的正确位置:
由于我们已经从后往前遍历了 ar,并且每次都将元素放置在 tempArray 的当前索引位置(初始为 n-1,每次递减),因此我们可以直接将 ar[i] 放置在 tempArray 的当前索引位置,并递减该索引。
但是,由于我们实际上是通过 countArray 来指导放置的,我们并不直接操作索引,而是通过 countArray[ar[i]] 的值来决定放置多少个 ar[i]。
4.更新 countArray 的值:
在将元素放置到 tempArray 后,我们需要更新 countArray[ar[i]] 的值,以反映该元素在 tempArray 中已经被放置了多少次。
由于我们是逐个放置元素的,因此每次放置后,我们将 countArray[ar[i]] 的值减1。
这确保了当我们再次遇到相同的元素时,它会被放置在 tempArray 中的下一个正确位置。
实际操作
在实际操作中,这个过程通常是通过一个内部循环来实现的,该循环根据 countArray[ar[i]] 的值来决定在当前元素 ar[i] 上循环多少次,并在每次循环中将 ar[i] 放置到 tempArray 的当前索引位置,并递减 countArray[ar[i]] 和 tempArray 的索引。
但是,由于计数排序的特性,我们实际上并不需要一个显式的索引来遍历 tempArray,因为 countArray 已经为我们提供了每个元素应该出现的次数和位置信息。因此,我们可以直接从 countArray 的值中递减,并将元素放置到 tempArray 的“逻辑上”的正确位置(即使我们没有显式地跟踪这个位置)。
5.将排序好的结果复制回原始数组(如果需要):
如果不需要保留原始数组的内容,可以直接将排序后的结果存储在原始数组中。
否则,需要将临时数组中的排序结果复制回原始数组。
三、示例
假设待排序列为 {4, 2, 2, 8, 3, 3, 1},则计数排序的过程如下:
1.找到最大值8和最小值1。
2.创建计数数组 countArray,长度为 8 - 1 + 1 = 8,并初始化为0。
3.统计每个元素出现的次数,得到 countArray = [1, 2, 2, 2, 0, 0, 0, 1]。
4.对 countArray 进行前缀和操作,得到 countArray = [1, 3, 5, 7, 7, 7, 7, 8]。
5.创建临时数组 tempArray,长度为待排序列的长度。
6.从待排序列的最后一个元素开始,根据 countArray 的信息,将每个元素放入 tempArray 的正确位置,并更新 countArray 的值。下面是个示例:
假设 ar = [4, 2, 2, 8, 3, 3, 1],并且我们已经计算出了 countArray = [1, 2, 2, 2, 0, 0, 0, 1](注意,这里为了简化,我假设了元素范围是从0到7加上一个额外的8,所以 countArray 的长度是9)。
在放置元素时,我们会从 ar 的最后一个元素开始,即 1,并查看 countArray[1] 的值(它是2),然后将两个 1 放置到 tempArray 的末尾(逻辑上,因为我们实际上是从后往前填充的),并递减 countArray[1] 的值。接着,我们继续对 ar 中的下一个元素进行相同的操作,直到处理完所有元素。
7.得到排序后的结果 tempArray = [1, 2, 2, 3, 3, 4, 8]。
8.将排序好的结果复制回原始数组(如果需要)。
二、复杂度分析
时间复杂度:计数排序的时间复杂度为 O(n+k),其中 n 是待排序数组的长度,k 是待排序数组中元素的范围。
空间复杂度:计数排序的空间复杂度也为 O(n+k),因为需要创建一个长度为 k 的计数数组和一个长度为 n 的临时数组(或直接在原始数组上进行操作以避免额外的空间开销)。
三、适用情况
稳定性:计数排序是稳定的排序算法,因为两个相等的元素在排序后的序列中的相对位置和它们在原始序列中的相对位置相同。
适用场景:计数排序特别适用于待排序元素为整数且范围较小的情况。如果待排序的元素不满足这个要求(例如元素不是整数或范围很大),则需要考虑其他排序算法或进行额外的预处理步骤(如映射转换)。
四、算法实现
下面是计数排序算法的实现:
#include<stdio.h>
#include<stdlib.h>//排序ar中的n个元素,范围是[0,k)
int countingSort(int* ar, int n, int k)
{int i;int idx = 0;int* B =(int*) calloc(k,sizeof(int));//实际完成对位置的排序for (i = 0;i < n;i++){B[ar[i]]++;}//填充排序好的位置for (i = 0;i < k;i++){while (B[i]-- > 0){ar[idx++] = i;}}free(B);return 0;
}int main() {int ar[] = { 4, 2, 2, 8, 3, 3, 1 };int n = sizeof(ar) / sizeof(ar[0]);int k = 9; // 假设元素范围在[0, 9)之间 printf("Original array:\n");for (int i = 0; i < n; i++) {printf("%d ", ar[i]);}printf("\n");// 调用countingSort函数 if (countingSort(ar, n, k) != 0) {fprintf(stderr, "Sorting failed\n");return 1;}printf("Sorted array:\n");for (int i = 0; i < n; i++) {printf("%d ", ar[i]);}printf("\n");return 0;
}
五、算法优化
计数排序对整个数组进行了两次遍历。第一次处理输入数列中的所有 n 个元素。在第二次遍历时,内部的 while 循环将会执行 B[i]次,对于每一个 0<=i<k;因此表达式 ar[idx++]恰好执行 n 次。这个是实现中的关键表达式执行了 2*n 次,结果总的情能是 O(n)的。
因为待排序的数组中的元素必须是从有限的 k 个元素中得到,计数排序的使用受到了限制。我们现在讨论桶排序,这个排序方法克服了这个限制,每个元素都能够映射到一个桶中。
六、引用及参考文献
1.《算法设计手册》