【算法】5 传说中的快排是如何的

什么是高速排序

高速排序简单介绍

高速排序(英文名:Quicksort,有时候也叫做划分交换排序)是一个高效的排序算法,由Tony Hoare在1959年发明(1961年发布)。当情况良好时。它能够比主要竞争对手的归并排序和堆排序快上大约两三倍。这是一个分治算法,并且它就在原地排序。

所谓原地排序,就是指在原来的数据区域内进行重排。就像插入排序一般。

而归并排序就不一样,它须要额外的空间来进行归并排序操作。为了在线性时间与空间内归并,它不能在线性时间内实现就地排序,原地排序对它来说并不足够。而高速排序的优点就在于它是原地的,也就是说,它非常节省内存。

引用一张来自维基百科的能够非常清晰表示高速排序的示意图例如以下:

高速排序的分治思想

由于高速排序採用了分治算法,所以:

一、分解:本质上高速排序把数据划分成几份,所以高速排序通过选取一个重要数据,再依据它的大小。把原数组分成两个子数组:第一个数组里的数都比这个主元数据小或等于,而还有一个数组里的数都比这个主元数据要大或等于。

二、解决:用递归来处理两个子数组的排序。

(也就是说。递归地求上面图示中左半部分,以及递归地求上面图示中右半部分。)

三、合并:由于子数组都是原址排序,所以不须要合并操作,通过上面两步后数组已经排好序了。

所以高速排序的主要思想是递归与划分。

如何划分

当然最重要的是它的复杂度是线性的,也就是Θ(n)个划分的子程序。

Partition(A,p,q)   // A[p,..q]
1   x=A[p]   // pivot=A[p] 主元
2   i=p
3   for j=p+1 to q
4       do if A[j]<=x
5          then i=i+1
6             exch A[i]<->A[j]
7   exch A[p]<->A[i]
8   return i // i pivot 

这就是划分的伪代码,主要的结构就是一个for循环语句,中间加上了一个if条件语句,它实现了对子数组A[p...q]的原址排序。

刚開始时i等于p,j等于p+1。在这个循环中查找i下标的数据。假设它比x大,那就将其存放到“>=x”区域并将j加1后进行下一次循环。而假设它比x小,那就要做些动作来维持循环不变量了。

将i的下标加1后将下标i相应的数据和下标j所相应的数据互换位置。

然后再移动区域的界限并開始下一次循环。

那么这个算法在n个数据下的执行时间大约是O(n)。由于它差点儿把每一个数都比較了一遍。而每一个步骤所需的时间都为O(1)。

上面这幅图具体的描写叙述了Partition过程,每一行后也加了凝视。

将递归的思想作用于划分上

有了上面这些准备工作,再加上分治的思想实现高速排序的伪代码也是非常easy的。

Quicksort(A,p,q)
1   if p<q
2     then r=Partition(A,p,q)
3          Quicksort(A,p,r-1)
4          Quicksort(A,r+1,q) 

为了排序一个数组A的全部元素。初始调用时Quicksort(A,1,A.length)。

高速排序的算法分析

相信通过前面的诸多实践,大家也发现了高速排序的执行时间依赖于Partition过程,也就是依赖于划分是否平衡,而归根结底这还是由于输入的元素决定的。

假设划分是平衡的,那么高速排序算法性能就和归并排序一样。

假设划分是不平衡的。那么高速排序的性能就接近于插入排序。

如何是最坏的划分

1)输入的元素已经排序或逆向排序

2)每一个划分的一边都没有元素

也就是说当划分产生的两个子问题分别包括了n-1个元素和0个元素时。高速排序的最坏情况就发生了。

T(n)=T(0)+T(n?1)+\Theta(n)=Θ(1)+T(n?1)+Θ(n)=Θ(n?1)+Θ(n)=Θ(n2)

这是一个等差级数,就和插入排序一样。它并不比插入排序快,由于当相同是输入元素已经逆向排好序时,插入算法的执行时间为Θ(n)。

但高速排序仍旧是一个优秀的算法,这是由于在平均情况下它已经非常高效。

我们为最坏情况画一个递归树。

这是一课高度不平衡的递归树,图中左边的那些T(0)的执行时间都为Θ(1),而总共同拥有n个。

所以算法的中执行时间为:

T(n)=Θ(n)+Θ(n2)=Θ(n2)

最坏划分的算法分析

通过上面的图示我们知道了在最坏情况下高速排序的复杂度是Θ(n2),但以图示的方式并非一种严谨的证明方式,我们应该使用代入法来证明它。

当输入规模为n时。时间T(n)有例如以下递归式:

T(n)=max0≤r≤n?1(T(r)+T(n?r?1))+Θ(n)

除去主元后,在Partition函数中生成的两个子问题的规模的和为n-1,所以r的规模才是0到n-1。

假设T(n)≤cn2成立。当中c为常数这个大家都知道的。于是上面的递归式为:

T(n)≤max0≤r≤n?1(cr2+c(n?r?1)2)+Θ(n)≤c?max0≤r≤n?1(r2+(n?r?1)2)+Θ(n)

1)而r2+(n?r?1)2对于r的二阶导数为正,所以在区间0≤r≤n?1的右端点取得最大值。

于是有max0≤r≤n?1(r2+(n?r?1)2)≤(n?1)2=n2?2n+1。所以对于T(n)有:

T(n)≤cn2?c(2n?1)+Θ(n)

终于由于我们能够选择一个足够大的c,来使得c(2n?1)大于Θ(n),所以有T(n)=O(n2)。

2)r2+(n?r?1)2对于r的二阶导数为正。所以在区间0≤r≤n?1的左端点取得最小值。

于是有max0≤r≤n?1(r2+(n?r?1)2)≥(n?1)2=n2?2n+1,所以对于T(n)有:

T(n)≥cn2?c(2n?1)+Θ(n)

相同我们也能够选择一个足够小的c,来使得c(2n?1)小于Θ(n),所以有T(n)=Ω(n2)。

综上这两点得到T(n)=Θ(n2)

如何是最好的划分

当Partition将数组分为n/2和n/2两个部分时是最高效的。此时有:

T(n)=2T(n/2)+Θ(n)=Θ(nlgn)

如何是平衡的划分

高速排序的平均执行时间更接近于其最好情况,而非最坏情况。

此处有一个经典的演示样例,将数组按1:9的比例进行划分会如何呢?这样的划分看似非常不平衡,但真的会因此而影响效率么?

当中此时的递归式是:

T(n)=T(110n)+T(910n)+Θ(n)

这里依然通过递归树来观察一番。

由于每次都降低十分之中的一个,须要减多少次才干达到n呢,也恰好也是以10为底对数的定义。所以左側的高度为log10n了,相应的右側的高度为log109n。

全部那些叶子加在一起也仅仅有Θ(n),所以有:

T(n)≤cn?log109n+Θ(n)

事实上T(n)的下界也渐近为nlgn,所以总时间为:

T(n)=Θ(nlgn)

仅仅要划分是常数比例的,算法的执行时间总是O(nlgn)。

随机化高速排序

随机算法的思想

在前面分析高速排序的平均情况性能时,是建立在输入数据的全部排列都是等概率的条件下的。但在实际project中往往不会总出现这样的良好的情况。

【算法】3 由招聘问题看随机算法中我们介绍了随机算法,它使得对于全部的输入都有着较好的期望性能。因此随机化高速排序在有大量数据输入的情况下是一种更好的排序算法。

下面是随机化高速排序的优点:

1)其执行时间不依赖与输入序列的顺序

2)无需对输入序列的分布做不论什么假设

3)没有 一种特别的输入会引起最差的执行情况

4)最差的情况由随机数产生器决定

随机抽样技术

如今我们来使用一种叫做随机抽样(random sampling)的随机化技术,使用该技术就不再始终採用A[p]作为主元,而是从A[p…q]中随机选择一个元素作为主元。

为了达到这一目的,首先将A[p]与从A[p...q]中随机选出的一个元素交换。

通过对序列p...q的随机抽样。我们能够保证主元元素x=A[p]是等概率地从子数组的q?p+1个元素中选取的。

由于主元元素是随机选择的,我们能够期望在平均情况下对输入数组的划分是比較均衡的。所以对前面的两份伪代码做例如以下改动:

RANDOMIZED-PARTITION(A,p,q)
1   i=RANDOM(p,q)
2   exchange A[p] with A[i]
3   return PARTITION(A,p,q)
RANDOMIZED-QUICKSORT(A,p,q)
1   if p<q
2       r=RANDOMIZED-PARTITION(A,p,q)
3       RANDOMIZED-QUICKSORT(A,p,r-1)
4       RANDOMIZED-QUICKSORT(A,r+1,q)

有了随机抽样技术后再也不用操心高速排序遇到最坏划分的情况啦。所以说随机化高速排序的期望执行时间是O(nlgn)。




感谢您的訪问,希望对您有所帮助。 欢迎大家关注、收藏以及评论。



为使本文得到斧正和提问,转载请注明出处:

http://blog.csdn.net/nomasp


时间: 2024-10-11 21:53:25

【算法】5 传说中的快排是如何的的相关文章

算法导论学习之快排+各种排序算法时间复杂度总结

快排是一种最常用的排序算法,因为其平均的时间复杂度是nlgn,并且其中的常数因子比较小. 一.快速排序 快排和合并排序一样都是基于分治的排序算法;快排的分治如下: 分解:对区间A[p,r]进行分解,返回q,使得A[p–q-1]都不大于A[q] A[q+1,r]都大于A[q]; 求解:对上面得到的区间继续递归进行快排 合并:因为快排是原地排序,所以不需要特别的合并 从上可以看出最重要的就是分解函数,其按关键值将数组划分成3部分,其具体实现的过程见代码注释. 我们一般取数组的最后一个元素作为划分比较

普林斯顿公开课 算法3-3:三路快排

很多时候排序是为了对数据进行归类,比如对城市进行排序,对员工的职业进行排序.这种排序的特点就是重复的值特别多. 如果使用普通的快排对这些数据进行排序,会造成N^2复杂度,但是归并排序和三路快排就没有这样的问题. 三路快排 三路快排的基本思想就是,在对数据进行分区的时候分成左中右三个部分,中间都是相同的值,左侧小于中间,右侧大于中间. 性能 三路快排的复杂度比普通快排小,主要取决于数据中重复数据的数量.重复数据越多,三路快排的复杂度就越接近于N. 代码 public class Quick3 {

算法学习——单链表快排

/**  * 以p为轴对start-end间的节点进行快排(包括start && 不包括end):  * 思路:  * 1.将头节点作为轴节点start,从start.next开始遍历,如果节点小于轴start的值,将该节点插入到轴节点后面:  * 2.将轴节点插入合适位置,即找到最后一个小于轴的节点,将该节点与轴节点值互换,此时就链表分为两部分,小于轴节点和大于轴节点:  * 3.递归的遍历2中两部分节点.  *   * @param p  * @param start  * @para

算法总结--排序(快排未写)

算法第四版的简化的笔记!给自己看的 数据,一个长度为n的无序数组 api exch([]a,i,j) 交换数组i与j位置的元素 less(i,j) 判断大小:数组元素i<j?true:false 一 选择排序 selection i=0:从i~n中选取最小值与i交换位置:i++ :循环: 特点 运行时间与输入无关(无论多么乱,或者元素全部一样) 排序时间是一样的. 数据移动是最少的,每个元素只交换一次. public static void sort(Comparable[] a) { int

排序算法之冒泡和快排

冒泡排序: 顾名思义:参与排序的数据就像水中的气泡慢慢浮出水面一样"浮"到数列顶端. 冒泡排序要点: 1.  两层循环,外层循环控制走访数列重复进行的次数,内层循环进行数据的比较.交换,是数据"上浮". 2.  内层循环是相邻的数据进行比较. C语言代码实现: // 冒泡排序 void sort_bubble(int n){ int i,j; for(i=0;i<n-1;i++){ for(j=0;j<n-1-i;j++){ if(arr[j]>a

随机快排算法

1 package Sort; 2 3 import org.junit.Test; 4 5 // 随机快排算法 6 public class RandQuickSort { 7 8 // 交换数组中的两个元素 9 public void exchange(int[] array, int index1, int index2) { 10 int tmp = array[index1]; 11 array[index1] = array[index2]; 12 array[index2] = t

排序算法复习:直接插入排序、堆排序、快排、冒泡排序

冒泡排序,感觉是最简单的排序: 基本思路:每次把数组中最小的一个元素像气泡一样浮动.固定到最顶端: 从前向后遍历数组,每次拿到一个元素,就执行一遍冒泡: 从数组末尾开始,到当前元素截止,从后向前遍历,每次比较数组中相邻的两个元素,如果后者比较小,就把两者互换. 这样经过第一次冒泡,可以把最小的元素『浮』到数组的首位.第二次冒泡会把第二小的元素『浮』到数组的第二位.直到所有的元素都被『浮动』到正确的位置. 代码极其简单: var BubbleSort = function (array) { va

Java实现的各种排序算法(包括冒泡,快排等)

//堆排序 不稳定 import java.util.Arrays; public class HeapSort { public static void main(String[] args) { int[] a={49,38,65,97,76,13,27,49,78,34,12,64}; int arrayLength=a.length; //循环建堆 for(int i=0;i<arrayLength-1;i++){ //建堆 buildMaxHeap(a,arrayLength-1-i)

算法总结——三大排序(快排,计数排序,归并)

快排: 适用条件:方便...只要数字不是很多 复杂度:O(nlogn)  每一层n复杂度,共logn层 原理:利用一个随机数与最后面一个数交换,那么这个随机数就到了最后一位,然后循环,如果前面的数大于最后一个数,那么把这个数放到前面去,经过一次排序之后,前面的数都是大于最后一个的,然后对1到k和k+1到n进行排序,一层一层地下去 模板: #include<cstdio> #include<algorithm> #include<time.h> using namespa