针对特殊情况,我们是否还有更好的答案?
归并排序、堆排序、快排使用广泛,但仍不能忽视三种排序算法都不圆满。
因此,我们需要考虑在特定的应用中寻找更好的排序算法,由于使用一种排序算法难以兼顾前面讲到的各个维度的多种需求,今天人们对排序算法的改进大多是结合几种排序算法的思想,形成混合排序算法(Hybrid Sorting Algorithm)。
比如将快速排序和堆排序结合起来的内省排序(Introspective Sort,简称Introsort),它成为如今大多数标准函数库(STL)中的排序函数使用的算法,还有接下来要介绍的蒂姆排序(Timsort),它是今天Java和安卓(Android)操作系统内部使用的排序算法。
Timsort
将插排(节省内存)和归并排序(节省时间)结合,是稳定的排序算法。
时间复杂度:
可以看作以run(块)为单位的归并排序,这些块内部元素是排好序的(不限制排序顺序)。
思想:
-
找出序列中各个递增和递减的子序列。
如果这样的子序列太短,小于一个预先设定的常数(通常是32或者64),则用简单的插入排序将它们整理为有序的子序列(也称块,run)。
在寻找插入位置时,该算法采用了二分查找。随后将这些有序子序列一个一个放入一个临时的存储空间(堆栈)中。
-
按照规则合并这些块,合并的过程是先合并两个最短的,而不是一长一短地合并,可以证明这样效率会高些。
合并的方法从原理上讲和归并排序中两个子序列的合并是相同的,但是为了提高效率,算法中所说的块,其实都可以通过批处理的方式进行归并,而不需要一个个地进行。
例如图1.8所展示的两个子序列在合并时,可以直接将X序列中的前四个数(7,9,13,16)一次加到Y序列的3之后,类似的,可以把Y序列中的三个数(33,36,37)直接归并到X序列的19之后。这样成组归并的数字在图中用[ ]表示。当然,能够成批归并的前提是知道这些组的边界。比如当Y序列中的3首先进入归并后的序列之后,接下来不仅要比较X中的7和Y中的17,而且需要知道在X序列中大于17的数字的位置(第5个,19)。如果一个个顺序扫描,就和传统的归并排序算法没有区别了。蒂姆排序中采用的是一种跳跃式(galloping)预测的方式。
Q1.赛跑问题(GS)
假定有25名短跑选手比赛争夺前三名,赛场上有五条赛道,一次可以有五名选手同时比赛。比赛并不计时,只看相应的名次。假设选手的发挥是稳定的,也就是说如果约翰比张三跑得快,张三比凯利跑得快,约翰一定比凯利跑得快。最少需要几次比赛才能决出前三名?(在第6章给出了这一问题的解答。(难度系数3颗星))
假设25名短跑选手随机形成一个序列,按照速度给选手编号,速度最快的为1,最慢的为25。第一次比赛选择前五位,第二次选择六到十位......以此类推。
即五个小区间会排好序,利用Timsort进行排序。
Q2.区间排序
如果有N个区间[l1,r1],[l2,r2],…,,只要满足下面的条件我们就说这些区间是有序的:存在x**i∈[l**i,r**i],其中i=1,2,…,N。
比如,[1, 4]、[2, 3]和[1.5, 2.5]是有序的,因为我们可以从这三个区间中选择1.1、2.1和2.2三个数。同时[2, 3]、[1, 4]和[1.5, 2.5]也是有序的,因为我们可以选择2.1、2.2和2.4。但是[1, 2]、[2.7, 3.5]和[1.5, 2.5]不是有序的。
对于任意一组区间,如何将它们进行排序?(难度系数3颗星)
解法:若,将第i个区间和第i+1个区间交换位置。
总结
对于同一个问题,可以使用不同的计算机算法。不同算法之间效率的差异可谓天差地别,因此在计算机领域很大一部分工作就是在各种应用中寻找效率更高的算法。当然不同的算法在处理不同规模的问题时所表现的效率可能会有很大的差异,因此在衡量计算机算法的效率时,我们假定要处理的问题规模都非常巨大,近乎无穷。然后,我们需要找到计算量和问题规模N之间的函数关系。在计算机科学中,通常我们感兴趣的不是具体的计算量函数,而是它的上界,这个上界可以采用数学中关于函数上界的概念,也就是大O的概念来描述。
附录
为什么排序算法的复杂度不可能小于O(NlogN)
什么是最小的序列
假定有两个序列和,我们需要对它们的大小进行比较。规则是这样确定的:假如是第一对不同的元素,且,而它们前面的元素都相同,即a1=b1,a2=b2,…,,那么我们就说第一个序列小于第二个序列。
对于任意一个序列,任意对序列排序,最小的序列是将其中每一个元素从小到大排好序的序列。
在所有可能的排列组合中,通过元素的比较挑出最小的一个,就是排序。
比较M个序列需要做多少次元素之间的比较
假定有两个序列,它们除了在第i个和第j个位置上的元素彼此互换,其中i<j,其他元素都相同,即这两个序列可以写为和,如果第一个序列就小于第二个序列;反之,如果,则第二个序列小于第一个序列。也就是说,将两个元素做一次比较,我们最多能区分出两个不同序列的大小。
如果我们进行两次比较,最多能够区分出多少个序列的大小呢?显然最多是四种。类似地,假如我们做k次比较,最多能区分出种不同序列的大小。反过来,如果我们有M种序列,要区分出它们的大小,需要logM次比较。
接下来我们思考一下,N个元素的数组能排出多少种可能的序列呢?显然是N!种。因此要区分出这么多种序列的大小,挑出最小的一个,至少需要logN!次比较,如图1.9所示。
计算logN!需要使用斯特林(Stirling)公式,即lnN!=NlnN−N+O(lnN)。因此我们可以得出logN!=O(NlogN)的结论。注意,我们现在估算出的是排序所需要进行比较的次数的下限。也就是说,任何排序算法的复杂度不会低于O(NlogN)。
区分出N!种不同序列的大小所需要的比较次数,不能少于包含N!个叶节点的二叉树的高度(从根节点到最远的叶节点的节点数)