黑马程序员____四种排序算法的比较分析

----------------------android培训java培训、期待与您交流! ----------------------

一、问题描述
比较insertionsort,quicksort,mergesort以及radixsort对32位无符号整数的排序效果。输入数据随机产生,数据范围为[0,2^32-1],输入数据量分别为:10,10^2,20^3,10^4,10^5,10^6,10^7,2*10^8,(10^9,此数量级选做)。

二、算法分析
四种算法的实现在算法导论书上都已经写得很清楚了,因此这里不再描述算法以及伪代码,仅仅说明下每种算法的时间和空间复杂度
1)插入排序(insertionsort)
时间复杂度O(n^2),最好情况n,最坏情况n^2
空间复杂度Θ(n)
插入排序实现起来非常简单,不用额外的空间开销,不过时间复杂度较高。
2)归并排序(mergesort)
时间复杂度Θ(nlgn)
空间复杂度Θ(1.5n)
分治法思想的典型代表,将数组分成两段,分别排序,然后合并成1个序列,时间复杂度较低,空间上需要额外空间,书上给了需要L和R两个0.5n的数组,实际上R数组可以不用,直接使用原数组即可,因此空间上的额外开销为0.5n
3)快速排序(quicksort)
平均期望下的时间复杂度Θ(nlgn),最坏情况也为n^2
空间复杂度Θ(n)
比较排序中几乎最好的排序,不增加额外的空间开销,并且由于读数据的特殊性,cache命中的概率很高,平均时间也是最低的,实现起来也相对比较简单。
4)基数排序(radixsort)
期望的复杂度趋于Θ(n)
空间复杂度Θ(2n)
基数排序从低位向高位进行有限次排序,每次排序采用稳定排序,这里使用的是计数排序,虽然空间开销是最大的,但是速度也是最快的,但它仅限于有范围限定的整数,适用范围没那么理想。
三、设计实现
本程序使用c++代码进行编写,直接双击可执行文件即可打开,界面如下所示。

下面将详细介绍随机数的生成以及四种排序算法的设计技巧。
1)随机数生成
由于题目要求生成[0,2……32−1]之间的随机数,而c标准库中的随机数函数rand()只能生成[0,32767]之间的随机数,因此采用拼接的方法来生成32位的随机数。
将32位的数分成三段,即2位,15位,15位三段。后面两段可以直接用rand()函数生成,前面2位的段可以去尾数之后再对4取模,这样保证了所有数的等概率。
2)插入排序
按照书上的伪代码实现的,由于算法本身已十分简单,没有可改进的变化。
3)归并排序
与书上的伪代码相比,除去了R数组,由于在归并过程中,右半边的数据没有必要复制出来做保护,它不可能会被提前占用位置,是安全的,因此没有必要将右半边的数据拷贝出来,然后在填回原数组,并且当最后剩余的大数据都在右半边时,可以不用再执行拷贝。具体见如下伪代码。
int n1 = q - p + 1;
unsigned int *L = new unsigned int[n1];
memcpy(L, A + p, 4 * n1);
int i = 0, j = q + 1;
while (i < n1 && j <= r){
if (L[i] <= A[j]){
A[p] = L[i];
p++;
i++;
}else{
A[p] = A[j];
p++;
j++;
}
}
//如果i<n1,则证明左半边还有数据,将其拷贝到原数组中
for (; i < n1; i++){
A[p] = L[i];
p ++;
}
4)快速排序
代码实现基本按照书上提供的伪代码,除了一个小细节方面,书上在交换A[j]和A[q]时可以先判断一下j和q是否一样,如果一样的话就没必要自己和自己交换位置了。
5)基数排序
基数排序的稳定排序使用的是计数排序,计数排序的伪代码书上已经给了很详细了。基数排序只有两点可以研究,
a)研究划分的每一小段应该包含多少位,即r的大小。例如r = 8时,将32位的数分成4个8位的数,这样要做4次计数排序,每次排序的取值范围是0到255 。r越大,次数越少,但是每次的工作量越大;r越小,次数越多,每次的工作量越小。书上给出了排序的时间复杂度公式 br (n+2r),因此书上给出了理论上的r的最合理的值为lg(n),但是可以发现b/r基本是一个小数,因此当 br 为同一个整数值的时候,r越小越好。因此理论上的最合理的值要在lg(n)的基础上做个修正,找到相同的 br 的情况下最小的r,但是理论和实验还是有些不同的,这个会在实验部分做详细分析。
b)研究如何快速的将原来的32位整数拆分为r位的若干段,这里取模的话肯定是可以实现的,但是由于取模运算非常慢,因此采用最直接的位操作进行每段的提取,可以先将原32位数进行右移之后再与11..111进行与运算得到想要的r位,公式如下 a= A≫offset & ( 1≪ r + 1 − 1)
其中offset=i ∙r (i=0,1,2…)
四、实验分析
测试机器环境如下:
CPU:AMD Athlon Ⅱ×4 640 @ 3.0 GHz
内存:4.00GB(3.25GB可用)
操作系统:win7 旗舰版 32位
编译环境:vs2010 C++
编译开关:vs2010 默认release开关

首先分析基数排序中r值的选取,针对不同的r进行测试,测试数据如下所示。以下数据从10到10^6测了100次取平均,10^7测了10次取平均,10^8和2*10^8由于时间过长,只测了1次,但是基数排序循环的次数比较固定,不像快速排序那么变化多,因此在大数据时只测1次也足够了。
表4.1 基数排序r值分析(单位:μs)

表中的log(n)就是书上为修正的理论值,fix就是取相同 br 的情况下最小的r,然后best是实验所得到的最优的r值,可以发现当n较小时,理论值预测的相当准确,可是当n逐渐变大到106时,r的最优值反而慢慢变小了,为了探究这个原因,我详细分析了当n较大时计数排序代码中的每段所执行的时间,看下面的计数排序的主要代码。
memset(C, 0, 4*(k+1));
for (int j = 0; j < n; j++){
C[(A[j]>>offset)&tmp]++;
}
for (int i = 1; i <= k; i++){
C[i] += C[i-1];
}
for (int j = n - 1; j >= 0; j --){
B[--C[(A[j]>>offset)&tmp]] = A[j];
}
可以看到以上的代码分别是k,n,k,n次的循环,经过中间的计时可以发现,前面3个循环所花费的时间相对于第4个循环来说可以忽略不计了,前3个总时间仍然没到第4个循环的1/10,因此第4个循环的时间将决定整个计数排序的时间,可以看到第4个循环次数是n,跟r无关。跟r有关的只有C数组的大小,因此立刻想到了cache问题,当r较小时,C数组的总容量较小,为几k到几十k的样子,这个大小可以全部装入cache中;当r较大时,C数组的大小就使得它无法全部放入cache中了,因此可以认为当r较小时,cache命中的概率将非常高,大大加快读写速度,而当r较大时,频繁的读写内存使得速度变慢。因此使得r的最佳值偏离理论值的最主要的原因应该就是cache命中的问题。
既然知道了这个情况,那么继续进一步分析数据,得到一个较好的r也是有必要的,原则有代表性的几个r值数据画成图之后
图4.1基数排序r值分析

通过图表和数据可以发现,r=8的值是相对来说最优的一条线,它在大数量级时是最优的,在小数量级时与最优的差距也不大,因此最后实现的r值选用的就是8。
然后在确定了基数排序的r之后,再将基数排序与其他三种排序结果进行比较。数据从10^1到10^6之间都是测了100次取平均,merge的2*10^8规模的只测了1次,其他都是测了10次取平均。插入排序由于在10^6以后就时间过大,因此没有再测。
表4.2四种排序时间比较(单位:μs)

图4.2 四种排序时间比较

从图中可以看出在n为10,100时,插入排序和快速排序比较快,而归并排序和基数排序由于过多额外的开销而比较慢,在n大于10^6后,插入排序n2的复杂度劣势就体现的非常明显了。当n从1000开始,排序时间的排名就固定为基数排序<快速排序<归并排序<插入排序。可见快速排序由于它特有的缓存命中,在与相同时间复杂度的归并排序比较的时候优势比较明显,基本只需要归并排序一半的时间即可。而基数排序由于它趋于n的时间复杂度,因此优势比较明显,只需要不到快速排序1/2的时间即可完成。
因此,在确定范围的整数排序中,基数排序是最好的,在条件允许的情况下应该选择基数排序。在比较排序中,快速排序的优势比较明显,而且空间上也是最好的,因此在比较排序中应该毫不犹豫选择快速排序。
五、收获
本来以为这次的排序应该还是比较简单的,书上都已经有详细的伪代码了,而且以前接触的也算比较多了,唯一接触较少的就是基数排序,因此这次把重点放在了基数排序上,结果果然对于基数排序的研究还是很有意思的,从如何获得32位数中的r位开始,到r的选择都有一定的探究性,最有趣的莫过于研究为什么理论修正值和实验最佳值之间仍然有差距,然后分析每个程序段落所消耗的时间,最后想到了cache命中。可见理论和实际总是有差距的,在考虑复杂度时,我们是不会考虑cache的,相信这也是快速排序不能从复杂度上分析出的优秀点。

        ---------------------- android培训java培训、期待与您交流! ----------------------

        详细请查看:http://edu.csdn.net/heima


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值