下划线间隔数字 排序_数据结构学习之排序算法(c++)

05baa50ab513cbc05443ea5a4f09e239.png

本文主要介绍了六种排序:冒泡排序、希尔排序、归并排序、插入排序、快速排序、堆排序。并总结了自己对各种排序方法的一点看法以及最后对性能进行了比较。

这里讨论的排序是基于比较的排序(>、=、<有定义,例如1、2、3、4、5就有大小之分,但是 你 我 他,就没法用> = <来进行比较),只讨论内部排序,什么是内部排序呢?内部排序是指待排序列完全存放在内存中所进行的排序过程,适合不太大的元素序列.

一、冒泡排序

通过每次交换两个相邻的数字,每一次排序将最大或者最小的数字移到应该放的位置,接下来下一次循环将第二大或者第二小的数字放在应该放的位置,一个具体的排序过程如下所示:

18c0d52bb8ee8938a0004d3fb679766b.png
图1

从上面这张图可以看出,首先是最大的4353在一次排序后被交换到了最后的位置,然后是第二大的3456被交换到了倒数第二的位置,这样不断的交换,直到最后所有的位置都放应该放的数字,但是可以看到从第9行开始,整个数组都没有了任何变化,其实这就是最基本的冒泡排序可能存在的冗余计算,我们首先看下面的代码:

#include

所以下一步的优化工作就是去掉图1中从第9行开始后的那些冗余操作,代码也比较简单,就是当数组中不存在交换这个操作之后,就可以结束排序了,代码如下:

#include

996499caed2494939f5c09d8667b2649.png
图2

从图2中可以看出,后续的那些无用的比较已经没有了,接下来测试一下冒泡排序的性能。

1W个排序的数,排序用时为:

f647a27511ef05d5de141ceda6fc5b35.png

1W个乱序的数,排序用时为:

df885a3e4c29dad3514f28309e7bf3d0.png

最好情况下它的时间复杂度为O(N),只有在待排序数字是已经排好序的情况下才会出现,也就是第一种情况,最坏情况下它的时间复杂度是O(N^2),是在待排序数字是完全逆序的情况下,冒泡排序是一个稳定排序,因为当A[i]和A[i+1]两者相等时,两者之间是不需要交换的,所以保证了原来的相对顺序。

二、插入排序

插入排序类似于一个取扑克牌的过程,首先有一个具有N个数字的未排序的数组A,取出其第一个数字A[0],这样A[0]就是一个有序部分,A[1]到A[N-1]就是剩下的未排序的数字,然后取出未排序部分的第一个数字A[1]加入到已排序的部分,从已排序部分的最后一个数字开始比较,如果待插入数字小于此数字,继续和已排序部分中此数字的前一个数字比较,直到找到第一个小于待插入数字的已排序部分数字,或者已排序部分被遍历完了,在已排序部分的首部位置插入该数字,一直到将所有的未排序部分的数字都插入已排序部分,则排序结束,排序过程如下所示:

43f5176799034221a737fcdf7005c6cd.png
图3

可以看出是将后半部分的数字依次插入前半部分,第一次插入383,第二次插入886,第三次插入777,一直到插入763代表排序结束。

代码如下所示:

#include

最好情况下它的时间复杂度为O(N),也是在待排序数字已经排好序的情况下,最坏情况下它的时间复杂度是O(N^2),是在待排序数字是逆序的情况下,并且它也是一个稳定排序,因为这条语句a[j]<a[j-1],在a[j]等于此a[]j-1]的时候,会停止往前移动,所以相对位置并没有改变。

看最好情况和最差情况,上面两种排序是一样的,那么针对上面两个排序算法,一共需要多少次数字交换才能完成排序呢?其实两者需要的次数是相等的,因为它们都是交换相邻的数字。

数组之所以无序是因为存在逆序对,交换两个相邻数字正好可以消去1个逆序对,而上述两个简单排序都只是交换两个相邻的数字,所以两者进行交换的次数是相等的,而且插入排序在序列基本有序的情况下是非常简单和高效的,更为准确的来说,插入排序的时间复杂度为O(N+I),其中N是元素个数,I是数组中的逆序对。

下面说两条定理:

2.1任意N个不同元素所组成的序列平均具有N(N-1)/4 个逆序对。

2.2任何仅以交换相邻两元素来排序的算法,其平均时间复杂度为O(N^2)。

通过上述两个定理,可以得出如果想提高一个排序算法的效率,那么必须在一次交换中不止消去一个逆序对,那么就需要每次交换相隔比较远的2个元素,只有这样才能一次消去多个逆序对。

接下来测试一下插入排序的性能。

1W个排序的数,排序用时为:

d5eb9354277e86f644fc18d74d0069b9.png

1W个乱序的数,排序用时为:

e53a6cdb5d7c3c301de40fdf86650ed5.png

三、希尔排序(Donald Shell)

我们首先定义一个增量序列Dm>D(m-1)>.....>D1=1,然后对对每个Dk进行一个K间隔排序(K=M,M-1,....1),希尔排序的基础之一就是Dk间隔排序后有序的数组,在执行D(k-1)间隔排序后,它依然是一个Dk间隔有序的。最原始的希尔排序中,选择的的增量序列为Dm=N/2,Dk=D(k+1)/2,换言之就是假设有一个N个数字的未排序数组A,第一次做一个N/2长度的排序,意思就是在数组的有效长度范围内,将A[0]与A[0+N/2]进行比较,将A[1]与A[1+N/2]进行比较,直到最后比到最后一个有效长度为止,第二次选择的长度为N/4进行比较,同理,将A[0]与A[0+N/4]进行比较,将A[1]与A[1+N/4]进行比较,直到最后比到最后一个有效长度为止,一直比到长度为1,也就是两个相邻数字比较,代码如下:

#include

这种排序算法在最坏的情况下,依然是一个N^2的复杂度,在当增量元素不互质的情况下,小增量可能根本就不起作用,只是在浪费比较时间而已,所以对于希尔排序来说,很重要的一个是选择合适的增量序列,下面介绍两个很常用的增量序列:

3.1 Hibbard增量序列

这个增量序列是按照Dk=2^k-1来进行计算的,这样保证了相邻的两个增量序列是互质的,这个增量序列在最坏的情况下O(N^(3/2)),在平均情况下目前只有一个猜想(2014年)是O(N^(5/4)).

3.2 Sedgewick增量序列

这个序列是{1,5,19,41,109,....},它是9*4^i-9*2^i+1或4^i-3*2^i+1,这个增量序列在最坏的情况下猜想为O(N^(4/3)),在平均情况下目前一个猜想(2014年)是O(N^(7/6))(2014年).

下面测试下不同增量序列的排序性能。

普通增量(N/2,N/4,......1):

59950b000f497d28fc6c083da11bf5f1.png

Hibbard增量序列:

52b5c910d26b1b676bc73ca34c5dc4aa.png

3*x+1增量序列:

0e79d18cbb1f2799b91f52f95f287119.png

四、堆排序

堆排序是一种利用最大堆或者最小堆进行排序的一种排序算法了,它是结合了选择排序和堆的特性的一种排序算法,如果只是用暴力算法每次找出数组A中的最小数字,那么需要遍历整个数组,然后外层还有一个循环,所以整个排序算法的时间复杂度是N^2级别的,排序N个数字,肯定要将N个数字素都遍历一遍,所以只能从找到最小元素这里做出改进,那如何找到最值数字呢,这里就用到了堆这种数据结构,代码如下所示:

#include

已知在从堆中取数字之后调整堆的结构的时间复杂度是logN,因为一共有N个元素,所以要调整N次,所以时间复杂度是N*logN,如果排序利用的是最小堆结构,那么它需要额外的O(N)的空间用来存储堆,并且复制元素也需要一定的时间,那么可以就在数组提供的空间内实现一个堆排序,不占用额外空间吗?可以的,只需要将数组设置成最大堆结构,每次把A[0]这个元素弄到A[N-1]的位置,然后最大堆长度减一,调整堆,接下来再把A[0]放到A[N-2]这个位置,就可以实现最大堆排序,也就是上面代码所表示的排序方法。

接下来测试一下堆排序的性能。

1W个排序的数,排序用时为:

f3a6806834e663063246b904dafa4524.png

1W个乱序的数,排序用时为:

0790a79c82612afd855f584ac7c691ec.png

五、归并排序

归并排序的核心思想可以用下面这张图来表示:

8cb8667d57f3591725ad6b801455ca88.png

将上图中的左右两个有序子数组合并成一个有序数组需要的时间复杂度为O(N).那么首先需要得到一个左右两边分别有序的子数组,那如何才能得到一个左右分别有序的子数组呢,同理,左右两边的有序子数组可以由它们自己的子数组归并而来,这样就变成了4个子数组,同理,还是相同的问题,如何让这四个子数组有序呢,接着划分,一直到子数组被划分成为了只有一个数字的子数组,那么只有一个数字的子数组就自然是有序的,整个过程代码如下:

#include

此算法的时间复杂度为T(N)=T(N/2)+T(N/2)+O(N),这样的算法的时间复杂度为O(NlogN),推理过程如下图,并且由上归并过程可知,当左右相等时,优先把左边归并进去,所以是一个稳定的排序算法。

9bdf8a8ca80931bfbc70e451a8190a65.png

上述代码利用了递归,可以将其修改为循环,这样提高程序的运行效率,循环算法就是将递归算法的过程反着来,非递归算法要注意几个细节,一个是如何确定此时的排序数组是在辅助空间内还是在原来的数组中,还有就是奇数时,数组怎么进行归并。

接下来测试一下归并排序的性能。

1W个排序的数,排序用时为:

3f98cf3692d38a2f0e4d22d86bcfe1e7.png

1W个乱序的数,排序用时为:

64194e467d7283f17725f29635fd538e.png

六、快速排序

快速排序的思想和归并有点类似,就是一样的分而治之的思想,选择一个数字,将比它大的放在左边,比它小的放在右边,这样该数字的位置在数组中就肯定正确了,不需要移动它了,这也是快速排序之所以很快的一个原因,比如插入排序,进入的数组之后可能还要面临多次移动,不像快排一步到位。由此算法的性质,可知最佳情况是每次选择的这个数字都是中间的数字,这样就是一个明显的NlogN的时间复杂度,代码如下:

#include


由以上代码可以看出,每次点的选择是最重要的一步,如果选择每次都是最差的情况,那么它会退化到N^2的时间复杂度,那什么是最差情况呢,如下图所示:

c111b8e1a407c0691b0425aa78a22bfa.png

此时的时间复杂度为:

2078d85ed272b5ebccdeced7d1e76117.png

快排中还会遇到一些问题,比如我们刚好有数字等于选定的数字怎么办,一是不理它,继续移动指针,二是停下来等待交换,两种方法最后的排序结果都是正确的,但是要选择第二种,为什么呢?因为交换之后,会让下一次选取的数字更加接近中间的数字值,这样能够更逼近NlogN,比如假设有一个全是1的数组,开始比较的时候,发现第一个和最后一个都等于1,于是交换,然后再比较,再交换,最后虽然进行了一堆无用的交换,但是最后i和j的位置基本是在数组中间的,而如果遇到之后不交换,直接跳过,那先从后面开始寻找小于选定数字的数字,直到找到待排序数组的头部都找不到,但是此时i和j的位置就在很左边的地方了了,很有可能退化成一个N^2的算法,解决的办法是这样的,在设置一个左边结束坐标,一个右边结束坐标,当相等时可以将该数字放在选定数字的身边,这样就相当于替所有值相等的选定数字找到了该待的位置,还减少了下一步的计算量。

快速排序的速度确实很快,但是在小规模的排序上,例如(1到100),因为存在递归,所以它的速度可能还比不上插入排序,所以当递归数据的规模很小的时候,应当停止递归,直接调用简单排序。显然,快速排序也是一个不稳定排序。

接下来测试一下归并排序的性能。

1W个排序的数,排序用时为:

82459df1fdd184e2b2aa3ea0fa5380a6.png

1W个乱序的数,排序用时为:

4855616cbf7bcc5219be7cd410ae2378.png

排序方法里面还有表排序和基排序,综合上面的时间数据我们可以得出结论:并没有任何一种排序是在任何情况下都表现得最好的,但是有些排序方法在每种情况下表现都很优秀,最后用一张图来表达这些排序算法的效率:

918dd7abee5d3f1c3084ec1b3092f8a0.png

这篇文章是在学习浙大那门数据结构的课之后写的,其中还有很多没有学习好的地方,这是我第二次总结了,写下来是想看一下自己的错误和不懂的地方,写完之后发现自己确实很难讲一个事情讲清楚,后面接着改进,希望能越来越好。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值