C++算法之计数排序

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的例子:
C++算法之计数排序_第1张图片
值得一提的是,如果元素的范围可以被很容易转换到[0..K],我们也可以使用计数排序。如果元素范围是[A..B],我们可以通过简单的平移关系将其对应到[0..B-A]上。或者所有数值均为绝对值不超过100的两位小数,那么我们可以通过将所有数字放大100倍将其转换为整数。

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

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

C++算法之计数排序_第2张图片
试想,对于原序列中的数字x,它排序后的位置可能出现在哪里呢?

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

sum数组的求法和意义
那么,我们怎样求出AB呢?假设我们对cnt数组求前缀和,如下图所示,cnt数组元素求前缀和后为sum数组:
C++算法之计数排序_第3张图片
这里,我们指出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数组的位置标出来:
C++算法之计数排序_第4张图片
然后我们从后往前扫描每个元素,把它填到当前的sum对应值指向的格子中,并把sum向前移动。如下图:
C++算法之计数排序_第5张图片
有了原序列和答案序列的位置对应,我们也可以据此将对应元素放入答案数组中。所以该版本的计数排序算法描述如下:

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

二、代码实现

代码如下(示例):

#include 
#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对应的内层循环次数相加,即可得到:
C++算法之计数排序_第6张图片
所以,整个算法的复杂度为O(n + K)

我们提到过,有一条结论

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

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

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

你可能感兴趣的:(C++算法,算法,c++,排序算法)