在(17)中咱们对排序算法进行了简单的分析,并得出了两个结论:算法
1.只进行相邻元素交换的排序算法时间复杂度为O(N2)数组
2.要想时间复杂度低于O(N2),排序算法必须进行远距离的元素交换函数
而今天,咱们将对排序算法进行进一步的分析,这一次的分析将针对“使用比较进行排序”的排序算法,到目前为止咱们所讨论过的全部排序算法都在此范畴内。所谓“使用比较进行排序”,就是指这个算法实现排序靠的就是让元素互相比较,好比插入排序的元素与前一个元素比较,若反序则交换位置,再好比快速排序小于枢纽的元素分为一组,大于枢纽的元素分为另外一组。它们都是依靠“比较”来完成排序工做。spa
要对使用比较进行排序的算法进行分析,咱们首先要引入一个概念:决策树。code
决策树就是这样的二叉树:树的根结点表示“元素的全部可能顺序”,树的每一条边表示“一种可能的结果”,一条边链接的孩子结点则是“父结点通过该边所表明的比较结果后剩余的可能顺序”。这样的解释很难理解,但有图搭配就能够好不少:blog
上图是一棵三元素排序决策树,根结点处表示全部可能的顺序,而从根延伸下来的两条边分别表示了两种“决策结果”,或者说“比较结果”,若符合该“决策结果”就能够得出剩余的可能状况,好比根结点的左孩子是经历决策“a<b”后剩余的可能。显然,叶子表明只剩一种可能顺序。排序
注意,决策树并无表明任何排序算法,即没有哪一个排序算法是这样工做的。可是决策树能够给咱们这样一个信息:经过比较来排序的算法,本质上就是沿着该元素集合的决策树从根到某个叶子的路径比较下去。class
所以,分析这条“路径”平均通过多少条边,就至关于分析使用比较的排序算法平均须要多少次比较。这也是本次分析与(17)的不一样之处,在(17)中咱们的分析针对的是排序算法的“交换”次数,此次咱们分析的是“比较”次数,而比较次数显然更为特殊,由于不论元素是否远距离交换,比较老是存在的。二叉树
要分析使用比较进行排序的算法平均进行几回比较,咱们就必须知晓如下定理。搜索
定理1:深度为d的二叉树,最多拥有2d个叶子
证实很简单:二叉树的深度d即二叉树中深度最大的叶子的深度d,若存在某个叶子深度不是d,则能够在该叶子下添加两个孩子而不改变树的深度,所以深度为d的二叉树要有最多的叶子则必为满二叉树,此时有叶子2d个(深度为d的层最多有2d个结点)
定理2:有Y个叶子的二叉树,深度至少为[logY](底数默认为2)
证实:由定理1能够直接推出。
这个证实可能有点难懂,咱们能够举一反三一下:假如1元钱最多能够买5个糖,那么5个糖最少须要多少钱?答案是1元,刚好是反函数的关系。相似的,深度为x的二叉树最多有y个叶子,那么有y个叶子的二叉树最少有多少深度?答案就是x了。
定理3:N元素排序的决策树有N!个叶子结点
证实:N元素排序的可能顺序共有N!个,而决策树的叶子就是表示“仅剩的可能性”即某一种可能顺序,因此N元素排序的决策树共有N!个叶子
定理4:使用元素比较的排序算法至少须要O(logN!)次比较
证实:由定理2可知,有y个叶子的决策树,深度至少为[logy],而N元素排序决策树叶子数量必为N!,因此N元素排序决策树深度至少为[logN!],也即N元素排序决策树的任一叶子深度至少为[logN!],而叶子的深度就表示了从根到该叶子的路径上通过的边的数量,也就是“比较”的次数,所以定理4成立。
定理5:使用元素比较的排序算法至少须要Ω(N*logN)次比较
证实:根据定理4进行继续计算:
logN!=log(N*(N-1)*(N-2)*……*2*1)
=logN+log(N-1)+log(N-1)+……+log2+log1
>=logN+log(N-1)+……log(N/2)
>=(N/2)*log(N/2)=(N/2)*log(N*1/2)=(N/2)*logN+(N/2)*log(1/2)
>=(N/2)*logN-N/2
=Ω(N*logN)
定理5就是咱们此次分析的最终结果,而且咱们能够将定理5进行一个推广:假设存在X种可能情形,肯定具体情形的方法是不断地问“是或否”型的问题,那么累计须要问的次数至少是[logX]。
那么根据定理5,堆排序、合并排序和快速排序是否已经表明了排序的最快境界呢?不是的,由于定理5依然是有“限定”的,那就是经过比较进行排序的算法才符合,也就是说不是经过比较来完成排序的话,是可能突破这个界限的。
不经过比较来完成排序,是个什么样子?咱们这里能够举一个简单的例子:桶式排序。其时间复杂度是O(N)。
现实生活中桶式排序的思想是很多见的,举个例子感觉一下:
假设咱们有不少硬币,一分、二分、五分、一角、五角和一元都有,如今咱们想要将它们按从小到大排好序,该怎么作?手工模拟任意排序算法均可以完成这项工做,但没有人会这么傻。大部分人的作法都是:准备6个“桶”,分别存放这6种硬币,一分的扔进一分桶,一元的扔进一元桶,全部硬币扔进桶里了,再按顺序从桶里倒出来,排序就完成了。
将上述思想转换到计算机中就是这样:假设咱们的元素都是天然数,且必定小于MAX,那咱们只要准备MAX个空桶,即定义一个整形数组bucket[MAX],并将其所有初始化为0。而后遍历全部元素,若元素为i,则令bucket[i]加1,最后统计数组bucket的状况,就能够得出元素的顺序:
//size为数组src的大小,也即元素个数 void BucketSort(unsigned int *src,unsigned int size) { //MAX为宏,表示src中元素不会大于等于的值 unsigned int bucket[MAX] = { 0 }; //将元素们“扔进桶里” for (unsigned int i = 0;i < size;++i) ++bucket[src[i]]; //将桶里的元素“倒出来” unsigned int j = 0; for (unsigned int i = 0;i < MAX;++i) for (unsigned int x = 0;x < bucket[i];++x) src[j++] = bucket[i]; }
显然,桶式排序的局限性在于要求元素必须是天然数,必须存在上限且上限不可过度大,由于元素的上限决定了桶的数量,而桶的数量并非想要多少有多少,好比个人电脑就不支持分配一个大小为INT_MAX的数组。
桶式排序还有一种变种,只须要10个桶便可,感兴趣的能够去搜索“桶式排序”或“基数排序”,此处不作介绍。
本篇博文就是有关排序的最后一篇博文了,下一篇博文开始,我将会介绍图论算法,并不难,至少理解起来是不难(实现起来就难说了)。