算法导论 第七章:快速排序 笔记(快速排序的描述、快速排序的性能、快速排序的随机化版本、快速排序分析)

版权声明:站在巨人的肩膀上学习。 https://blog.csdn.net/zgcr654321/article/details/83068013

快速排序的最坏情况时间复杂度为Θ(n^2)。虽然最坏情况时间复杂度很差,但是快速排序通常是实际排序应用中最好的选择,因为它的平均性能很好。它的期望运行时间复杂度为Θ(n lg n),而且Θ(n lg n)中蕴含的常数因子非常小,而且它还是原址排序的。

快速排序是一种排序算法,对包含n个数的输人数组,最坏情况运行时间为Θ(n^2) 。虽然这个最坏情况运行时间比较差,但快速排序通常是用于排序的最佳的实用选择,这是因为其平均性能相当好:期望的运行时间为Θ(nlgn), 且Θ(nlgn)记号中隐含的常数因子很小。另外, 它还能够进行就地排序,在虚存环境中也能很好地工作

快速排序的描述:

快速排序也采用分治法进行排序,首先在数组中选择一个元素P,根据元素P将数组划分为两个子数组,在元素P左侧的子数组的所有元素都小于或等于该元素P,右侧的子数组的所有元素都大于元素P。

下面是对一个典型子数组A[p...r]排序的分治过程的三个步骤

分解:

数组A[p..r]被划分为两个子数组A[p..q-1]和A[q+1..r]使得A[p..q-1]中的每个元素都小于等于A(q),而且,元素A(q)小于等于A[q+1..r]中的元素。下标q也在这个划分过程中进行计算。

解决:

通过递归调用快速排序,对子数组A[p..q-1]和A[q+1...r]排序。

合并:

因为两个子数组是就地排序的,将它们的合并不需要操作:整个数组A [ p.. r ] 己排序。

伪代码:

//快速排序
QUICKSORT(A,p,r)
if p < r //当数组中只剩一个元素时,退出递归,数组已经有序
    q <- PARTITION(A,p,r)
    QUICKSORT(A,p,q-1) //对小于等于A[q]的元素进行递归
    QUICKSORT(A,q+1,r) //对大于A[q]的元素进行递归
//数组划分
PARTITION(A,p,r)
    x <- A[r] //将A[r]选取为“主元”
    i <- p - 1 //因为可能不存在小于等于A[r]的元素,所以i的值由p-1开始
    for j <- p to r-1 
        if A[j] <= x 
            i <- i + 1 //小于等于x的元素增加一个
            exchange A[i] <-> A[j] 
//i+1后,i指向了一个大于x的元素,此时,j指向的是一个小于等于x的元素,交换这两个元素的位置,使其符合规则
    exchange A[i+1] <-> A[r] //r前所有元素比较完后,将A[r]置于正确位置:两个子数组的交界处
    return i + 1 //返回"主元"的位置

如:

习题:

7.1-2:

当数组A[p.. r] 中的元素均相同时, PARTITION返回的q值是什么?修改PARTITION,使得当数组A[p.. r] 中所有元素的值相同时, q = (p+r)/2 。

返回最后一个元素r 。

修改PARTITION:

PARTITION_1(A,p,r)
    x <- A[r]
    i <- p - 1
    count <- 0
    //加入一个计数器
    for j <- p to r - 1
        if A[j]<= x
            if A[i] == x
                count = count + 1
            i <- i + 1
            exchange A[i] <-> A[j]
    if count == r - p + 1
        return (p + r)/2
    exchange A[i + 1] <-> A[r]
    return i + 1

快速排序的性能:

快速排序的运行时间依赖于划分是否平衡,如果划分平衡,那么快速排序算法性能与归并排序一样,如果划分不平衡,那么快速排序的性能就接近于插入排序了。

 最坏情况划分(不平衡):

最坏情况下,每次划分的两个子问题都分别包含了n-1个元素和0个元素。划分操作的时间复杂度是 Θ(n),因为对一个大小为0的数组进行递归调用后,返回了T(n)=O(1),故算法的运行时间可递归的表示为:

T(n) = T(n-1) + T(0) + Θ(n) =T(n-1) + Θ(n)

该递归式的解为:T(n) =Θ(n^2)。因此,最坏情况下,也就是数组中元素已经排好序的时候,快速排序的时间复杂度为Θ(n^2),而在同样的情况下,插入排序的时间复杂度为O(n)。

最好情况划分(最平衡):

最好的情况下,每次划分都是平均的划分为n/2个元素子数组,此时递归式为:

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

该式的解为T(n) = Θ(nlgn)。

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

事实上,任何一种常数比例的划分都会产生深度为Θ(nlgn)的递归树,其中每一层的代价都是O(n),因此,只要划分是常数比例的,算法的运行时间总是O(nlgn)

如:

例如,假设划分过程总是产生9 : 1 的划分,乍一看这种划分很不平衡,这时,快速排序运行时间的递归式为
T(n) <= T(9n/10) + T(n/10) + cn

该树每一层的代价都是cn, 直到在深度处达到边界条件时为止,在此之下各层的代价至多为cn。递归于深度处终止。这样,快速排序的总代价为O(nlgn) 。

当数据量很小的时候,比如只有十几个元素的小型序列,快排的优势并不明显,甚至比插入排序慢。但是一旦数据多,它的优势就充分发挥出来了。

C++ STL 中的sort函数,就充分发挥了快排的优势,并且取长补短,在数据量大时采用QuickSort,分段递归排序。一旦分段后的数据量小于某个门槛,为避免QuickSort递归调用带来过大的额外负荷,就改用插入排序。如果递归层次过深,还会改用HeapSort(堆排序)。所以说,C++的“混合兵种”sort的性能肯定会比C的qsort好

习题:

7.2-1:

利用代换法证明: 递归式的解为,如第7. 2 节开头提到的那样。

这里只证明上界,下界可类似证明。 

猜测

综合上下界有T(n)=Θ(n2)。

7.2-2:

当数组A 的所有元素都具有相同值时, QUICKSORT 的运行时间是什么?

当数组A的所有元素都具有相同值时,此时出现最坏情况,每次划分会出现大小为0和n−1的子数组,此时时间复杂度为Θ(n2)。

快速排序的随机化版本:

当输入的数据是随机排列的时候,快速排序的时间复杂度是O(nlgn)。但是在实际中,输入并不总是随机的,因此需要在算法中引入随机性,可以对输入进行重新排列是算法实现随机化, 也可以进行随机抽样,随机抽样是从数组A[p…r]中随机选择一个元素作为主元。

伪代码:

Randomized-Partition(A, p, r)
    i <- Random(p, r)
    exchange A[r] <-> A[i]
    return Partition(A, p, r)
Randomized-Quick-sort(A, p, r)
    if p < r
        q <- Randomized-Partition(A, p, r)
        Randomized-Quick-sort(A, p, q-1)
        Randomized-Quick-sort(A, q+1, r)

使用尾递归优化快速排序:

传统的递归算法在很多时候被视为洪水猛兽. 它的名声狼籍, 好像永远和低效联系在一起,尾递归是极其重要的,不用尾递归,函数的堆栈耗用难以估量,需要保存很多中间函数的堆栈。

QUICKSORT算法包含两个对其自身的递归调用,即调用PARTITION后,左边的子数组和右边的子数组分别被递归排序。

QUICKSORT中的第二次递归调用并不是必须的,可以用迭代控制结构来代替它,这种技术叫做“尾递归”,大多数的编译器也使用了这项技术。

下面的伪代码模拟了尾递归:

QUICKSORT'(A, p, r)
    while p < r
    do ▸ Partition and sort left subarray.
        q <- PARTITION(A, p, r)
        QUICKSORT'(A, p, q - 1)
        p <- q + 1

注意第一行是while而不是if。

上面的版本在最坏的情况下,就是划分不好的时候,递归深度为O(n)。

我们还可以进一步优化,用二分思想,为了使最坏情况下栈的深度为Θ(lgn),我们必须让PARTITION后左边的子数组为原来数组的一半大小,这样递归的深度最多为Θ(lgn)。

一种处理方案:

首先求得(A, p, r)的中位数,作为PARTITION的枢轴元素,这样可以保证左右两边的元素的个数尽可能的均衡。

因为求中位数的过程MEDIAN的时间复杂度为Θ(n),因此可以保证算法的期望的时间复杂度O(nlgn)不变。

优化后的尾递归快排:

QUICKSORT (A, p, r) 
    while p < r
        do Partition and sort the small subarray Prst 
        q <- PARTITION(A,p,r) 
        if q - p < r - q
            then QUICKSORT (A, p, q - 1) 
                p <- q + 1
        else QUICKSORT (A, q + 1,r)
            r <- q - 1

除此之外,快排还可以有优化:

三数取中:

从子数组中随机选出三个元素,取其中间数作为主元,不过这只能影响快速排序时间复杂度O(nlgn)的常数因子。

非递归方法:

即模拟递归,这样可以完全消去递归的调用。

三划分快速排序:

基本思想是,在划分阶段以V=A[r]为基准,将带排序数组A[p..r]划分为左、中、右三段A[p,j],A[j+1..q-1],A[q..r],其中左段数组元素值小于V,中断数组等于V,有段数组元素大于V。其后,算法对左右两段数组递归排序。 这个方法对于有大量相同数据的数组排序效率有很大的提高,即使没有大量相同元素,也不降低原快排算法的效率

快速排序分析:

利用RANDOMIZED-PARTITION,快速排序算法期望的运行时间当元素值不同时,为O(nlgn)。

习题:

7.4-1:

证明:在递归式:中,T(n)=Ω(n^2)。

使用代换法。

猜测 T(n) <= c*n^2,c为某个常数。


选择足够大的c,使得 c*(2*n-1)可以支配Θ(n), T(n) <= c*n^2成立。

7.4-4:

证明:RANDOMIZED-QUICKSORT 算法的期望运行时间是Ω(nlgn)。

猜你喜欢

转载自blog.csdn.net/zgcr654321/article/details/83068013