排序算法的时空复杂度、稳定性分析
1.基本概念
2.时空复杂度
3.稳定性
4.使用情况分析
排序算法总结(C语言版)已介绍排序算法的基本思想和C语言实现,本文只介绍时空复杂度和稳定性。
1.基本概念
时间复杂度:
一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法的语句执行次数称为语句频度或时间频度。记为T(n)。n称为问题的规模,当n不断变化时,时间频度T(n)也会不断变化。但有时我们想知道它变化时呈现什么规律,为此,引入时间复杂度概念。若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称O(f(n))为算法的渐进时间复杂度,简称时间复杂度。
按数量级递增排列,常见的时间复杂度有:常数阶O(1),对数阶O(log2n),线性阶O(n),线性对数阶O(nlog2n),平方阶O(n2),立方阶O(n3),...,k次方阶O(nk),指数阶O(2n)。随着问题规模n的不断增大,上述时间复杂度不断增大,算法的执行效率越低。
在各种不同算法中,若算法中语句执行次数为一个常数,则时间复杂度为O(1)。
空间复杂度:
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度。算法在运行过程中临时占用的存储空间随算法的不同而异,有的算法只需要占用少量的临时工作单元,而且不随问题规模的大小而改变,我们称这种算法是"就地"进行的,是节省存储的算法;有的算法需要占用的临时工作单元数与解决问题的规模n有关,它随着n的增大而增大,当n较大时,将占用较多的存储单元。
当一个算法的空间复杂度为一个常量,即不随被处理数据量n的大小而改变时,可表示为O(1)。
稳定性:
稳定性,是指假设待排序记录中有两个相同的元素,它们排序前后的相对位置是否变化。主要用在排序时有多个排序规则的情况下。
如何对于不稳定的算法进行改进,使之稳定?其实很简单,只需要在每个记录上加一个index,表示初始时的数组索引,当根据不稳定的算法排好序后,对于相同的元素再根据index排序即可。
2.时空复杂度
排序方法 |
时间复杂度 |
空间复杂度 |
稳定性 |
|||
平均情况 |
最好情况 |
最坏情况 |
||||
插入排序 |
直接插入 |
O(n2) |
O(n) |
O(n2) |
O(1) |
稳定 |
Shell排序 |
O(n1~2) |
O(nlog2n) |
O(n2) |
O(1) |
不稳定 |
|
选择排序 |
直接选择 |
O(n2) |
O(n2) |
O(n2) |
O(1) |
不稳定 |
堆排序 |
O(nlog2n) |
O(nlog2n) |
O(nlog2n) |
O(1) |
不稳定 |
|
交换排序 |
冒泡排序 |
O(n2) |
O(n) |
O(n2) |
O(1) |
稳定 |
快速排序 |
O(nlog2n) |
O(nlog2n) |
O(n2) |
O(log2n) |
不稳定 |
|
归并排序 |
O(nlog2n) |
O(nlog2n) |
O(nlog2n) |
O(n) |
稳定 |
|
基数排序 |
O(d(n+r)) |
O(d(n+r)) |
O(d(n+r)) |
O(n+rd) |
稳定 |
|
注:1.希尔排序的时间复杂度和增量的选择有关。 2.基数排序的复杂度中,r代表关键字的基数,d代表长度,n表示关键字的个数。 |
下述讨论中假设排序为升序。
直接插入排序:
最好的情况:序列已经是升序排列,在这种情况下,需要比较(n-1)次即可。O(n)
最坏的情况:序列是降序排列,此时需要比较n(n-1)/2次。O(n2)
希尔排序:
希尔算法在最坏的情况下O(n2)和平均情况下O(n1~2)执行效率相差不是很多,与此同时快速排序在最坏的情况下执行的效率会非常差。
希尔排序是按照不同步长对元素进行插入排序,刚开始元素很无序时,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小,插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比o(n2)好一些。
直接选择排序:
比较次数与初始状态无关,为n(n-1)/2。故最好,最坏和平均情况均为O(n2)。交换次数为0~n-1,最好的情况是序列为升序,交换0次,最坏的情况是序列为降序,交换n-1次。赋值次数为0~3(n-1)。(?)
堆排序:
最好,最坏和平均情况均为O(nlog2n)。
冒泡排序:
最好的情况:序列已经是升序排列,在这种情况下,需要比较(n-1)次即可。O(n)
最坏的情况:序列是降序排列,此时需要n(n-1)/2次比较和n(n-1)/2次交换。O(n2)
插入排序和冒泡排序的比较:插入排序在比较过程中发现逆序对时,不进行交换,只是赋值,而冒泡排序每发现一次逆序对就要进行交换。故插入排序比冒泡排序快。
快速排序:
最好的情况下:每次都将序列均分为两个部分,故为O(nlog2n)(一般二分的复杂度都和log2n相关)。
最坏的情况下:序列已经是升序排列时,要比较n(n-1)/2次,故为O(n2)。此时快速排序不如插入排序快。
归并排序:
归并排序是一种非“就地”排序,需要与待排序序列一样多的辅助空间。故归并排序的缺点是所需额外空间多。
对长度为n的数组,需进行log2n趟二路归并,每趟归并的时间为O(n),故其时间复杂度无论是在最好情况下还是在最坏情况下均是O(nlog2n)。
基数排序:
分配需要O(n),收集为O(r),其中r为分配后链表的个数,以r=10为例,则有0~9这样10个链表来将原来的序列分类。而d,也就是位数(如最大的数是1234,位数是4,则d=4),即"分配-收集"的趟数。因此时间复杂度为O(d*(n+r))。
3.稳定性
稳定排序:直接插入排序,冒泡排序,归并排序,基数排序
不稳定排序:希尔排序,快速排序,直接选择排序,堆排序
(1)冒泡排序
冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,我想你是不会再无聊地把他们俩交换一下的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。
(2)直接选择排序
选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n-1个元素,第n个元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果当前元素比一个元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。比较拗口,举个例子,序列5 8 5 2 9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法。
(3)直接插入排序
插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。当然,刚开始这个有序的小序列只有1个元素,就是第一个元素。比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。
(4)快速排序
快速排序有两个方向,左边的i下标一直往右走,当a[i] <= a[center_index],其中center_index是中枢元素的数组下标,一般取为数组第0个元素。而右边的j下标一直往左走,当a[j] > a[center_index]。如果i和j都走不动了,i <= j, 交换a[i]和a[j],重复上面的过程,直到i>j。交换a[j]和a[center_index],完成一趟快速排序。在中枢元素和a[j]交换的时候,很有可能把前面的元素的稳定性打乱,比如序列为 5 3 3 4 3 8 9 10 11,现在中枢元素5和3(第5个元素,下标从1开始计)交换就会把元素3的稳定性打乱,所以快速排序是一个不稳定的排序算法,不稳定发生在中枢元素和a[j]交换的时刻。
(5)归并排序
归并排序是把序列递归地分成短序列,递归出口是短序列只有1个元素(认为直接有序)或者2个序列(1次比较和交换),然后把各个有序的段序列合并成一个有序的长序列,不断合并直到原序列全部排好序。可以发现,在1个或2个元素时,1个元素不会交换,2个元素如果大小相等也没有人故意交换,这不会破坏稳定性。那么,在短的有序序列合并的过程中,稳定性是否受到破坏?没有,合并过程中我们可以保证如果两个当前元素相等时,我们把处在前面的序列的元素保存在结果序列的前面,这样就保证了稳定性。所以,归并排序也是稳定的排序算法。
(6)基数排序
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序,最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以其是稳定的排序算法。
(7)希尔排序(shell)
希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小,插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比o(n^2)好一些。由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。
(8)堆排序
我们知道堆的结构是节点i的孩子为2*i和2*i+1节点,大顶堆要求父节点大于等于其2个子节点,小顶堆要求父节点小于等于其2个子节点。在一个长为n的序列,堆排序的过程是从第n/2开始和其子节点共3个值选择最大(大顶堆)或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。但当为n/2-1, n/2-2, ...1这些个父节点选择元素时,就会破坏稳定性。有可能第n/2个父节点交换把后面一个元素交换过去了,而第n/2-1个父节点把后面一个相同的元素没有交换,那么这2个相同的元素之间的稳定性就被破坏了。比如满二叉树5,10,7,8,9,8,9,第一次建大顶堆时,9和7交换,5,10,9,8,9,8,7,第二次建堆时,10,9,8中10最大,不需要交换,此时两个9的顺序发生了变化。所以,堆排序不是稳定的排序算法。
4.使用情况分析
(1)若n较小(如n≤50),可采用直接插入或直接选择排序。
当记录规模较小时,直接插入排序较好;否则因为直接选择移动的记录数少于直接插入,应选直接选择排序为宜。
(2)若文件初始状态基本有序(指正序),则应选用直接插入、冒泡或随机的快速排序为宜;
(3)若n较大,则应采用时间复杂度为O(nlogn)的排序方法:快速排序、堆排序或归并排序。
快速排序是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况。这两种排序都是不稳定的。若要求排序稳定,则可选用归并排序。但从单个记录起进行两两归并的排序算法并不值得提倡,通常可以将它和直接插入排序结合在一起使用。先利用直接插入排序求得较长的有序子文件,然后再两两归并之。因为直接插入排序是稳定的,所以改进后的归并排序仍是稳定的。
(4)若n很大,记录的关键字位数较少且可以分解时,采用基数排序较好。
箱排序和基数排序只需一步就会引起m种可能的转移,即把一个记录装入m个箱子之一,因此在一般情况下,箱排序和基数排序可能在O(n)时间内完成对n个记录的排序。但是,箱排序和基数排序只适用于像字符串和整数这类有明显结构特征的关键字,而当关键字的取值范围属于某个无穷集合(例如实数型关键字)时,无法使用箱排序和基数排序,这时只有借助于"比较"的方法来排序。虽然桶排序对关键字的结构无要求,但它也只有在关键字是随机分布时才能使平均时间达到线性阶,否则为平方阶。同时要注意,箱、桶、基数这三种分配排序均假定了关键字若为数字时,则其值均是非负的,否则将其映射到箱(桶)号时,又要增加相应的时间。
参考: