快速排序
基础快排实现
一种允许就地排序并且通常快于合并排序的递归算法。
在数组中寻找一个标记(pivot)将数组分成两部分
平均情况:时间复杂度 O ( l o g n ) O(logn) O(logn),空间复杂度 O ( l o g n ) O(log n) O(logn)
最坏情况:时间复杂度 O ( n 2 ) O(n^2) O(n2),空间复杂度 O ( n ) O(n) O(n)
划分过程简单实现
PARTITION(A,p,r)
x <- A[r] //pivot
i <- p-1
for j <- p to r-1
do if A[j] <= x
then i <- i+1
exchange A[i] and A[j]
exchange A[i+1] and A[j]
return i+1
给出划分过程:
图中轻阴影部分表示小于pivot,重阴影部分大于pivot
快排实现(递归):
QUICKSORT(A,p,r)
IF p<r
THEN q <- PARTITION(A,p,r) //返回标签index
QUICKSORT(A,p,q-1) //分治思想
QUICKSORT(A,q+1,r)
运行时间分析
在最好的情况下,列表每次被划分成大致相等的两个部分,因此运行时间和合并排序相似( Θ ( n l o g n ) \Theta(nlogn) Θ(nlogn))
最好情况下的递归表达式: T ( n ) = 2 T ( n / 2 ) + Θ ( n ) T(n) = 2T(n/2) + \Theta(n) T(n)=2T(n/2)+Θ(n)
说明: Θ ( n ) \Theta(n) Θ(n)指的是PARTITION的时间代价。如何求解出具体的时间复杂度可以用递归树求解并通过替换法证明。第一章写过在此不再赘述。
在最坏的情况下,设想如果我们每次很不幸的选择了最小或最大的元素来作为我们的标记值,那么我们每次都将递归排序比上一次长度-1的序列。
最坏情况下的递归表达式: T ( n ) = T ( n − 1 ) + Θ ( n ) T(n)=T(n-1)+\Theta(n) T(n)=T(n−1)+Θ(n),求解为 Θ ( n 2 ) \Theta(n^2) Θ(n2)
如果我们假设每次pivot将序列划分为1:9两个部分,同样可以得到时间复杂度为 Θ ( n l o g n ) \Theta(nlogn) Θ(nlogn)
通过中位数提升快排
- 如果我们将中位数作为我们的pivot那么它的划分效果将是最为完美的,但是寻找中位数显得非常困难。所以我们将数组头部、尾部、中间下标的中位数作为pivot。
- 对于小型的数组采用插入排序。
ex:找到数组中从小到大第k个元素
算法1:直接排序+遍历到第k个 时间复杂度: Θ ( n l o g n ) \Theta (nlogn) Θ(nlogn)
算法2:顺序选择: O ( k n ) O(kn) O(kn)
算法3:快速搜索
QuickSearch(A,p,r,k)
if (p==r) return p
t <- Partition(A,p,r) //pivot
if (t==k) return t //pivot所在的位置是他从小到大排列正确的下标
if (k<t)
QUICKSearch(A,p,t-1,k) //第k个比pivot小
else
QUICKSearch(A,t+1,r,k)
时间复杂度:
最差情况: O ( n 2 ) O(n^2) O(n2) 额外空间: O ( n ) O(n) O(n)
最好情况: O ( 1 ) O(1) O(1)
平均情况: O ( n ) O(n) O(n),额外空间: O ( l o g n ) O(logn) O(logn)
三种排序的比较
注意:很多人会好奇,快排在原地排序为什么需要额外的空间,这里的空间指的是递归调用时用来保存原来状态的栈空间,即你画的递归树的高度。