最近完成了分治章节的学习,包括分治求解数组子段和,分治法求解最大值,堆排,快排。今天又遇见了计数排序,这里使用了前缀和和一些小技巧,我觉得会经常用到,所以写下这篇文章,记录一下今天在算法方面的学习记录
计数排序的原理和步骤
计数排序适合于元素是整数的数组,小数的话是无法去建立计数数组下标与数组元素的对应关系(不理解的话可以继续看),负数的话也是支持的
这里需要四次遍历,每一次遍历都有自己的意义,让我们继续分析
第一次遍历:获取最大值和最小值
我们假设需要排序的数组是arr = <6,0,2,0,1,3,4,6,1,3,2> 共11个数
遍历求最小值和最大值的方法应该不用说了吧,初始化的时候可以拿第一个数作为min或者max(在没有INT_MIN和INT_MAX的情况下)
获取最大值max=6,最小值为min=0,我们需要创建max-min+1个元素的数组arr2
这是为什么呢?实际上我们需要一个连续排列的从0到6的数组,6-0总共有7个数,所以要求连续的值的个数时是max-min+1
这个数组包括从min到max的所有整数,数组中不一定全都有,比如arr就没有5这个元素
有了这个数组后我们可以进行下一步
第二次遍历:获得计数数组
我们在这次遍历要统计每一个元素的出现次数,比如6就出现了2次,0出现了2次
保存在哪里呢?我们在第一次中有一个七个元素的数组arr2,我们将min元素的次数保存在下标为0的地方,max元素的次数保存在下标为max-min+1这里是7的地方
min的下标是0,min+1的下标是1.... 直到max的下标是max-min
由于这里的arr的min为0,所以和新数组的下标重合了,我们再举个例子,假如min是3,max是8,max-min+1就是6
对应于arr中min到max: 3,4,5,6,7,8
arr2: 0,1,2,3,4,5
3(min)+0(arr2下标) = arr中的min
3(min)+1(arr2下标) = arr中的min+1 (可能存在也可能不存在,这只是范围中可以出现的值)
这里就有对应关系 arr2的下标+min 对应 arr数组区间内可以存在的min到max元素值,这里和求连续值个数可以一起当作一个技巧
然后就要在这个数组保存个数,最后arr2=<2,2,2,2,1,0,2>
意思是arr2下标为0的元素,也就代表arr为0的元素的个数是2,同理其他的
有了计数数组的话,我们可以继续下一步
第三次遍历:获得前缀和
前缀和的概念是一个连续求和的数
pre[0] = arr2[0]
pre[1] = arr2[0] + arr2[1]
pre[2] = arr2[0] + arr2[1] + arr2[2]
pre[n] = arr2[0] + ... arr[n]
但是我们可以发现pre[0]实际上已经把arr2[0]计算出来了,pre[1]已经把arr2[0]+arr2[1]计算出来了
pre[1] = pre[0] + arr2[1]
pre[2] = pre[1] + arr2[2]
...
这样子就有新的递归式 pre[n] = pre[n-1] + arr2[n](n>=1); pre[0] = arr2[0]
我们根据arr2求得的pre为
我们求得pre[7] = <2,4,6,8,9,9,11>
这个有什么意义呢?
pre[0] = 2意思是 arr2下标为0的元素包括自己,假如在11个数排序的时候,在它前面的有几个数(包括自己)
arr2下标为0的元素代表着arr中的min,也就是说 arr中的0这个元素 ,在它前面有包括自己应该有两个值
0是arr中最小的值,且有两个,所以这个2代表的是0在排序后最后一个'0'元素的应该处的位置,也就说第二个'0'的位置
2是代表是下标为2吗?也不是。
数组是从0开始的,0,1,2从0开始为第一个值的话,第二个值的下标就为1,所以2-1=1才是数组排序后真正的坐标
0是第一个'0'的真正的坐标
总结一下就是pre[n] 实际上是在arr中,n+min这个元素,排序后在它前面的有几个数(包括自己)
怎么排序呢?
拿arr中的2举例子,2有两个分别在arr下标为2,10的位置
当我们遍历arr到下标为2的值'2'时,我们记得哪个对应关系吗, arr2中下标+min=arr中代表的元素
我们交换一下 arr2下标 = arr元素 - min
arr2下标 = 2 - 0 = 2 arr2的下标和pre的下标是一样的
pre2[2] = 6 也就是说,第二个'2',在排序后应该处的位置是 6-1 = 5
我们拿排序的数组好的举个例子
数组 0 0 1 1 2 2 3 3 4 6 6
下标 0 1 2 3 4 5 6 7 8 9 10
第二个怎么排序呢?
我们要把pre[2] = 6 要-1=5 ,因为第二个2已经排完了,除了第二个2之外,第一个2包括自己,在它之前还有5个值
所以在arr[10]遇到2时,下标就是pre[2]-1 = 5 - 1 = 4
第二个放到下标为4的位置
第四次遍历
这样就很明白了,我们现在要遍历arr,然后再从pre中找到这个arr中元素应该处在的位置,如果有相同元素的话,是最后一个元素处的位置。
为了保持序列排序稳定,从后往前遍历,最后一个2先遍历就应该放在最后一个2位置上
分析完结,给出C++代码
C++代码
#include <iostream>
#include <vector>
#include <algorithm>void countingSort(std::vector<int>& arr) {if (arr.empty()) return;// 找出数组中的最大值和最小值int max = *std::max_element(arr.begin(), arr.end());int min = *std::min_element(arr.begin(), arr.end());// 计算计数数组的长度int range = max - min + 1;// 初始化计数数组std::vector<int> count(range, 0);// 统计每个元素的出现次数for (int num : arr) {// 通过 num - min 计算出在计数数组中的索引count[num - min]++;}// 计算前缀和for (int i = 1; i < range; i++) {count[i] += count[i - 1];}// 初始化输出数组std::vector<int> output(arr.size());// 填充输出数组for (int i = arr.size() - 1; i >= 0; i--) {output[count[arr[i] - min] - 1] = arr[i];count[arr[i] - min]--;}// 将排序结果复制回原数组for (int i = 0; i < arr.size(); i++) {arr[i] = output[i];}
}int main() {std::vector<int> arr = {3, 5, 7};countingSort(arr);std::cout << "Sorted array: ";for (int num : arr) {std::cout << num << " ";}std::cout << std::endl;return 0;
}