概念:
计数排序是一种线性时间复杂度的排序算法,适用于处理整数且整数的范围不大的情况。它是基于这样的思想:对于每个元素,我们可以计算出它在排序后数组中的位置,然后将元素放到该位置。
算法步骤:
计数排序的基本步骤如下:
- 找出数组中最大的元素。
- 计算数组中每个元素出现的次数,使用一个额外的数组来存储这些计数。
- 将每个元素放到它在排序后数组中的位置上。
以下是计数排序的简单理解:
将每个元素放到它在排序后数组中的位置上,我们可以使用遍历:
给定一个长度为 n 的数组 arr ,其中的元素都是“非负整数”
由于 count
的各个索引天然有序,因此相当于所有数字已经排序好了。接下来,我们遍历 count
,根据各数字出现次数从小到大的顺序填入 arr 即可:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 计数排序函数
void countingSort(vector<int>& arr, int maxVal) {
int n = arr.size();
vector<int> count(maxVal + 1, 0); // 创建计数数组,大小为maxVal+1
// 计算每个元素出现的次数
for (int i = 0; i < n; i++) {
count[arr[i]]++;
}
// 重新构建arr数组
int i = 0;
for (int j = 0; j < maxVal + 1; j++) {
while (count[j] > 0) {
arr[i++] = j;
count[j]--;
}
}
}
但是假设输入数据是商品对象,我们想按照商品价格(类的成员变量)对商品进行排序,而上述算法只能给出价格的排序结果。所以说上述代码并不是非常的完整
- 确定最大值:首先找出待排序数组中的最大元素,以确定计数数组的大小。
- 创建计数数组:初始化一个计数数组,大小为最大元素加一,并将所有元素初始化为0。
- 计数:遍历待排序数组,对于数组中的每个元素,将其在计数数组中对应的位置加一。
- 计算累积计数:将计数数组中的每个元素加上前一个元素的值,这样可以得到每个元素在最终排序数组中的起始位置。
- 构建排序数组:从后向前遍历待排序数组,将每个元素放置到计数数组中相应位置减一的位置,并将计数数组中该位置的值减一。
- 复制到原数组:将构建的排序数组复制回原数组。
这种思路保持数据的稳定性,与完整性(相对顺序不变)
-
包含必要的头文件:
#include <iostream> #include <vector> #include <algorithm> // 用于std::max_element
这些头文件提供了输入输出流、动态数组(
std::vector
)和最大元素查找功能。 -
计数排序函数定义:
void countingSort(std::vector<int>& arr, int maxElement);
定义了一个函数
countingSort
,它接受一个整数数组arr
和一个整数maxElement
作为参数。 -
创建计数数组:
std::vector<int> count(maxElement + 1, 0);
创建一个计数数组
count
,大小为maxElement + 1
,所有元素初始化为0。这个数组将用来记录每个数字出现的次数。 -
统计每个元素出现的次数:
for (int i = 0; i < n; i++) { count[arr[i]]++; }
遍历数组
arr
,对每个元素,将其在count
数组中相应位置的计数加1。 -
计算累积计数:
for (int i = 1; i <= maxElement; i++) { count[i] += count[i - 1]; }
从1开始遍历
count
数组,将每个位置的值加上前一个位置的值,得到累积计数。这表示每个数字应该放在排序后数组的哪个位置。 -
构建排序数组:
for (int i = n - 1; i >= 0; i--) { sorted[count[arr[i]] - 1] = arr[i]; count[arr[i]]--; // 更新计数数组 }
创建一个新数组
sorted
,从arr
的末尾开始,将每个元素放到sorted
数组的count[arr[i]] - 1
位置。这样做是因为count[arr[i]]
表示数字arr[i]
在sorted
数组中前面有多少个相同的数字。然后,更新count
数组,将该数字的计数减1,为下一个相同数字的放置做准备。 -
复制排序后的数组回原数组:
for (int i = 0; i < n; i++) { arr[i] = sorted[i]; }
将
sorted
数组中的元素复制回原数组arr
,完成排序。
如何保持相对位置不变?
- 在步骤6中,我们从
arr
的末尾开始处理元素,这样当我们放置相同数字时,先处理的元素会先被放置在sorted
数组中,从而保持了它们的相对顺序。 - 当
count[arr[i]]--;
执行时,我们实际上是在为下一个相同数字的元素腾出位置,确保它们紧挨着前一个相同数字的元素。
通过这种方式,计数排序不仅能够快速地对数字进行排序,而且还能够保持相同数字元素之间的相对位置不变。
代码实现:
#include <iostream>
#include <vector>
#include <algorithm> // 用于std::max_element
// 计数排序函数
void countingSort(std::vector<int>& arr, int maxElement) {
int n = arr.size();
std::vector<int> count(maxElement + 1, 0); // 步骤2:创建计数数组
std::vector<int> sorted(n, 0); // 步骤5:创建排序后的数组
// 步骤3:统计每个元素出现的次数
for (int i = 0; i < n; i++)
{
count[arr[i]]++;
}
// 步骤4:计算累积计数
for (int i = 1; i <= maxElement; i++)
{
count[i] += count[i - 1];
}
// 步骤5:构建排序数组
for (int i = n - 1; i >= 0; i--)
{
sorted[count[arr[i]] - 1] = arr[i]; // 将元素放置到正确的位置
count[arr[i]]--; // 更新计数数组
}
// 步骤6:复制排序后的数组回原数组
for (int i = 0; i < n; i++)
{
arr[i] = sorted[i];
}
}
int main() {
std::vector<int> arr = { 4, 2, 2, 8, 3, 3, 1 };
int maxElement = *std::max_element(arr.begin(), arr.end()); // 步骤1:找出最大元素
countingSort(arr, maxElement); // 调用计数排序函数
// 输出排序后的数组
for (int num : arr) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
难点解释:
计数排序是如何保持相对位置不变的????
假设我们有以下数组作为输入:
std::vector<int> arr = {4, 2, 2, 8, 3, 3, 1};
步骤 1: 找出最大元素
使用 std::max_element
找出数组中的最大值,这里是 8。
步骤 2: 创建计数数组
创建一个长度为最大元素加1的计数数组 count
,并初始化所有元素为0。对于我们的例子,计数数组 count
初始化为:
count = [0, 0, 0, 0, 0, 0, 0, 0, 0]
这里的索引代表 arr
中的元素值,数值代表该元素出现的次数
步骤 3: 统计每个元素出现的次数
遍历 arr
,对于每个元素,增加 count
数组中对应索引的值。例如,数组中的元素 '2' 会在索引2的位置增加两次:
count = [0, 1, 2, 2, 1, 0, 0, 0, 1]
步骤 4: 计算累积计数
将 count
数组中的每个元素设置为其前面所有元素的和。这告诉我们每个元素应该在最终排序数组 sorted
中的起始位置:
count = [0, 1(1+0), 3(2+1), 5(2+3), 6(1+5), 6(0+6), 6(0+6), 6(0+6), 7(1+6)]
步骤 5: 构建排序数组
创建一个与 arr
同样大小的数组 sorted
,初始化为0。然后,从 arr
的末尾开始,对于每个元素:
- 找到它在
sorted
中的位置,这是由count
数组的相应索引减1得到的。 - 将元素放在
sorted
的这个位置上。 - 减少
count
数组相应索引的值,以便下一个相同元素知道它应该放在哪里。
例如,对于元素 '1':
- 在
arr
中找到 '1',查看count[1]
,它是1,表示 '1' 在sorted
中的起始位置是1(索引2,因为索引从0开始)。 - 将 '1' 放在
sorted[0]
。 - 将
count[1]
减1,现在count[1]
是0。
count = [0, 1, 3, 5, 6, 6, 6, 6, 7] // 更新前的count数组
sorted = [1, 0, 0, 0, 0, 0, 0] // 1被放置在sorted的[count[arr[n-1]]-1],即0位置
count = [0, 0, 3, 5, 6, 6, 6, 6, 7] // 更新后的count数组
以下是继续的流程:
count = [0, 0, 3, 5, 6, 6, 6, 6, 7] // 更新前的count数组
sorted = [1, 0, 0, 0, 3, 0, 0] // 3被放置在sorted的[count[arr[5]]-1],即4位置
count = [0, 0, 3, 4, 6, 6, 6, 6, 7] // 更新后的count数组
count = [0, 0, 3, 4, 6, 6, 6, 6, 7] // 更新前的count数组
sorted = [1, 0, 0, 3, 3, 0, 0] // 3被放置在sorted的[count[arr[4]]-1],即3位置
count = [0, 0, 3, 3, 6, 6, 6, 6, 7] // 更新后的count数组
count = [0, 0, 3, 3, 6, 6, 6, 6, 7] // 更新前的count数组
sorted = [1, 0, 0, 3, 3, 0, 8] // 8被放置在sorted的[count[arr[3]]-1],即6位置
count = [0, 0, 3, 3, 6, 6, 6, 6, 6] // 更新后的count数组
count = [0, 0, 3, 3, 6, 6, 6, 6, 6] // 更新前的count数组
sorted = [1, 0, 2, 3, 3, 0, 8] // 2被放置在sorted的[count[arr[2]]-1],即2位置
count = [0, 0, 2, 3, 6, 6, 6, 6, 6] // 更新后的count数组
count = [0, 0, 2, 3, 6, 6, 6, 6, 6] // 更新前的count数组
sorted = [1, 2, 2, 3, 3, 0, 8] // 2被放置在sorted的[count[arr[1]]-1],即1位置
count = [0, 0, 1, 3, 6, 6, 6, 6, 6] // 更新后的count数组
count = [0, 0, 1, 3, 6, 6, 6, 6, 6] // 更新前的count数组
sorted = [1, 2, 2, 3, 3, 4, 8] // 4被放置在sorted的[count[arr[0]]-1],即5位置
count = [0, 0, 1, 3, 5, 6, 6, 6, 6] // 更新后的count数组
经过这些流程,我们就可以得到排序后的结果了,这也是应用到了前缀和的概念与其知识
特性:
- 稳定性:计数排序是稳定的,因为它不会改变相同元素的相对顺序。
- 时间复杂度:计数排序的时间复杂度是O(n+k),其中n是待排序数组的大小,k是最大元素的值。如果k远小于n,计数排序非常高效。
- 空间复杂度:计数排序需要额外的空间来存储计数数组,空间复杂度也是O(k)。
局限性:
计数排序只适用于非负整数。若想将其用于其他类型的数据,需要确保这些数据可以转换为非负整数,并且在转换过程中不能改变各个元素之间的相对大小关系。例如,对于包含负数的整数数组,可以先给所有数字加上一个常数,将全部数字转化为正数,排序完成后再转换回去。这其实就是一种相对映射的结果,为我们可以应用映射来解决相关的问题。
计数排序适用于数据量大但数据范围较小的情况。比如,在上述示例中 m 不能太大,否则会占用过多空间。而当 n≪m 时,计数排序使用 O(m) 时间,可能比 O(nlogn) 的排序算法还要慢。