引言
?
?
本文从多个方面讲解了快速排序的知识点,包括快排分而治之的思想,以及他与归并排序注重点的不同,快排的性能,包括最优最差以及平均性能,并以均匀分布为例,证明了快排的平均性能是1.39*(n+1)logn,接下来又从熵的角度说明了一下快排为什么下界只能达到nlogn,为什么堆排比快排慢,而基排又能够逃脱nlogn的界限,因为nlogn只针对基于比较的排序算法
?
?
快排的思想
?
?
快排采取分而治之的思想,将待排序的序列分为两个子序列S分为SL和SR两个子序列,这个思想跟归并排序有点类似,但是快排又与归并排序有一些小小的不同之处,比如快排中子序列之间的独立性更为鲜明,快排中要求max(SL)要严格小于等于min(SR)。另外快排的重点是去找如何将两个子序列分开,而归并排序的重点是在于如何将两个子序列合起来,一个注重于分,一个注重于合,这也是他们之间的不同
?
?
性能分析
?
?
快排的时间复杂度在每次划分都比较平均的情况下达到时间复杂度的下界,nlogn
?
?
T(n)=2T((n-1)/2)+O(n)≈nlogn
?
?
而在每次划分都极不均匀的情况下,时间复杂度到达上界,n^2
?
?
T(n)=T(n-1)+T(0)+O(n)=n^2
?
?
而快排的平均性能呢,我们可以证明快排的平均性能是nlogn。
?
?
证明
?
?
以均匀分布为例
?
?
?
?
两边同时乘以n
?
?
再将n替换为n-1
?
?
两式相减
?
?
nT(n)-(n-1)T(n-1)=n*2+2T(n-1)
?
?
nT(n)=n*2+(n+1)T(n-1)
?
?
两边同时除以n(n+1)
T(n)/n+1=2/n+1+T(n-1)/n
?
?
记左边为S(n),那么S(n)=2/(n+1)+2/n+……+2/2+T(0)/1
?
?
右边是一个调和级数,调和级数与lnn同阶
?
?
所以S(n)=(2lnn)=2*ln2*logn=1.39logn
?
?
所以T(n)=1.39(n+1)logn
?
?
为什么快排的下界是nlogn
?
?
排序的本质可以这样来表述:有一组未排序的N个数字,它们一共有N!种排列方式,而其中只有一种排列是满足题意的(譬如从大到小排列)。
?
?
我们考虑N个数字中的任意两个数字ab,假设我们在比较a和b的时候,a<b和a>b的概率是均等的,那么我们我们在确定了a和b的大小关系之后,就可以将排列方式减少为N!/2种(因为a<b和a>b的概率是均等的)。一个直接的推论是,如果每次都像上面这样的完美比较,那么N个元素的N!种可能排列只需要log_2{N!}就排查玩了,而log_2{N!}近似于NlogN。这正是快排的复杂度。
?
?
如果想进一步深入了解可以参看刘未鹏老师的《快排为什么那样快》,以下部分摘自这篇文章
?
?
为什么堆排比快排慢
?
?
回顾一下堆排的过程:
?
?
1. 建立最大堆(堆顶的元素大于其两个儿子,两个儿子又分别大于它们各自下属的两个儿子… 以此类推)
?
?
2. 将堆顶的元素和最后一个元素对调(相当于将堆顶元素(最大值)拿走,然后将堆底的那个元素补上它的空缺),然后让那最后一个元素从顶上往下滑到恰当的位置(重新使堆最大化)。
?
?
3. 重复第2步。
?
?
这里的关键问题就在于第2步,堆底的元素肯定很小,将它拿到堆顶和原本属于最大元素的两个子节点比较,它比它们大的可能性是微乎其微的。实际上它肯定小于其中的一个儿子。而大于另一个儿子的可能性非常小。于是,这一次比较的结果就是概率不均等的,根据前面的分析,概率不均等的比较是不明智的,因为它并不能保证在糟糕情况下也能将问题的可能性削减到原本的1/2。
?
?
在堆排里面有大量这种近乎无效的比较,因为被拿到堆顶的那个元素几乎肯定是很小的,而靠近堆顶的元素又几乎肯定是很大的,将一个很小的数和一个很大的数比较,结果几乎肯定是"小于"的,这就意味着问题的可能性只被排除掉了很小一部分。
?
?
这就是为什么堆排比较慢(堆排虽然和快排一样复杂度都是O(NlogN)但堆排复杂度的常系数更大)。
?
?
MacKay也提供了一个修改版的堆排:每次不是将堆底的元素拿到上面去,而是直接比较堆顶(最大)元素的两个儿子,即选出次大的元素。由于这两个儿子之间的大小关系是很不确定的,两者都很大,说不好哪个更大哪个更小,所以这次比较的两个结果就是概率均等的了。具体参考这里。
?
?
为什么快排其实也不是那么快
?
?
我们考虑快排的过程:随机选择一个元素做"轴元素",将所有大于轴元素的移到左边,其余移到右边。根据这个过程,快排的第一次比较就是将一个元素和轴元素比较,这个时候显而易见的是,"大于"和"小于"的可能性各占一半。这是一次漂亮的比较。
?
?
然而,快排的第二次比较就不那么高明了:我们不妨令轴元素为pivot,第一次比较结果是a1<pivot,那么可以证明第二次比较a2也小于pivot的可能性是2/3!这容易证明:如果a2>pivot的话,那么a1,a2,pivot这三个元素之间的关系就完全确定了——a1<pivot<a2,剩下来的元素排列的可能性我们不妨记为P(不需要具体算出来)。而如果a2<pivot呢?那么a1和a2的关系就仍然是不确定的,也就是说,这个分支里面含有两种情况:a1<a2<pivot,以及a2<a1<pivot。对于其中任一种情况,剩下的元素排列的可能性都是P,于是这个分支里面剩下的排列可能性就是2P。所以当a2<pivot的时候,还剩下2/3的可能性需要排查。
?
?
再进一步,如果第二步比较果真发现a2<pivot的话,第三步比较就更不妙了,模仿上面的推理,a3<pivot的概率将会是3/4!
?
?
这就是快排也不那么快的原因,因为它也没有做到每次比较都能将剩下的可能性砍掉一半。
?
?
基排为什么又那么快呢?
?
?
传统的解释是:基排不是基于比较的,所以不具有后者的局限性。话是没错,但其实还可以将它和基于比较的排序做一个类比。
?
?
基排的过程也许是源于我们理顺一副牌的过程:如果你有N(N<=13)张牌,乱序,如何理顺呢?我们假象桌上有十三个位置,然后我们将手里的牌一张一张放出去,如果是3,就放在位置3上,如果是J,就放在位置11上,放完了之后从位置1到位置13收集所有的牌(没有牌的位置上不收集任何牌)。
?
?
我们可以这样来理解基排高效的本质原因:假设前i张牌都已经放到了它们对应的位置上,第i+1张牌放出去的时候,实际上就相当于"一下子"就确立了它和前i张牌的大小关系,用O(1)的操作就将这张牌正确地插入到了前i张牌中的正确位置上,这个效果就相当于插入排序的第i轮原本需要比较O(i)次的,现在只需要O(1)了。
?
?
但是,为什么基排能够达到这个效果呢?上面只是解释了过程,解释了过程不代表解释了本质。
?
?
当i张牌放到位之后,放置第i+1张牌的时候有多少种可能性?大约i+1种,因为前i张牌将13个位置分割成了i+1个区间——第i+1张牌可以落在任意一个区间。所以放置第i+1张牌就好比是询问这样一个问题:"这张牌落在哪个区间呢?"而这个问题的答案有i+1种可能性?所以它就将剩下来的可能性均分成了i+1份(换句话说,砍掉了i/i+1的可能性!)。再看看基于比较的排序吧:由于每次比较只有两种结果,所以最多只能将剩下的可能性砍掉一半。
?
?
这就是为什么基排要快得多。而所有基于比较的排序都逃脱不了NlogN的宿命。
?
?
?
?
快排实例
?
?
?
?
?
?