浅谈计数排序

浅谈桶排序

上一篇文章中我们讲到了一种时间复杂度为O(n)的排序算法–桶排序,今天我们来分析一下另一种时间复杂度为O(n)的排序算法–计数排序

一、计数排序的原理

计数排序实际上是桶排序的一种延伸。在桶排序中,我们把要排序的n个数据均匀的分配到了m个桶中。而计数排序是当我们要排序的n个数据范围不大的时候,我们可以将n个数分配到n个桶中。这样每个桶中存储的都是相同的数据,省去了桶内的快速排序,从而节省了时间。

比如说我们每个人都经历过的高考,在查询成绩的时候我们可以看到自己的成绩以及所在省的排名。那么查分系统是怎么通过成绩快速的定位到我们所在的名次的呢?

我们尝试用计数排序算法来分析解决这个问题。假设我们高考满分为750分,加上0分这个数据,我们可以划分的桶的个数为751个,对应的分数为0-750分。根据所在省份所有考生的成绩,我们将每个考生的成绩对号入座,分别加入到对应分数的桶中。最后我们只需创建一个存储数据的结构,例如数组,按桶的编号由小到大分别将每个桶中的数据加入到数组中。那么,最后我们得到的数组就是一个按分数排好序的有序数组。

整个排序过程可以划分成两个部分,第一部分为入桶,将数据分别放入对应的桶中,这个过程我们只需要遍历一遍数据,所以时间复杂度为O(n),第二部分为整合,这个过程我们只需要按桶的序号,将桶中的数据依次放入同一个数组中即可,所以这个过程的时间复杂度也为O(n),将两部分的时间加起来,最后得到的时间复杂度还是为O(n)

看完了上边的分析,我们可能会产生疑问,既然算法思想与桶排序思想一样,就是划分桶的个数不一样,那么为什么我们称这种算法为计数排序呢?这个问题将在第二部分得到解答。

二、计数排序的实现

我们来看这样一个例子,假设有10个考生,他们的分数区间在0分到5分之间,我们将这10个数据存储到数组A[10] 中。于是根据计数排序的原理,我们用数组来表示桶。0分-5分,也就是6个分数,我们分配一个容量为6的数组 C[6] 作为桶,其中数组的一个下标编号代表着一个桶,而C[6]数组中。如下图所示:
在这里插入图片描述
C[0]位置存放的数字为2,代表着在A[10]数组中,分数为0分的考生个数为2。以此类推,C[1]位置存放的数据为3,代表着在A[10]数组中,分数为1分的考生为3个

接下来我们还需要一个数组用来存放最终的结果,因为有10个学生的成绩,所以我们设这个数组为R[10] 。从上边的C[6]数组中我们可以看出,分数为1分的考生有3个,小于1分的考生有2个,所以成绩为1分的考生在R[10]数组中,会存放在下标为2、3、4的位置上。如下图所示:
在这里插入图片描述
那么,我们该如何快速计算出每个分数的考生在有序数组中对应的存储位置呢?
思路是这样的:我们对C[6]数组的定义做出一些改造,C[k]中存储的数据为小于等于分数k的考生的个数,于是C[6]数组就变成了下面这样:
在这里插入图片描述
准备工作做完之后,接下来的内容就是重点了!!!

举个例子,我们从前到后遍历数组A[10],比如说我们遍历到下标为3的元素,A[3]中存储的分数为1,我们想要找到1在R[10]数组中的位置。这时需要我们把关注点放在C[6]数组上,由于我们需要寻找的分数为1,那么只需查找C[1]位置上的数据,可以看到C[1] = 5。也就是说分数小于等于1的考生个数为5。即1是数组R[10]中的第5个元素(也就是数组R中下标为6的位置的元素)。当我们把1放入到R[10]数组中,小于等于1的考生个数就要减1,也就是C[1]位置的数字要减1,减完之后 C[1] = 4。

以此类推,当我们扫描到第二个分数为1的考生的时候,就会把它放入R[10]中的第6个元素的位置,也就是下标为5的位置。当我们对A[10]中的所有元素都进行了以上的操作后,R[10]中存放的就是排好序的有序数组了。

总结: 计数排序只能用在数据范围不大的场景中,如果数据范围K,比要排序的数据n大很多,就不适合用计数排序了。而且,计数排序只能给非负整数排序,如果要排序的数据是其它类型的,要在不改变相对大小的情况下,转化为非负整数。

举个例子,如果考生成绩精确到小数点后一位,我们需要将所有的分数都先乘以10,转化成整数,然后在放到7510个桶内。再比如要排序的数据中有负数,数据的范围是[-2000,2000],那么我们需要在不改变相对大小的前提下,对每个数据都加2000,转化成非负数,范围就变成了[0,2000]。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
内部排序合集(插入、希尔、起泡、快速、选择、堆、归并和基数排序) 这是我在我们期末的时候写的一些内部排序的例子。因为我们的数据结构考试的范围就限定在内部排序上,所以我没有什么办法,只好对自己埋头苦干就行内部排序的编程了。有些内部排序的例子很是好理解,我们可以通过一些图示来很好地了解到这些排序的过程,但是这些程序的代码可能不是那么好写。而另外一些程序的思想既难以理解,也难于编码。着实地伤透了我的脑筋。下面就将我的程序的主框架代码展示给大家看看。注意,在这里我怎样也不好实现2-路插入排序,因为它这样的排序方法运用在顺序存储结构下不好,在判断数组越界的时候这样的复杂性使我最终放弃了这种方法。要不还不如使用其余的排序呢。 毕竟了解内部排序是一件好事,以后在看一些内部排序的时候也不至于摸不着头脑。最近我看到了一本有关介绍用C++来编辑游戏的书,它就以STL为例,讲解了STL内部实现的机理。 // 头文件 #include using namespace std; #include "InsertionSort.h" #include "Shell'sSort.h" #include "QuickSort.h" #include "SelectionSort.h" #include "MergingSort.h" #include "RadixSort.h" // 定义的宏 #define LENGTH 10 // 主函数 int main( int argc, char** argv ) { // 定义一个顺序表 SqList a; int objArray[LENGTH] = { 278, 109, 63, 930, 589, 184, 505, 269, 8, 83 }; a.length = LENGTH; int i; for ( i = 1; i < a.length + 1; i++ )// 需要将0号位设为监视哨 { a.r[i].key = objArray[i-1]; a.r[i].otherinfo = '\0'; } // 直接插入排序 //InsertSort( a ); // 折半插入排序 //BInsertSort( a ); // 2-路插入排序 //BinRouteInsertSort( a, 2 );// 这个算法有问题 // 希尔排序 /* { int dlta2[3] = { 3, 2, 1 }; ShellSort( a, dlta2, 3 ); } */ // 起泡排序 //BubbleSort( a ); // 快速排序 //QuickSort( a, 1, LENGTH ); // 选择排序 //SelectSort( a ); // 堆排序 //HeapSort( a ); // 归并排序 //MergeSort( a ); // 基数排序 { SLList b; int i; b.keynum = 3, b.recnum = LENGTH;// 对3位整数进行基数排序 for ( i = 1; i <= b.recnum; i++ ) { b.r[i].keys[0] = objArray[i-1] % 10;// 个位 b.r[i].keys[1] = objArray[i-1] % 100 / 10;// 十位 b.r[i].keys[2] = objArray[i-1] / 100;// 百位 } RadixSort( b ); } // 显示排序后的数组 for ( i = 1; i < a.length + 1; i++ )// 需要将0号位设为监视哨 cout<<a.r[i].key<<' '; cout<<'\n'; return 0; } 要想得到这些排序的具体实现,那么就下载我的代码吧。最近我的积分由于下载一些Boost的资料而变为0了。所以需要的朋友一定要支持一下哦。(使用VS2005开发,算法都是C风格的,可以直接使用。) 我最新的动态:最近在研究DirectInput,希望能够在几天后写一篇技术文章,分享一些我的经验。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值