好记性不如烂笔头o(^▽^)o
前言
记得之前毕业找工作的时候好好总结过,但是,工作中基本不怎么用到这些排序算法,所以久而久之就忘了。以前写的文章链接(包括详细的思路、图和完整代码):
《直接选择排序》
《冒泡排序算法》
《希尔排序算法》
《快速排序算法》
《直接插入排序》
其实每种看了后很容易混淆和记不住,所以这里就对这些排序算法进行归类和总结,主要涉及十大排序算法,包括算法思路和算法分析。最后给出排序算法的选择,让你在面对排序时心里有数。
正文
排序算法总结
分类总结
排序算法按照是否涉及数据的内、外存交换分为内排序和外排序。
排序过程中整个文件都放在内存中处理,排序时不涉及数据的内外存交换,称为内排序,使用于记录个数不是很多的小文件,反之为外排序。
按照是否进行比较分为比较排序和非比较排序。
非比较排序:桶
比较排序:其他
按照性能可分为稳定排序和不稳定排序。
稳定排序:插入、冒泡、归并、桶、二叉树排序、基数排序
不稳定排序:选择、快速、堆、希尔、计数
那么,什么是稳定的排序?
假设在排序中存在多个相同关键字的记录,经排序后,这些记录的相对位置保持不变,则是稳定的排序。
下面表格给出常见排序算法的一些复杂度统计:
排序方法 | 平均情况 | 最好情况 | 最坏情况 | 辅助空间 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O(n^2) | O(n) | O(n^2) | O(1) | 稳定 |
选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 稳定 |
直接插入排序 | O(n^2) | O(n) | O(n^2) | O(1) | 稳定 |
希尔排序 | O(nlogn) ~ O(n^2) | O(n^1.3) | O(n^2) | O(1) | 不稳定 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不稳定 |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 稳定 |
快速排序 | O(nlogn) | O(nlogn) | O(n^2) | O(logn) ~ O(n) | 不稳定 |
根据上表,按平均时间将排序分为四类:
(1)平方阶(O(n2))排序
一般称为简单排序,例如直接插入、直接选择和冒泡排序;
(2)线性对数阶(O(nlgn))排序
如快速、堆和归并排序;
(3)O(n1+£)阶排序
£是介于0和1之间的常数,即0<£<1,如希尔排序;
(4)线性阶(O(n))排序
如桶、箱和基数排序。
各排序算法思想
十大排序算法:
一、冒泡(Bubble)排序——相邻交换
二、选择排序——每次最小/大排在相应的位置
三、插入排序——将下一个插入已排好的序列中
四、希尔排序——缩小增量
五、归并排序
六、快速排序
七、堆排序
八、计数排序
九、桶排序
十、基数排序
冒泡排序
基本思想:两两比较待排序记录的关键字,发现两个记录的次序相反时即进行交换,直到没有反序的记录为止。
因为每一趟排序都使有序区增加了一个气泡,在经过n-1趟排序之后,有序区中就有n-1个气泡,而无序区中气泡的重量总是大于等于有序区中气泡的重量,所以整个冒泡排序过程至多需要进行n-1趟排序。
若在某一趟排序中未发现气泡位置的交换,则说明待排序的无序区中所有气泡均满足轻者在上,重者在下的原则,因此,冒泡排序过程可在此趟排序后终止。
算法分析:时间复杂度最好的情况为O(n),平均和最坏的情况是O(n^2)。
选择排序
基本思想是:每一趟从待排序的记录中选出关键字最小的记录,顺序放在已排好序的子文件的最后,直到全部记录排序完毕。
初始状态:无序区为R[1..n],有序区为空,第i趟有序区和无序区分别为R[1..i-1]和R[i..n]。无论文件初始状态如何,在第i趟排序中选出最小关键字的记录,需做n-i次比较,因此,总的比较次数为:n(n-1)/2=0(n2)。
算法分析:最差平均时间复杂度都是O(n^2),是就地排序,选择排序与冒泡排序的时间复杂度相同,但简单选择排序的性能要比冒泡优。选择是通过下标改动来排,冒泡是是直接交换,冒泡的交换次数要比选择多。
插入排序
基本思想:每次将一个待排序的记录,按照其关键字大小插入到已经排好序的子序列的适当位子,知道全部记录插入完成为止。
假设待排序的记录都存储在R[n]中,首先把R[1]自成一个有序区,R[2..n]为无序区,然后将R[2..n]中的记录依次插入到R[1..i]中,直到生成含有n个记录的有序区。在插入的某一中间时刻,存在两个区,一个是R[1..i-1]有序区,一个R[i..n]无序待排序的区。
算法分析:最差、平均都是O(n^2),最好是O(n)。
希尔排序
希尔排序(Shell Sort)是插入排序的一种。先取一个小于n的整数d1作为第一个增量,所有距离为dl的倍数的记录放在同一个组中。先在各组内进行直接插人排序;然后,取第二个增量d2 < d1
重复上述的分组和排序,直至所取的增量dt=1(dt<dt-l<…<d2<d1)
,即所有记录放在同一组中进行直接插入排序为止。实质上是一种分组插入方法。
算法分析:时间复杂度O(nlogn)
1.增量序列的选择
Shell排序的执行时间依赖于增量序列。好的增量序列的共同特征:
① 最后一个增量必须为1;
② 应该尽量避免序列中的值(尤其是相邻的值)互为倍数的情况。
有人通过大量的实验,给出了目前较好的结果:当n较大时,比较和移动的次数约在n^l.25到1.6n^1.25之间。
2.Shell排序的时间性能优于直接插入排序,原因:
①当文件初态基本有序时直接插入排序所需的比较和移动次数均较少。
②当n值较小时,n和n^2的差别也较小,即直接插入排序的最好时间复杂度O(n)和最坏时间复杂度0(n^2)差别不大。
③在希尔排序开始时增量较大,分组较多,每组的记录数目少,故各组内直接插入较快,后来增量di逐渐缩小,分组数逐渐减少,而各组的记录数目逐渐增多,但由于已经按di-1作为距离排过序,使文件较接近于有序状态,所以新的一趟排序过程也较快。
因此,希尔排序在效率上较直接插人排序有较大的改进。
归并排序
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
将二个有序数列合并:这个非常简单,只要比较二个数列的第一个数,谁小就先取谁,取了后就在对应数列中删除这个数。然后再进行比较,如果有数列为空,那直接将另一个数列的数据依次取出即可。时间复杂度O(N)。
归并排序,其的基本思路就是将数组分成二组A,B,如果这二组组内的数据都是有序的,那么就可以很方便的将这二组数据进行排序。可以将A,B组各自再分成二组。依次类推,当分出来的小组只有一个数据时,可以认为这个小组组内已经达到了有序,然后再合并相邻的二个小组就可以了。这样通过先递归的分解数列,再合并数列就完成了归并排序。一共要logN步。
算法分析:设数列长为N,将数列分开成小数列一共要logN步,每步都是一个合并有序数列的过程,时间复杂度可以记为O(N),故一共为O(N*logN)。因为归并排序每次都是在相邻的数据中进行操作,所以归并排序在O(N*logN)的几种排序方法(快速排序,归并排序,希尔排序,堆排序)也是效率比较高的。
快速排序
基本思想:快速排序是一种划分交换排序。它采用了一种分治的策略,通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
1.先从数列中取出一个数作为基准数。
2.分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
3.再对左右区间重复第二步,直到各区间只有一个数。
算法分析:平均时间复杂度为O(n*log n),最坏情况下O(n^2),空间复杂度O(log n)。
算法分析:与归并排序不同,这两个子问题并不保证具有相等的大小,而是在适当的位置进行并且非常有效。
初值选取问题:错误的方法是选取第一个元素;随机选取,策略安全,但减少不了其余部分的平均时间;三数中值分割法,选取左端、右端、中心位置上三个元素的中值。
堆排序
举例最小堆来说,首先可以看到堆建好之后堆中第0个数据是堆中最小的数据。取出这个数据再执行下堆的删除操作。这样堆中第0个数据又是堆中最小的数据,重复上述步骤直至堆中只有一个数据时就直接取出这个数据。
由于堆也是用数组模拟的,i结点的父结点下标就为(i – 1) / 2,它的左右子结点下标分别为2 * i + 1和2 * i + 2。故堆化数组后,第一次将A[0]与A[n - 1]交换,再对A[0…n-2]重新恢复堆。第二次将A[0]与A[n – 2]交换,再对A[0…n - 3]重新恢复堆,重复这样的操作直到A[0]与A[1]交换。由于每次都是将最小的数据并入到后面的有序区间,故操作完成后整个数组就有序了。有点类似于直接选择排序。
算法分析:堆排序的时间,主要由建立初始堆和反复重建堆这两部分的时间开销构成,平均和最坏情况下时间复杂度都是O(N*logN) ,由于建初始堆所需的比较次数较多,所以堆排序不适宜于记录数较少的文件。堆排序是就地排序,辅助空间为O(1),它是不稳定的排序方法。
计数排序
计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数。然后根据数组C来将A中的元素排到正确的位置。
桶排序
算法思想:将数组分到有限数量的桶子里,每个桶子再分别排序,最后合并。
假设有一组长度为N的待排关键字序列K[1….n]。首先将这个序列划分成M个的子区间(桶) 。然后基于某种映射函数 ,将待排序列的关键字k映射到第i个桶中(即桶数组B的下标 i) ,那么该关键字k就作为B[i]中的元素(每个桶B[i]都是一组大小为N/M的序列)。接着对每个桶B[i]中的所有元素进行比较排序(可以使用快排)。然后依次枚举输出B[0]….B[M]中的全部内容即是一个有序序列。
桶排序利用函数的映射关系,减少了计划所有的比较操作,是一种Hash的思想,可以用在海量数据处理中。
算法分析:如果输入数据有N个,分到M个桶里,时间复杂度O(M+N)。
基数排序
基本思想: Radix sort是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。
实现:将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
算法分析:由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。基数排序也可以看成是对每一种基数位数使用桶排序。
排序方法的选择
(1)若n较小(如n≤50),可采用直接插入或直接选择排序。
当记录规模较小时,直接插入排序较好;否则因为直接选择移动的记录数少于直接插人,应选直接选择排序为宜。
(2)若文件初始状态基本有序(指正序),则应选用直接插人、冒泡或随机的快速排序为宜。
(3)若n较大,则应采用时间复杂度为O(nlgn)的排序方法:快速排序、堆排序或归并排序。
快速排序是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短。
堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况。这两种排序都是不稳定的。
若要求排序稳定,则可选用归并排序,通常可以将它和直接插入排序结合在一起使用。先利用直接插入排序求得较长的有序子文件,然后再两两归并之。因为直接插入排序是稳定的,所以改进后的归并排序仍是稳定的。
(4)若n很大,记录的关键字位数较少且可以分解时,采用基数排序较好。
虽然桶排序对关键字的结构无要求,但它也只有在关键字是随机分布时才能使平均时间达到线性阶,否则为平方阶。同时要注意,箱、桶、基数这三种分配排序均假定了关键字若为数字时,则其值均是非负的,否则将其映射到箱(桶)号时,又要增加相应的时间。