版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
1、排序算法比较研究人员:高二(12)班 孙亦超、朱俊杰;指导老师:徐 伟研究内容:比较排序算法的空间、时间复杂度;研究目的:有效提高程序运行的速度;研究意义:通过深入探究,分析其空间和时间复杂度,从而有效提高程序设计者的算法设计;研究日期:2004年8月2005年5月预期成果:flash演示课件、word研究报告、书面报告;排序Sorting排序问题的输入是一个线性表,该线性表的元素属于一个偏序集;要求对该线性表的元素做某种重排,使得线性表中除表尾外的每个元素都小于等于(或大于等于)它的后继。设R为非空集合A上的二元关系,如果R满足自反性(对于每一个xA,(x,x)R ),反对称性(x,y)R(
2、y,x)Rx=y )和传递性(x,y)R(y,x)R(x,z)R),则称R为A上的偏序关系,记作。如果(x,y)R,则记作xy,读作“x小于等于y”。存在偏序关系的集合A称为偏序集。注意,这里的不是指数的大小,而是指在偏序关系中的顺序性。xy的含义是:按照这个序,x排在y前面。根据不同的偏序定义,有不同的解释。例如整除关系是偏序关系,36的含义是3整除6。大于或等于关系也是偏序关系,针对这个关系54是指在大于或等于关系中5排在4的前面,也就是说5比4大。在实际应用中,经常遇到的偏序关系是定义在一个记录类型的数据集合上的。在该记录类型中有一个主键域key,key域的类型是某一个偏序集,记录的其他
3、域称为卫星数据。比较线性表中的两个元素Li和Lj的大小,实际上是比较Li.key和Lj.key的大小(这种比较当然也是偏序集中的比较)。举例而言,某公司的数据库里记 录了员工的数据,每一项纪录包括姓名,编号,年龄,工资等几个域,如果以编号为key域对员工记录排序,则是将员工记录按照编号排序;如果以工资为key域对员工记录排序,则是将员工记录按照工资高低排序;如果以姓名为key域对员工记录排序,则是以员工姓名的汉语拼音按照字典顺序排序。关于偏序集的具体概念和应用,请参见离散数学的相关资料。如果一个排序算法利用输入的线性表在原地重排其中元素,而没有额外的内存开销,这种排序算法叫做原地置换排序算法(
4、in place sort);如果排序后并不改变表中相同的元素原来的相对位置,那么这种排序算法叫做稳定排序算法(stable sort)。排序问题一般分为内排序( internal sorting )和外排序( external sorting )两类:1. 内排序:待排序的表中记录个数较少,整个排序过程中所有的记录都可以保留在内存中; 2. 外排序:待排序的记录个数足够多,以至于他们必须存储在磁带、磁盘上组成外部文件,排序过程中需要多次访问外存。排序问题的计算复杂性对排序算法计算时间的分析可以遵循若干种不同的准则,通常以排序过程所需要的算法步数作为度量,有时也以排序过程中所作的键比较次数作为
5、度量。特别是当作一次键比较需要较长时间,例如,当键是较长的字符串时,常以键比较次数作为排序算法计算时间复杂性的度量。当排序时需要移动记录,且记录都很大时,还应该考虑记录的移动次数。究竟采用哪种度量方法比较合适要根据具体情况而定。在下面的讨论中我们主要考虑用比较的次数作为复杂性的度量。为了对有n个元素的线性表进行排序,至少必须扫描线性表一遍以获取这n个元素的信息,因此排序问题的计算复杂性下界为(n)。如果我们对输入的数据不做任何要求,我们所能获得的唯一信息就是各个元素的具体的值,我们仅能通过比较来确定输入序列<a1,a2,.,an>的元素间次序。即给定两个元素ai和aj,通过测试ai
6、<aj ,aiaj ,ai=aj ,aiaj ,ai>aj 中的哪一个成立来确定ai和aj间的相对次序。这样的排序算法称为比较排序算法。下面我们讨论一下比较排序算法在最坏情况下至少需要多少次比较,即比较排序算法的最坏情况复杂性下界。我们假设每次比较只测试aiaj ,如果aiaj 成立则ai排在aj 前面,否则ai排在aj 后面。任何一个比较排序算法可以描述为一串比较序列: (ai,aj),(ak,al),.,(am,an),.表示我们首先比较(ai,aj),然后比较(ak,al),.,比较(am,an),.,直到我们获取了足够的信息可以确定所有元素的顺序。显而易见,如果我
7、们对所有的元素两两进行一次比较的话(总共比较了Cn2次),就一定可以确定所有元素的顺序。但是,如果我们运气足够好的话,我们可能不必对所有元素两两进行一次比较。比如说对于有三个元素a1,a2,a3的线性表进行排序,如果我们先比较a1和a2,得到a1a2;然后比较a2和a3,得到a2a3;则不必比较a1和a3,因为根据偏序集的传递性,必有a1a3;但是如果a2a3,我们还必须比较a1和a3才能确定a1和a3的相对位置。如果我们适当的安排比较的次序的话,也可以减少比较的次数。这样我们可以用一棵二叉树表示比较的顺序,如下图所示:该树的每一个非叶节点表示一次比较,每一根树枝表示一种比较结果,每一个叶节点
8、表示一种排列顺序。这样的一棵二叉树叫做决策树,它用树枝表示了每次决策做出的选择。如此我们可以将任何一个比较排序算法用一棵决策树来表示。请注意上图只表明了对三个元素的一种比较算法,这种比较算法依次比较(a1,a2)(a2,a3)(a1,a3),一旦中间某步得到足够的信息就可以停止比较,但是当算法执行完后(三次比较后),一定可以确定三个元素间的次序。因此我们有理由将算法在最坏情况下的比较次数作为算法复杂性的度量,对于本例该算法在最坏情况下要进行C32=3次比较。显然,一棵决策树中最高叶节点的高度就是该决策树对应的算法在最坏情况下所需的比较次数,而决策树中最低叶节点的高度就是该决策树对应的算法在最好
9、情况下所需的比较次数。我们的问题就变为:对于任意一棵决策树(任意一种比较排序算法),它的最高的树叶的高度是多少?这个高度就对应于比较排序算法所需的最多比较次数(在运气最坏的情况下);换句话说,对于任何一个输入,该算法至少需要比较多少次就可以对元素进行排序。我们发现,决策树的每个叶节点对应一个n个元素的排列,其中可能有重复的;但是由于决策树表明了所有可能遇到的情况,因而n个元素的所有排列都在决策树中出现过。n个元素共有n!种排列,即决策树的叶节点数目至少为n!。又因为一棵高度为h的二叉树(指二叉树的最高树叶高度为h)的叶节点数目最多为2h个(这时正好是满二叉树,即每个非叶节点都有两个子节点),因
10、此n!2h,得到hlog(n!),其中log以2为底。根据Stirling公式有n!>(n/e)n,于是h>nlogn-nloge,即h=(nlogn)。这样我们就证明了对于任意一种利用比较来确定元素间相对位置的排序算法,其最坏情况下复杂性为(nlogn)。在下文中我们将讨论几种比较排序算法,其中快速排序在平均情况下复杂性为O(nlogn),最坏情况下复杂性为O(n2);堆排序和合并排序在最坏情况下复杂性为O(nlogn),因此堆排序和合并排序是渐进最优的比较排序算法。排序算法是否还能够改进呢?从前文我们知道,如果要改进排序算法的效率,就不能只利用比较来确定元素间相对位置。因此我们
11、还需要知道元素的其他附加信息,光知道元素的大小信息是不够的。下文中我们介绍的计数排序,基数排序和桶排序是具有线性时间复杂性的排序算法,这些算法无一例外地对输入数据作了某些附加限制,从而增加已知的信息,因此可以不通过比较来确定元素间的相对位置。比较排序算法通过比较来确定输入序列<a1,a2,.,an>的元素间相对次序的排序算法称为比较排序算法。在下面讨论的排序算法中,冒泡排序、选择排序和插入排序的比较次数为O(n2),快速排序在平均情况下复杂性为O(nlogn),堆排序和合并排序在最坏情况下复杂性为O(nlogn)。可见,合并排序和堆排序是比较排序算法中时间复杂度最优算法。o 冒泡排
12、序 o 选择排序 o 插入排序 o 快速排序 o 归并排序 o Shell排序 o 堆排序 冒泡排序 Bubble Sort最简单的排序方法是冒泡排序方法。这种方法的基本思想是,将待排序的元素看作是竖着排列的“气泡”,较小的元素比较轻,从而要往上浮。在冒泡排序算法中我们要对这个“气泡”序列处理若干遍。所谓一遍处理,就是自底向上检查一遍这个序列,并时刻注意两个相邻的元素的顺序是否正确。如果发现两个相邻元素的顺序不对,即“轻”的元素在下面,就交换它们的位置。显然,处理一遍之后,“最轻”的元素就浮到了最高位置;处理二遍之后,“次轻”的元素就浮到了次高位置。在作第二遍处理时,由于最高位置上的元素已是“
13、最轻”元素,所以不必检查。一般地,第i遍处理时,不必检查第i高位置以上的元素,因为经过前面i-1遍的处理,它们已正确地排好序。这个算法可实现如下。procedure Bubble_Sort(var L:List);vari,j:position;begin1 for i:=First(L) to Last(L)-1 do2 for j:=First(L) to Last(L)-i do3 if Lj>Lj+1 then 4 swap(Lj,Lj+1); /交换Lj和Lj+1end;上述算法将较大的元素看作较重的气泡,每次最大的元素沉到表尾。其中First(L)和Last(L)分别表示线性
14、表L的第一个元素和最后一个元素的位置,swap(x,y)交换变量x,y的值。上述算法简单地将线性表的位置当作整数用for循环来处理,但实际上线性表可能用链表实现;而且上述算法将线性表元素的值当作其键值进行处理。不过这些并不影响表达该算法的基本思想。今后如果不加说明,所有的算法都用这种简化方式表达。容易看出该算法总共进行了n(n-1)/2次比较。如果swap过程消耗的时间不多的话,主要时间消耗在比较上,因而时间复杂性为O(n2)。但是如果元素类型是一个很大的纪录,则Swap过程要消耗大量的时间,因此有必要分析swap执行的次数。显然算法Bubble_Sort在最坏情况下调用n(n-1)/2次Sw
15、ap过程。我们假设输入序列的分布是等可能的。考虑互逆的两个输入序列L1=k1,k2,.,kn和L2=kn,kn-1,.,k1。我们知道,如果ki>kj,且ki在表中排在kj前面,则在冒泡法排序时必定要将kj换到ki前面,即kj向前浮的过程中一定要穿过一次ki,这个过程要调用一次Swap。对于任意的两个元素ki和kj,不妨设ki>kj,或者在L1中ki排在kj前面,或者L2在中ki排在kj前面,两者必居其一。因此对于任意的两个元素ki和kj,在对L1和L2排序时,总共需要将这两个元素对调一次。n个元素中任取两个元素有Cn2 种取法,因此对于两个互逆序列进行排序,总共要调用Cn2 =n
16、(n-1)/2次Swap,平均每个序列要调用n(n-1)/4次Swap。那么算法Bubble_Sort调用Swap的平均次数为n(n-1)/4。可以对冒泡算法作一些改进,如果算法第二行的某次内循环没有进行元素交换,则说明排序工作已经完成,可以退出外循环。可以用一个布尔变量来记录内循环是否进行了记录交换,如果没有则终止外循环。冒泡法的另一个改进版本是双向扫描冒泡法(Bi-Directional Bubble Sort)。设被排序的表中各元素键值序列为:483 67 888 50 255 406 134 592 657 745 683对该序列进行3次扫描后会发现,第3此扫描中最后一次交换的一对纪录
17、是L4和L5:50 67 255 134 | 406 483 592 657 683 745 888显然,第3次扫描(i=3)结束后L5以后的序列都已经排好序了,所以下一次扫描不必到达Last(L)-i=11-4=7,即第2行的for 循环j不必到达7,只要到达4-1=3就可以了。按照这种思路,可以来回地进行扫描,即先从头扫到尾,再从尾扫到头。这样就得到双向冒泡排序算法:procedure Bi-Directional_Bubble_Sort(var L:List);varlow,up,t,i:position;begin1 low:=First(L);up:=Last(L);2 while
18、up>low do begin3 t:=low;4 for i:=low to up-1 do5 if Li>Li+1 then begin6 swap(Li,Li+1);7 t:=i; end;8 up:=t;9 for i:=up downto low+1 do10 if Li< Li-1 then begin11 swap(Li,Li-1);12 t:=i; end;13 low:=t; end; end;算法利用两个变量low和up记录排序的区域Llow.up,用变量t 记录最近一次交换纪录的位置,4-7行从前向后扫描,9-12行从后向前扫描,每次扫描以后利用t所记录
19、的最后一次交换记录的位置,并不断地缩小需要排序的区间,直到该区间只剩下一个元素。直观上来看,双向冒泡法先让重的气泡沉到底下,然后让轻的气泡浮上来,然后再让较大气泡沉下去,让较轻气泡浮上来,依次反复,直到排序结束。双向冒泡排序法的性能分析比较复杂,目前暂缺,那位朋友知道请告诉我。冒泡排序法和双向冒泡排序法是原地置换排序法,也是稳定排序法,如果算法Bubble_Sort中第3行的比较条件Lj>Lj+1改为Lj>= Lj+1,则不再是稳定排序法。选择排序 Selection Sort选择排序的基本思想是对待排序的记录序列进行n-1遍的处理,第i遍处理是将Li.n中最小者与Li交换位置。这
20、样,经过i遍处理之后,前i个记录的位置已经是正确的了。选择排序算法可实现如下。procedure Selection_Sort(var L:List);vari,j,s:position;begin1 for i:=First(L) to Last(L)-1 do begin2 s:=i;3 for j:=i+1 to Last(L) do4 if Lj< Ls then 5 s:=j; /记录Li.n中最小元素的位置6 swap(Li,Ls); /交换Li,Lsend; end;算法Selection_Sort中里面的一个for循环需要进行n-i次比较,所以整个算法需要次比较。显而易见
21、,算法Selection_Sort中共调用了n-1次swap过程。选择排序法是一个原地置换排序法,也是稳定排序法。插入排序 Insertion Sort插入排序的基本思想是,经过i-1遍处理后,L1.i-1己排好序。第i遍处理仅将Li插入L1.i-1的适当位置,使得L1.i又是排好序的序列。要达到这个目的,我们可以用顺序比较的方法。首先比较Li和Li-1,如果Li-1 Li,则L1.i已排好序,第i遍处理就结束了;否则交换Li与Li-1的位置,继续比较Li-1和Li-2,直到找到某一个位置j(1ji-1),使得Lj Lj+1时为止。图1演示了对4个元素进行插入排序的过程,共需要(a),(b),
22、(c)三次插入。图1 对4个元素进行插入排序在下面的插入排序算法中,为了写程序方便我们可以引入一个哨兵元素L0,它小于L1.n中任一记录。所以,我们设元素的类型ElementType中有一个常量-,它比可能出现的任何记录都小。如果常量-不好事先确定,就必须在决定Li是否向前移动之前检查当前位置是否为1,若当前位置已经为1时就应结束第i遍的处理。另一个办法是在第i遍处理开始时,就将Li放入L0中,这样也可以保证在适当的时候结束第i遍处理。下面的算法中将对当前位置进行判断。插入排序算法如下:procedure Selection_Sort(var L:List);vari,j:position;v
23、:ElementType;begin1 for i:=First(L)+1 to Last(L) do begin2 v:=Li;3 j:=i;4 while (j<>First(L)and(Lj-1< v) do /循环找到插入点 begin5 Lj:=Lj-1; /移动元素6 j:=j-1; end;7 Lj:=v; /插入元素 end;end;下面考虑算法Insertion_Sort的复杂性。对于确定的i,内while循环的次数为O(i),所以整个循环体内执行了O(i)=O(i),其中i从2到n。即比较次数为O(n2)。如果输入序列是从大到小排列的,那么内while循环
24、次数为i-1次,所以整个循环体执行了(i-1)=n(n-1)/2次。由此可知,最坏情况下,Insertion_Sort要比较(n2)次。如果元素类型是一个很大的纪录,则算法第5行要消耗大量的时间,因此有必要分析移动元素的次数。经过分析可知,平均情况下第5行要执行n(n-1)/4次,分析方法与冒泡排序的分析相同。如果移动元素要消耗大量的时间,则可以用链表来实现线性表,这样Insertion_Sort可以改写如下(当然前一个算法同样也适用于链表,只不过没下面这个好,但是下面算法这个比较复杂):注意:在下面的算法中链表L增加了一个哨兵单元,其中的元素为-,即线性表L的第一个元素是L.nextproc
25、edure Selection_Sort_II(var L:PList);vari,j,tmp:Position;begin1 if L.next=nil then exit; /如果链表L为空则直接退出2 i:=L.next; /i指向L的第一个元素,注意,L有一个哨兵元素,因此L.next才是L的第一个元素3 while i.next<>nil do begin4 tmp:=i.next; /tmp指向Li的下一个位置5 j:=L;6 while (j<>i)and(tmp.data>=j.next.data) do /从前向后找到tmp的位置,tmp应该插在
26、j后面7 j:=j.next;8 if j<>i then /j=i说明不需要改变tmp的位置 begin9 i.next:=tmp.next; /将tmp从i后面摘除10 tmp.next:=j.next; /在j后面插入tmp11 j.next:=tmp; end12 else i:=i.next; /否则i指向下一个元素 end;end;上述改进算法主要是利用链表删除和插入元素方便的特性,对于数组则不适用。插入排序法是一个原地置换排序法,也是一个稳定排序法。插入法虽然在最坏情况下复杂性为(n2),但是对于小规模输入来说,插入排序法是一个快速的原地置换排序法。许多复杂的排序法,
27、在规模较小的情况下,都使用插入排序法来进行排序,比如快速排序和桶排序。堆排序: procedure sift(i,m:integer);调整以i为根的子树成为堆,m为结点总数 var k:integer; begin a0:=ai; k:=2*i;在完全二叉树中结点i的左孩子为2*i,右孩子为2*i+1
28、; while k< =m do begin if (k< m) and (ak< ak+1) then inc(k);找出ak与ak+1中较大值 if a0< ak then begin ai:=ak;i:=k;k:=2*i; end
29、0; else k:=m+1; end; ai:=a0; 将根放在合适的位置 end; procedure heapsort; var j:
30、integer; begin for j:=n div 2 downto 1 do sift(j,n); for j:=n downto 2 do begin swap(a1,aj); si
31、ft(1,j-1); end; end; 归并排序 a为序列表,tmp为辅助数组 procedure merge(var a:listtype; p,q,r:integer); 将已排序好的子序列ap.q与aq+1.r合并
32、为有序的tmpp.r var I,j,t:integer; tmp:listtype; begin t:=p;i:=p;j:=q+1;t为tmp指针,I,j分别为左右子序列的指针 while (t< =r) do begin
33、60; if (i< =q)左序列有剩余 and (j >r) or (ai< =aj) 满足取左边序列当前元素的要求 then begin tmpt:=ai; inc(i); end else begin&
34、#160; tmpt:=aj;inc(j); end; inc(t); end; for i:=p to r do ai:=tmpi; end;merge
35、 procedure merge_sort(var a:listtype; p,r: integer); 合并排序ap.r var q:integer; begin if p< >r then begin
36、160;q:=(p+r-1) div 2; merge_sort (a,p,q); merge_sort (a,q+1,r); merge (a,p,q,r); end; end; main
37、 begin merge_sort(a,1,n); end.快速排序 Quick Sort我们已经知道,在决策树计算模型下,任何一个基于比较来确定两个元素相对位置的排序算法需要(nlogn)计算时间。如果我们能设计一个需要O(n1ogn)时间的排序算法,则在渐近的意义上,这个排序算法就是最优的。许多排序算法都是追求这个目标。下面介绍快速排序算法,它在平均情况下需要O算法的基本思想快速排序的基本思想是基于分治策略的。对于输入的子序列Lp
38、.r,如果规模足够小则直接进行排序,否则分三步处理:§ 分解(Divide):将输入的序列Lp.r划分成两个非空子序列Lp.q和Lq+1.r,使Lp.q中任一元素的值不大于Lq+1.r中任一元素的值。 § 递归求解(Conquer):通过递归调用快速排序算法分别对Lp.q和Lq+1.r进行排序。 § 合并(Merge):由于对分解出的两个子序列的排序是就地进行的,所以在Lp.q和Lq+1.r都排好序后不需要执行任何计算Lp.r就已排好序。 这个解决流程是符合分治法的基本步骤的。因此,快速排序法是分治法的经典应用实例之一。算法的实现算法Quick_Sort的实现:注
39、意:下面的记号Lp.r代表线性表L从位置p到位置r的元素的集合,但是L并不一定要用数组来实现,可以是用任何一种实现方法(比如说链表),这里Lp.r只是一种记号。procedure Quick_Sort(p,r:position;var L:List);conste=12;varq:position;begin1 if r-p<=e then Insertion_Sort(L,p,r)/若Lp.r足够小则直接对Lp.r进行插入排序 else begin2 q:=partition(p,r,L);/将Lp.r分解为Lp.q和Lq+1.r两部分3 Quick_Sort(p,q,L); /递归排
40、序Lp.q4 Quick_Sort(q+1,r,L);/递归排序Lq+1.r end;end;对线性表L1.n进行排序,只要调用Quick_Sort(1,n,L)就可以了。算法首先判断Lp.r是否足够小,若足够小则直接对Lp.r进行排序,Sort可以是任何一种简单的排序法,一般用插入排序。这是因为,对于较小的表,快速排序中划分和递归的开销使得该算法的效率还不如其它的直接排序法好。至于规模多小才算足够小,并没有一定的标准,因为这跟生成的代码和执行代码的计算机有关,可以采取试验的方法确定这个规模阈值。经验表明,在大多数计算机上,取这个阈值为12较好,也就是说,当r-p<=e=12即Lp.r的
41、规模不大于12时,直接采用插入排序法对Lp.r进行排序(参见 Sorting and Searching Algorithms: A Cookbook)。当然,比较方便的方法是取该阈值为1,当待排序的表只有一个元素时,根本不用排序(其实还剩两个元素时就已经在Partition函数中排好序了),只要把第1行的if语句该为if p=r then exit else .。这就是通常教科书上看到的快速排序的形式。注意:算法Quick_Sort中变量q的值一定不能等于r,否则该过程会无限递归下去,永远不能结束。因此下文中在partition函数里加了限制条件,避免q=r情况的出现。算法Quick_Sor
42、t中调用了一个函数partition,该函数主要实现以下两个功能:1. 在Lp.r中选择一个支点元素pivot; 2. 对Lp.r中的元素进行整理,使得Lp.q分为两部分Lp.q和Lq+1.r,并且Lp.q中的每一个元素的值不大于pivot,Lq+1.r中的每一个元素的值不小于pivot,但是Lp.q和Lq+1.r中的元素并不要求排好序。 快速排序法改进性能的关键就在于上述的第二个功能,因为该功能并不要求Lp.q和Lq+1.r中的元素排好序。函数partition可以实现如下。以下的实现方法是原地置换的,当然也有不是原地置换的方法,实现起来较为简单,这里就不介绍了。function parti
43、tion(p,r:position;var L:List):position;varpivot:ElementType;i,j:position;begin1 pivot:=Select_Pivot(p,r,L); /在Lp.r中选择一个支点元素pivot2 i:=p-1;3 j:=r+1;4 while true do begin5 repeat j:=j-1 until Lj<=pivot; /移动左指针,注意这里不能用while循环6 repeat i:=i+1 until Li>=pivot; /移动右指针,注意这里不能用while循环7 if i< j then s
44、wap(Li,Lj) /交换Li和Lj8 else if j<>r then return j /返回j的值作为分割点9 else return j-1; /返回j前一个位置作为分割点 end;end;该算法的实现很精巧。其中,有一些细节需要注意。例如,算法中的位置i和j不会超出Ap.r的位置界,并且该算法的循环不会出现死循环,如果将两个repeat语句换为while则要注意当Li=Lj=pivot且i<j时i和j的值都不再变化,会出现死循环。另外,最后一个if.then.语句很重要,因为如果pivot取的不好,使得Partition结束时j正好等于r,则如前所述,算法Qui
45、ck_Sort会无限递归下去;因此必须判断j是否等于r,若j=r则返回j的前驱。以上算法的一个执行实例如图1所示,其中pivot=Lp=5:图1 Partition过程的一个执行实例Partition对Lp.r进行划分时,以pivot作为划分的基准,然后分别从左、右两端开始,扩展两个区域Lp.i和Lj.r,使得Lp.i中元素的值小于或等于pivot,而Lj.r中元素的值大于或等于pivot。初始时i=p-1,且j=i+1,从而这两个区域是空的。在while循环体中,位置j逐渐减小,i逐渐增大,直到LipivotLj。如果这两个不等式是严格的,则Li不会是左边区域的元素,而Lj不会是右边区域的元
46、素。此时若i在j之前,就应该交换Li与Lj的位置,扩展左右两个区域。 while循环重复至i不再j之前时结束。这时Lp.r己被划分成Lp.q和Lq+1.r,且满足Lp.q中元素的值不大于Lq+1.r中元素的值。在过程Partition结束时返回划分点q。寻找支点元素select_pivot有多种实现方法,不同的实现方法会导致快速排序的不同性能。根据分治法平衡子问题的思想,我们希望支点元素可以使Lp.r尽量平均地分为两部分,但实际上这是很难做到的。下面我们给出几种寻找pivot的方法。1. 选择Lp.r的第一个元素Lp的值作为pivot; 2. 选择Lp.r的最后一个元素Lr的值作为pivot;
47、 3. 选择Lp.r中间位置的元素Lm的值作为pivot; 4. 选择Lp.r的某一个随机位置上的值Lrandom(r-p)+p的值作为pivot; 按照第4种方法随机选择pivot的快速排序法又称为随机化版本的快速排序法,在下面的复杂性分析中我们将看到该方法具有平均情况下最好的性能,在实际应用中该方法的性能也是最好的。下面是一个快速排序的Java Applet演示程序,该程序使用第一种pivot选择法,即选Lp为pivot,因此Partition过程作了一些简化,与我们这里的Partition过程实现方法不同,但功能相同。该程序是针对用数组实现的线性表,用C语言实现的。 性能分析下面我们就最
48、好情况,最坏情况和平均情况对快速排序算法的性能作一点分析。注意:这里为方便起见,我们假设算法Quick_Sort的范围阈值为1(即一直将线性表分解到只剩一个元素),这对该算法复杂性的分析没有本质的影响。我们先分析函数partition的性能,该函数对于确定的输入复杂性是确定的。观察该函数,我们发现,对于有n个元素的确定输入Lp.r,该函数运行时间显然为(n)。最坏情况无论适用哪一种方法来选择pivot,由于我们不知道各个元素间的相对大小关系(若知道就已经排好序了),所以我们无法确定pivot的选择对划分造成的影响。因此对各种pivot选择法而言,最坏情况和最好情况都是相同的。我们从直觉上可以判
49、断出最坏情况发生在每次划分过程产生的两个区间分别包含n-1个元素和1个元素的时候(设输入的表有n个元素)。下面我们暂时认为该猜测正确,在后文我们再详细证明该猜测。对于有n个元素的表Lp.r,由于函数Partition的计算时间为(n),所以快速排序在序坏情况下的复杂性有递归式如下:T(1)=(1), T(n)=T(n-1)+T(1)+(n) (1)用迭代法可以解出上式的解为T(n)=(n2)。这个最坏情况运行时间与插入排序是一样的。下面我们来证明这种每
50、次划分过程产生的两个区间分别包含n-1个元素和1个元素的情况就是最坏情况。设T(n)是过程Quick_Sort作用于规模为n的输入上的最坏情况的时间,则T(n)=max(T(q)+T(n-q)+(n) ,其中1qn-1 (2)我们假设对于任何k<n,总有T(k)ck2 ,其中c为常数;显然当k=1时是成立的。将归纳假设代入(2),得到:T(n)max(cq2+c(n-q)2)+(n)=c*max(q2+(n-q)2)+(n)因为在1,n-1上q2+(n-q)2关于q递减,所以当q=1时q2+(n-q)2有最大值n2-2(n-1)。于是有:T(n)cn2
51、-2c(n-1)+(n)cn2只要c足够大,上面的第二个小于等于号就可以成立。于是对于所有的n都有T(n)cn2 。这样,排序算法的最坏情况运行时间为(n2),且最坏情况发生在每次划分过程产生的两个区间分别包含n-1个元素和1个元素的时候。最好情况如果每次划分过程产生的区间大小都为n/2,则快速排序法运行就快得多了。这时有:T(n)=2T(n/2)+(n), T(1)=(1) (3)解得: T(n)=(nlogn)快速排序法最佳情况下执行过程的递归树如下图所示,图中lgn表示以2位底的对数,而本文中用logn表示以2位底的对数.图2
52、快速排序法最佳情况下执行过程的递归树由于快速排序法也是基于比较的排序法,其运行时间为(nlogn),所以如果每次划分过程产生的区间大小都为n/2,则运行时间(nlogn)就是最好情况运行时间。但是,是否一定要每次平均划分才能达到最好情况呢?要理解这一点就必须理解对称性是如何在描述运行时间的递归式中反映的。我们假设每次划分过程都产生9:1的划分,乍一看该划分很不对称。我们可以得到递归式:T(n)=T(n/10)+T(9n/10)+(n) , T(1)=(1) (4)这个递归式对应的递归树如下图所示:图
53、3 (4)式对应的递归树请注意该树的每一层都有代价n,直到在深度log10n=(logn)处达到边界条件, 以后各层代价至多为n。递归于深度log10/9n=(logn)处结束。这样,快速排序的总时间代价为T(n)=(nlogn),从渐进意义上看就和划分是在中间进行的一样。事实上,即使是99:1的划分时间代价也为(nlogn)。其原因在于,任何一种按常数比例进行划分所产生的递归树的深度都为(nlogn),其中每一层的代价为O(n),因而不管常数比例是什么,总的运行时间都为(nlogn),只不过其中隐含的常数因子有所不同。(关于算法复杂性的渐进阶,请参阅算法的复杂性)平均情况我们首先
54、对平均情况下的性能作直觉上的分析。要想对快速排序的平均情况有个较为清楚的概念,我们就要对遇到的各种输入作个假设。通常都假设输入数据的所有排列都是等可能的。后文中我们要讨论这个假设。当我们对一个随机的输入数组应用快速排序时,要想在每一层上都有同样的划分是不太可能的。我们所能期望的是某些划分较对称,另一些则很不对称。事实上,我们可以证明,如果选择Lp.r的第一个元素作为支点元素,Partition所产生的划分80%以上都比9:1更对称,而另20%则比9:1差,这里证明从略。平均情况下,Partition产生的划分中既有“好的”,又有“差的”。这时,与Partition执行过程对应的递归树中,好、差
55、划分是随机地分布在树的各层上的。为与我们的直觉相一致,假设好、差划分交替出现在树的各层上,且好的划分是最佳情况划分,而差的划分是最坏情况下的划分,图4(a)表示了递归树的连续两层上的划分情况。在根节点处,划分的代价为n,划分出来的两个子表的大小为n-1和1,即最坏情况。在根的下一层,大小为n-1的子表按最佳情况划分成大小各为(n-1)/2的两个子表。这儿我们假设含1个元素的子表的边界条件代价为1。(a) (b) 图4 快速排序的递归树划分中的两种情况在一个差的划分后接一个好的划分后,产生出三个子表,大小各为1,(n-1)/2和(n-1)/2,代价共为2n-1=(n)。这与图4(b)中的情况差不
56、多。该图中一层划分就产生出大小为(n-1)/2+1和(n-1)/2的两个子表,代价为n=(n)。这种划分差不多是完全对称的,比9:1的划分要好。从直觉上看,差的划分的代价(n)可被吸收到好的划分的代价(n)中去,结果是一个好的划分。这样,当好、差划分交替分布划分都是好的一样:仍是(nlogn),但记号中隐含的常数因子要略大一些。关于平均情况的严格分析将在后文给出。在前文从直觉上探讨快速排序的平均性态过程中,我们已假定输入数据的所有排列都是等可能的。如果输入的分布满足这个假设时,快速排序是对足够大的输入的理想选择。但在实际应用中,这个假设就不会总是成立。解决的方法是,利用随机化策略,能够克服分布
57、的等可能性假设所带来的问题。一种随机化策略是:与对输入的分布作“假设”不同的是对输入的分布作“规定”。具体地说,在排序输入的线性表前,对其元素加以随机排列,以强制的方法使每种排列满足等可能性。事实上,我们可以找到一个能在O(n)时间内对含n个元素的数组加以随机排列的算法。这种修改不改变算法的最坏情况运行时间,但它却使得运行时间能够独立于输入数据已排序的情况。另一种随机化策略是:利用前文介绍的选择支点元素pivot的第四种方法,即随机地在Lp.r中选择一个元素作为支点元素pivot。实际应用中通常采用这种方法。快速排序的随机化版本有一个和其他随机化算法一样的有趣性质:没有一个特别的输入会导致最坏
58、情况性态。这种算法的最坏情况性态是由随机数产生器决定的。你即使有意给出一个坏的输入也没用,因为随机化排列会使得输入数据的次序对算法不产生影响。只有在随机数产生器给出了一个很不巧的排列时,随机化算法的最坏情况性态才会出现。事实上可以证明几乎所有的排列都可使快速排序接近平均情况性态,只有非常少的几个排列才会导致算法的近最坏情况性态。一般来说,当一个算法可按多条路子做下去,但又很难决定哪一条保证是好的选择时,随机化策略是很有用的。如果大部分选择都是好的,则随机地选一个就行了。通常,一个算法在其执行过程中要做很多选择。如果一个好的选择的获益大于坏的选择的代价,那么随机地做一个选择就能得到一个很有效的算
59、法。我们在前文已经了解到,对快速排序来说,一组好坏相杂的划分仍能产生很好的运行时间。因此我们可以认为该算法的随机化版本也能具有较好的性态。在前文我们从直觉上分析了快速排序在平均情况下的性能为(nlogn),我们将在下面定量地分析快速排序法在平均情况下的性能。为了满足输入的数据的所有排列都是等可能的这个假设,我们采用上面提到的随机选择pivot的方法,并且在Select_pivot函数中将选出的pivot与Lp交换位置(这不是必需的,纯粹是为了下文分析的方便,这样Lp就是支点元素pivot)。那种基于对输入数据加以随机排列的随机化算法的平均性态也很好,只是比这儿介绍的这个版本更难以分析。我们先来
60、看看Partition的执行过程。为简化分析,假设所有输入数据都是不同的。即使这个假设不满足,快速排序的平均情况运行时间仍为(nlogn),但这时的分析就要复杂一些。由Partition返回的值q仅依赖于pivot在Lp.r中的秩(rank),某个数在一个集合中的秩是指该集合中小于或等于该数的元素的个数。如果设n为Lp.r的元素个数,将Lp与Lp.r中的一个随机元素pivot交换就得rank(pivot)=i(i=1,2,.,n)的概率为l/n。下一步来计算划分过程不同结果的可能性。如果rank(pivot)=1,即pivot是Lp.r中最小的元素,则Partition的循环结束时指针i停在i
61、=p处,指针j停在k=p处。当返回q时,划分结果的"低区"中就含有唯一的元素Lp=pivot。这个事件发生的概率为1/n,因为rank(pivot)=i的概率为1/n。如果rank(pivot)2,则至少有一个元素小于Lp,故在外循环while循环的第一次执行中,指针i停于i=p处,指针j则在达到p之前就停住了。这时通过交换就可将Lp置于划分结果的高区中。当Partition结束时,低区的rank(pivot)-1个元素中的每一个都严格小于pivot(因为假设输入的元素不重复)。这样,对每个i=1,2,.,n-1,当rank(pivot)2时,划分的低区中含i个元素的概率为 l/n。把这两种情况综合起来,我们的结论为:划分的低区的大小为1的概率为2/n,低区大小为i的概率为1/n,i=2,3,.n-1。现在让我们来对Quick_Sort的期望运行时间建立一个递归式。设T(n)表示排序含n个元素的表所需的平均时间,则: (5)其中T(1)=(1)。q的分布基本上是均匀的,但是q=1
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 2026内蒙古呼和浩特市实验幼儿园招聘教师1人备考题库及答案详解(全优)
- 2026重庆两江新区物业管理有限公司外包岗位招聘1人备考题库及答案详解(真题汇编)
- 2026陕西氢能产业发展有限公司(榆林)所属单位社会招聘27人备考题库附参考答案详解(夺分金卷)
- 2026中共北京市丰台区委党校面向应届毕业生招聘2人备考题库附答案详解(满分必刷)
- 柴科夫斯基教学设计小学音乐人音版五线谱北京三年级下册-人音版(五线谱)(北京)
- 2026江苏扬州市消防救援局政府专职消防人员国上半年招聘59人备考题库带答案详解(夺分金卷)
- 2026浙江台州市中医院招聘心电图诊断医生(编外)1人备考题库及参考答案详解(轻巧夺冠)
- 2026中共衢州市委党校引进高层次紧缺人才2人备考题库(浙江)附参考答案详解(基础题)
- 第二章 认识自我 悦纳自我教学设计中职心理健康第二版高教版(大学)
- 2026江西南昌市劳动保障事务代理中心招聘劳务派遣人员2人备考题库完整答案详解
- 规范信访基础业务培训
- 分汽缸安装施工方案
- 悬索桥毕业设计(小跨吊桥设计)
- DL∕T 1928-2018 火力发电厂氢气系统安全运行技术导则
- 2024年贵州六盘水市公安局合同制留置看护人员招聘笔试参考题库附带答案详解
- 银行资产配置方案
- 安捷伦GC仪器操作步骤
- GFM阀控密封铅酸蓄电池安装维护手册
- 牙体代型制备与修整(口腔固定修复工艺课件)
- 美学第六讲日常生活美
- 职业健康检查机构卫生管理自查表(2018年版)
评论
0/150
提交评论