快速排序
Posted a-runner
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了快速排序相关的知识,希望对你有一定的参考价值。
对于包含n个数的暑期如的数组来说,快速排序是一种最坏的情况为时间复杂度为n2的排序算法。虽然最坏情况时间复杂度很复杂,但是快速排序法通常是实际应用中最好的选择,因为平均性能非常好。在元素互异的情况下,期望的时间 复杂度为nlog(n)。
快速排序同样采用了分治策略:
通过递归调用,对数组A[p, .q-1],和A[q+1,.r]进行排序。因为数组都是按照原址排序的。所以不需要合并。
将最后一个作为主元,围绕它来划分数组。
def QuickSort(A, p, r): if p < r: q = Partition(A, p, r) QuickSort(A, p, q-1) QuickSort(A, q+1, r) # 实现了对子数组的重排 def Partition(A, p, r): x = A[r] i = p - 1 for j in range(p,r): if A[j]<=x: i = i + 1 A[i], A[j] = A[j], A[i] A[i+1],A[r] = A[r],A[i+1] return i+1
在Partition3-6行循环体的每一次迭代开始,对于任意的数组的下标k:
1 若p<=k<=i,则A[k]<=x.
2 若i+1<=k<=j-1,则A[k]>x
3 若k=r,则A[k]=x
快速排序的性能:
最坏情况:
当划分的两个子问题分别包含n-1和0个元素的时候。运行时间的递归式为:
T(n) = T(n-1) + T(0) + theta(n) = T(n-1) + theta(n)
每一层递归的代价叠加起来,从而得到一个算术级数,结果为theta(n2).
最好情况:
Pratition得到的两个子数组问题的规模都不大于二分之n,其中一个问题的规模为floor(n/2),另一个为ceil(n/2)-1。递归式为:
T(n) = 2T(n/2) + theta(n)
根据主方法可得时间复杂度为nlog(n).
利用带入法证明:正如7.2节开头提到的那样,递归式T(n)=T(n-1)+Θ(n)的解为T(n)=Θ(n^2) 令Θ(n)=cn (c常数)。
假设T(n)在n-1上成立。 先证明T(n)=Ο(n^2) T(n)<=c1(n-1)^2+cn<=c1n^2 <=> (c-2c1)n+c1<=0 <=> c-2c1<0 (c<2c1) n>=c1/(2c1-c) 当0<c<2c1时,有n>=n0=c1/(2c1-c) 对于足够大的n都成立。 再证明T(n)=Ω(n^2) T(n)>=c2(n-1)^2+cn>=c2n^2 <=> (c-2c2)n+c2>=0 <=> c-2c2>0 (c>2c2>0) n>0 当c>2c2>0时,有n>0,对于足够大的n都成立。 所以存在常数c1>c/2,c2<c/2时,存在n0=max{c1/(2c1-c),0},使得当n>=n0时,对于足够大的n都成立,T(n)=Θ(n^2)成立
插入排序在基本有序的情况下,基本无需移动任何元素来插入,所以只有外层循环了n次,所以时间复杂度为O(n)
快速排序在基本有序的情况下,在划分数组时,划分得非常不平衡,那么其时间复杂度是O(nlgn),而达到完全有序时,时间 复杂度达到O(n^2),</p><p>所以总之插入排序要优于快速排序。
快速排序的随机优化版本:
前面我们判断的时候,取的是最后一个当作主元,如果对于有序排列的情况,这种就会i俺的非常耗时。所以我们采用一种随机取样的方法。
# 随机化版本 def Random_Partition(A, p, r): i = random.randint(p, r) A[r], A[i] = A[i], A[r] return Partition(A, p ,r) def Random_QuickSort(A,p,r): if p < r: q = Random_Partition(A, p, r) QuickSort(A, p, q-1) QuickSort(A, q+1, r)
下面给出最早Hoare所设计的划分算法:
# A[p,j]中的每个元素都小于等于A[j,r]中的元素 def Hoare_Partition(A, p, r): x = A[p] i = p - 1 j = r + 1 while True: while A[j]>=x: j = j-1 while A[i]>=x: i = i+1 if i<j: A[i],A[j] = A[j],A[i] else: return j
尾递归:
QuickSort的第二个递归调用并不是必须的,我们可以用一个循环控制结构代替。
def Tail_Recursive_QuickSort(A, p, r): while p<r: # Partition and sort left subarray q = Partition(A, p, r) Tail_Recursive_QuickSort(A, p, q-1) p = q+1
7-4.2 来自教材手册:
def QUICKSORT”(A, p,r):
while p < r // Partition and sort the small subarray first. q = PARTITION(A, p,r) if(q-p<r-q): QUICKSORT"(A,p,q-1)/ p=q + 1 else:
QUICKSORT"(A,q+1,r)/ r=q+1
假设快速排序的每一层所做的划分的比例都是1-a:a,其中0<a<=1/2且是一个常数。是证明,在相应的递归树中,叶结点的最小深度大约是 -lgn/lga,最大深度大约是-lgn/lg(1-a)(无需考虑整数舍入问题)
0<=a<=1/2 => a<1-a 只有T(1)时,到达树底,设树高度为k,所以就有 最小深度(a^k)n=1=>k=-lgn/lga 最大深度(1-a)^kn=1 => k=-lgn/lg(1-a)
针对相同元素值的快速排序
PARTITION过程返回一个数组下标q,使得A[p..q-1]中的每个元素都小于等于A[q],而A[q+1..r]中的每个元素都大于A[q]。修改PARTITION代码来构造一个新的PARTITION‘(A,p,r),它排列A[p..r]的元素,返回值是两个数组下标q和t,其中p<=q<=t<=r,且有
A[q..t]中的所有元素都相等。
A[p..q-1]中的每个元素都小于A[q]。
A[t+1..r]]中的每个元素都大于A[q]。
与PARTITION类似,新构造的PARTITION’的时间复杂度是Θ(r-p)。
#include <iostream> #include <time.h> using namespace std; int PARTITION(int A[],int p,int r) { int x=A[r]; int i=p-1,k=0,t=r; for (int j=p;j<r-1;j++)//循环了r-1-p+1次=r-p次 {//此循环的目的是将所有和主元相同的元素都移动到数组最右边 if(A[j]==x) { if(A[j]!=A[r-1]) { swap(A[j],A[r-1]); r--; } else { j--;r--; } } } for (j=p;j<=r-1;j++)//循环了r-1-p+1次=r-p次 {//此循环的目的是记录第一个大于主元的元素A[i] if (A[j]<=x) { i++; swap(A[i],A[j]); } } for (int h=0;h<t-r+1;h++)//循环了t-r+1-0=t-r+1次,这也正是数组中相同元素个数,相同元素个数<=r-p {//此循环的目的是将已经划分好的数组的最右边和主元相同的元素移动到大于主元和小于主元之间。 swap(A[i+h+1],A[r+h]); } return i+1; }
参考:https://blog.csdn.net/z84616995z/article/details/18187401
以上是关于快速排序的主要内容,如果未能解决你的问题,请参考以下文章