《算法导论》学习总结——第二部分4计数排序

      曾经在一个不是很正式的面试中,被问到一个问题:怎么对1亿个数字进行排序?当时没加思索就答:快排。毕竟以有限的知识,只能想到伟大的快排了。又被追问,在1亿的数量级下,lgn也是不小的开支,况且是乘,对吧?最后知道答案是计数排序,那疑问就是,它真的有这么快吗?下面就开始美好的学习吧。

   首先,我们将比较排序抽象的视为决策树,一颗决策树是满二叉树(in fact,显然不是,因为只能保证除叶子节点外的每个节点都有2个子节点,但是没法保证所有叶子节点都在最后一层,所以没法达到满的状态,这点上同意TankyWoo)以书上决策树为例:

               虽然这棵树有俩地方不是按常理从大到小,不过不影响整体效果。

      为什么要引入决策树呢,是为了说明,各元素的顺序基于输入元素间比较的这种排序,即比较排序,在最坏情况下需要Ω(nlgn)次比较来进行排序。然后引入几个个排序时间为Θ(n)的排序算法。

      从决策树可以看到,要使排序算法能正确地工作,必要条件是n个元素的n!种排列中的每一种都要作为决策树的一个叶子而出现。(当然啦,不到叶子,什么都不输出,那还排什么呢?)从树上还能看到,一个比较排序的最坏情况比较次数,是和决策树的高度相等的。

     证明过程如下:


      从这个结果来看,堆排序和归并排序是渐进最优的了。

      

     下面正式进入计数排序的学习。    

     计数排序的基本思想,是对每一个输入元素x,确定出小于x的元素个数。发现了吗?这计数排序就根本不基于比较,蛋疼了吧,直接就超越Θ(nlgn)的下界了,颤抖吧,凡人。

计数排序的一个实现程序如下:

//============================================================================
// Name        : Count.cpp
// Author      : xia
// Copyright   : NUAA
// Description : 计数排序的实现
//============================================================================
#include <iostream>
#include <vector>
#include <algorithm>
#include <ctime>//rand()
#include <iomanip>//setw
#include <climits>//INT_MIN

using namespace std;
const int MAX = 1000;

void CountSort(vector<int> &A , vector<int> &B , int k)
{
	//for i<-0 to k , C[i]<-0
	vector<int> C(k+1);//初始化为0
	int i;
	for ( i=1 ; i<A.size() ; i++)//C[i]包含等于i的个数
		C[A[i]] = C[A[i]] + 1 ;
	for ( i=1 ; i<=k ; i++)//C[i]包含小于或等于i的元素个数
		C[i] = C[i] + C[i-1];
	for ( i=A.size()-1 ; i>0 ; i--)
	{	
		B[C[A[i]]] = A[i] ;
		C[ A[i] ] --;
	}
}

int main(int argc, char **argv)
{
	vector<int> v; 
 	int i,k=0;
	srand((unsigned)time(NULL));
	for ( i=0 ; i<MAX ; i++)
	{
		v.push_back(rand()%100);//为了造成足够重复数,体现计数
		if (v[i]  > k)
			k = v[i] ;
	}
	v.insert(v.begin(),INT_MIN);//插入个负无穷,0位置不用
	vector<int> result(v); //用result来保存结果

	CountSort(v,result,k);

	for (i=1 ; i<result.size() ; i++)
	{
		cout << setw(3) << result[i];
		if (i%25 == 0)
			cout << endl;
	}
	return 0;
}
  

  计数排序的精髓,在于引入计数的数组C[k+1],在第一次将CountSort的前2个循环执行完后,对每个A[i],C[A[i]]即为A[i]在输出数组上的最终位置,in为共有C[A[i]]个元素小于等于A[j],由于每个元素可能不一定不同,所以每将一个值A[i]放入数组B时,都要减少C[A[i]]的值。不过说实话,看到

B[C[A[i]]] = A[i] ;
三层方括号的时候,有没有想砸电脑的冲动?

      计数排序,第一个for消耗Θ(k),第二个for消耗Θ(n),第三个for消耗Θ(k),第四个for消耗Θ(n),那总共消耗就是 Θ(n)。所以,在时间中,当k=O(n),常常采用计数排序,这个时候运行时间为Θ(n)。
    本例的运行结果如下:


其实理论上, 我们对于A和B的0元素也是可以使用的,这里为了切合源代码,弃之不用,插入个负无穷数字,当我们采用0元素时,即如下所示时:

void CountSort(vector<int> &A , vector<int> &B , int k)
{
	vector<int> C(k+1);//初始化为0
	int i;
	for ( i=1 ; i<A.size() ; i++)//C[i]包含等于i的个数
		C[A[i]] = C[A[i]] + 1 ;
	for ( i=1 ; i<=k ; i++)//C[i]包含小于或等于i的元素个数
		C[i] = C[i] + C[i-1];
	for ( i=A.size()-1 ; i>=0 ; i--)
	{	
		B[C[A[i]]] = A[i] ;
		C[ A[i] ] --;
	}
}

  也能输出最终结果。不过 最上面的程序和这个,有时候都会出这个错,概率现象呢么?不解,莫非是重复删除内存空间了?先不管吧。

 

 另外,由于C数组下标 i 就是A 的值,所以我们不需要保留A中原来的数了,可以考虑u如下代码实现,好处是减少了一个数组B

void CountSort(vector<int> &A , int k)
{
	vector<int> C(k+1);//初始化为0
	int i;
	for ( i=1 ; i<A.size() ; i++)//C[i]包含等于i的个数
		C[A[i]] = C[A[i]] + 1 ;
	int z=1;
	for (i=0 ; i<=k ; i++)
	{
		while (C[i]--  > 0)
			A[z++] = i;
	}
}

     参考自:http://www.cnblogs.com/eaglet/archive/2010/09/16/1828016.html。经验证,结果正确且不崩。

     另外,可以参考http://www.cppblog.com/tanky-woo/archive/2011/04/24/144890.html,TankyWoo的代码,他的数组是用全局保存,所以没有传参过程,测试案例为书上P99页的图,也比较具体。

 与快速排序的比较结果,是比较惊人

vc6下:

 

1w

10w

100w

快速排序

10

127

1048

计数排序

3

31

309

gcc下:

 

1w

10w

100w

快速排序

3

24

288

计数排序

0

4

39

      看到这个结果,我很受伤。难道这就是传说中的“所谓一山还有一山高”吗?

      本节最后,还能看到计数排序的重要性质是它的稳定的:具有相同值的元素在输出中的相对次序与在输入数组中的次序相同。那么,在怎么保证稳定性的呢?应该是最后一个for循环从length[A]减到1,而且C[A[i]]--构成,因为在第二个for中加入A元素的时候,从小到大,可以看做一个入栈和出栈的过程,保证了相同元素的相同位置。计数排序的稳定性对基数排序的正确性,是非常关键的,等着看看吧,嘿嘿。。。

     菜鸟gose on ~~~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值