排序之计数排序

概念:

计数排序是一种线性时间复杂度的排序算法,适用于处理整数且整数的范围不大的情况。它是基于这样的思想:对于每个元素,我们可以计算出它在排序后数组中的位置,然后将元素放到该位置。

算法步骤:

计数排序的基本步骤如下:

  1. 找出数组中最大的元素。
  2. 计算数组中每个元素出现的次数,使用一个额外的数组来存储这些计数。
  3. 将每个元素放到它在排序后数组中的位置上。

以下是计数排序的简单理解:

将每个元素放到它在排序后数组中的位置上,我们可以使用遍历:

给定一个长度为 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]--;
        }
    }
}

但是假设输入数据是商品对象,我们想按照商品价格(类的成员变量)对商品进行排序,而上述算法只能给出价格的排序结果。所以说上述代码并不是非常的完整

  1. 确定最大值:首先找出待排序数组中的最大元素,以确定计数数组的大小。
  2. 创建计数数组:初始化一个计数数组,大小为最大元素加一,并将所有元素初始化为0。
  3. 计数:遍历待排序数组,对于数组中的每个元素,将其在计数数组中对应的位置加一。
  4. 计算累积计数:将计数数组中的每个元素加上前一个元素的值,这样可以得到每个元素在最终排序数组中的起始位置。
  5. 构建排序数组:从后向前遍历待排序数组,将每个元素放置到计数数组中相应位置减一的位置,并将计数数组中该位置的值减一。
  6. 复制到原数组:将构建的排序数组复制回原数组。

这种思路保持数据的稳定性,与完整性(相对顺序不变)

  1. 包含必要的头文件

    #include <iostream>
    #include <vector>
    #include <algorithm> // 用于std::max_element

    这些头文件提供了输入输出流、动态数组(std::vector)和最大元素查找功能。

  2. 计数排序函数定义

    void countingSort(std::vector<int>& arr, int maxElement);

    定义了一个函数countingSort,它接受一个整数数组arr和一个整数maxElement作为参数。

  3. 创建计数数组

    std::vector<int> count(maxElement + 1, 0);

    创建一个计数数组count,大小为maxElement + 1,所有元素初始化为0。这个数组将用来记录每个数字出现的次数。

  4. 统计每个元素出现的次数

    for (int i = 0; i < n; i++) {
        count[arr[i]]++;
    }

    遍历数组arr,对每个元素,将其在count数组中相应位置的计数加1。

  5. 计算累积计数

    for (int i = 1; i <= maxElement; i++) {
        count[i] += count[i - 1];
    }

    从1开始遍历count数组,将每个位置的值加上前一个位置的值,得到累积计数。这表示每个数字应该放在排序后数组的哪个位置。

  6. 构建排序数组

    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,为下一个相同数字的放置做准备。

  7. 复制排序后的数组回原数组

    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(nlog⁡n) 的排序算法还要慢。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值