C++算法之计数排序

C++算法之计数排序



一、算法描述

给定长度为n的序列,假设已知序列元素的范围都是[0..K]中的整数,并且K的范围比较小(例如10^6,开长度为10^6左右的int类型数组所占用的内存空间只有不到4M)。解决该问题的计数排序算法描述如下:

  • 使用整数数组cnt统计[1..K]范围内所有数字在序列中出现的个数。
  • 使用变量i枚举1K,如果i出现了cnt[i]次,那么在答案序列的末尾添加cnt[i]i

下图是一个n=6, K=3的例子:
在这里插入图片描述
值得一提的是,如果元素的范围可以被很容易转换到[0..K],我们也可以使用计数排序。如果元素范围是[A..B],我们可以通过简单的平移关系将其对应到[0..B-A]上。或者所有数值均为绝对值不超过100的两位小数,那么我们可以通过将所有数字放大100倍将其转换为整数。

找出原序列中元素在答案中的位置

在有些场景中,比如我们根据(key, value)中的key关键字进行排序,如果只是使用上面的计数排序,我们无法将value放到相应的key在答案序列中的对应位置中。但是,如果我们可以将原序列和答案序列元素的位置对应求出来,那么这个问题就能得到解决。

在这里插入图片描述
试想,对于原序列中的数字x,它排序后的位置可能出现在哪里呢?

因为在排序后的序列中,假设x第一次出现的位置是i,最后一次出现的位置是j,那么i之前的元素一定比x小,j出现的位置之后的元素一定比x大。假设原序列中<x元素的个数是A≤x的元素个数是B,那么x可能出现的位置一定是[(A+1)..B]

sum数组的求法和意义
那么,我们怎样求出AB呢?假设我们对cnt数组求前缀和,如下图所示,cnt数组元素求前缀和后为sum数组:
在这里插入图片描述
这里,我们指出sum数组的意义:对于一个序列中可能出现的值xsum[x]的含义是“小于等于x的数字个数”,同时,也可以看作指向答案序列中最后一个x出现的位置的指针。

利用sum数组分配位置

所以对于值xA即为sum[x - 1]B即为sum[x]x出现的排名为(sum[x - 1] + 1)..sum[x]],等价于[(sum[x] - cnt[x] + 1)..sum[x]]。我们将sum数组的位置标出来:
在这里插入图片描述
然后我们从后往前扫描每个元素,把它填到当前的sum对应值指向的格子中,并把sum向前移动。如下图:
在这里插入图片描述
有了原序列和答案序列的位置对应,我们也可以据此将对应元素放入答案数组中。所以该版本的计数排序算法描述如下:

  • 统计原序列中每个值的出现次数,记为cnt数组。
  • 从小到大枚举值的范围,对cnt数组求前缀和,记为sum数组。
  • 从后往前枚举每个元素a[i],分配其在答案中的位置idx[i]为当前的sum[a[i]],也就是将其放在所有值等于a[i]中的最后一个。并且将sum[a[i]]减少1,保证下次再遍历到同样的值时,它分配的位置正好在idx[i]前面一个。

二、代码实现

代码如下(示例):

#include <bits/stdc++.h>
#define N 1000005
#define K 1000001	// 假设非负整数最大元素范围为1000000
using namespace std;
int a[N], n, b[N];
int cnt[K];
int main() {
    // 输入
    cin >> n;
    for (int i = 1; i <= n; ++i) {
        cin >> a[i];
        ++cnt[a[i]];	// 这里通过计数数组cnt来维护每一种值出现的次数
    }
    
    // 维护最终有序序列
    for (int i = 0, j = 0; i < K; ++i)      // 枚举每一种值i,指针j用来枚举填充答案数组中的位置
        for (int k = 1; k <= cnt[i]; ++k)   // 根据该值出现的次数
            b[++j] = i;                     // 添加对应个数的i到答案序列
	
    // 输出
    for (int i = 1; i <= n; ++i)
        cout << b[i] << ' ';
    cout << endl;
    
    return 0;
}

三、复杂度分析

空间复杂度

因为在上面的代码中一共开了3个数组,长度分别为O(N)(对于ab)和O(K)(对于cnt)。整个空间复杂度为O(N + K)

时间复杂度

容易发现,算法的输入输出部分所占时间复杂度为O(n)

在“维护有序序列”的部分,我们首先考虑最外层循环,因为它遍历了所有[0..K]的数字,所以它的复杂度是O(K)

其次,我们考虑内层循环的循环次数,其在外层循环为i时为cnt[i]。因为对于不同的输入,以及外层循环枚举到的不同的icnt[i]差别很大。但如果我们把所有i对应的内层循环次数相加,即可得到:
在这里插入图片描述
所以,整个算法的复杂度为O(n + K)

我们提到过,有一条结论

  • 所有基于比较的排序算法的时间复杂度都为Ω(nlogn)。(ΩO记号类似,但O表示的是“不会超过”,而Ω表示的是“不会少于”)。

我们看到当K = O(n)时,整个算法的时间复杂度为O(n)。之所以计数排序可以达到比O(nlogn)更好的时间复杂度,就是因为它并不是基于比较的排序

对于基于原序列和答案序列位置对应设计的计数排序,经过分析可以发现其复杂度和第一种一样。大家可以自己尝试分析一下。

  • 7
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
基数排序(Radix Sort)是一种非比较排序算法,它根据元素的大小,将元素分配到不同的桶中进行排序。计数排序(Counting Sort)是一种稳定的排序算法,它利用桶的思想,对每个元素出现的次数进行统计,再根据桶的顺序依次输出排序结果。 基数排序可以使用计数排序作为其内部排序算法,具体实现步骤如下: 1. 找到最大数,并确定其位数 2. 对所有数按照个位数进行排序,利用计数排序 3. 对所有数按照十位数进行排序,利用计数排序 4. 重复步骤 3,直到所有位数都排完序 下面是使用 C++ 实现基数排序的代码: ```cpp #include <iostream> #include <vector> using namespace std; void countingSort(vector<int>& arr, int exp) { vector<int> count(10, 0); vector<int> output(arr.size()); // 统计每个数位上出现的数字的个数 for (int i = 0; i < arr.size(); i++) { int digit = (arr[i] / exp) % 10; count[digit]++; } // 计算每个数字在输出数组中的位置 for (int i = 1; i < count.size(); i++) { count[i] += count[i - 1]; } // 将元素从输入数组复制到输出数组中,保证稳定性 for (int i = arr.size() - 1; i >= 0; i--) { int digit = (arr[i] / exp) % 10; output[count[digit] - 1] = arr[i]; count[digit]--; } // 将排序好的数组赋值给原数组 for (int i = 0; i < arr.size(); i++) { arr[i] = output[i]; } } void radixSort(vector<int>& arr) { int max_num = *max_element(arr.begin(), arr.end()); // 从个位开始,依次对每个数位进行排序 for (int exp = 1; max_num / exp > 0; exp *= 10) { countingSort(arr, exp); } } int main() { vector<int> arr = { 170, 45, 75, 90, 802, 24, 2, 66 }; radixSort(arr); for (auto num : arr) { cout << num << " "; } return 0; } ``` 输出结果为:2 24 45 66 75 90 170 802
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值