算法基础4—快速排序

来源:互联网 发布:最新网络红歌排行榜 编辑:程序博客网 时间:2024/04/29 16:25

快速排序也是在面试中常被问的排序算法之一,它与归并算法一样,也使用了分治的思想。快速排序的三步分治过程:

  1. 分解:将一个待排序数组A[p,…,r]划分为两个字数组(可能为空)A[p…q-1], A[q+1…r],其中A[p…q-1]中的每一个元素都小于A[q], A[q+1…r]中的每一个元素都大于A[q]。计算下标q也是划分过程中的一部分。
  2. 解决: 通过递归调用快速排序,对子数组A[p…q-1]和A[q+1…r]进行排序。
  3. 合并:因为子数组都是原址排序的,所以不需要合并操作,数组A[p…r]已经有序。
    下面先给出快速排序的伪代码:
quickSort(A, p, r) 1.if p<r 4.  q = partition(A,p,r) 5.  quickSort(A, p, q-1) 6.  quickSort(A, q+1, r)

为了排序一个数组的所有元素,初始调用是quickSort(A, 1,A.length)。
首先介绍一个快速排序的最关键的一步就是数组的划分:
即partition部分,它实现了对数组A[p…r]的原址重排。
番外:<原址在百度百科中给出的定义是在排序算法中,如果输入数组中仅有常数个元素需要在排序过程中存储在数组之外,则称排序算法是原址的。为什么归并排序不是原址排序,而堆排序是原址的呢?要怎么理解这句话呢?
因为归并排序还要另外一个数组做merge。那个是O(n)空间。in-place归并是可以写出来的,就是超级麻烦。快排是in-place的,因为它就是在自己数组里折腾。堆排也是,它建立了一个堆的结构,每次deletemax少一个元素,但它还是在自己数组里折腾的,只是这个数组前面若干个元素维护了堆结构,后面若干个元素是由小到大排好顺序的,堆永远不会访问后面排好顺序的元素。>
继续给出partition的伪代码:

partition(A, p, r) 7. x = A[r]            //选定的基准值 8. i = p-1             //类似一个指针,指定要交换的元素 9. for j = p to r        //也类似一个指针,指定后面满足交换条件的元素 10.     if A[j] <= x 11.        i ++        12.        exchange A[i] with A[j] 13. exchange A[i+1] with A[r] 14. return i+1

在子数组A[p…r]上,partition维护了4个区域,如下图所示,A[p…i]区间内的所有值都是小于等于x,A[i..j]区间内的所有值都是大于x,A[r]=x。字数组A[j…r-1]中的值可能属于任何一种情况。


当A[j..r]中的元素小于等于x时,执行如算法中的操作,i++,并且交换A[i]和A[j]。元素大于x时,没有明显操作,即加入到A[i..j]中。
js代码如下:

var partition = function(a, p, r){    var x = a[r-1];    var i = p-1;    for(var j = p; j < r-1; j++)    {        if(a[j] <= x)        {            i = i+1;            var temp = a[i+1];            a[i+1] = a[j];            a[j] = temp;        }    }    var ans = a[i+1];    a[i+1] = a[r-1];    a[r-1] = ans;    return i+1;}var qSort = function(a, p, r){    if(p < r)    {        var q = partition(a, p, r);        qSort(a, p, q-1);        qSort(a, q+1, r);    }}

交换两个元素时,有个swap()函数,可是我还没有具体看它是怎么实现的,所以还是先这样利用一个变量进行交换。
还有一个过程,就是算法的时间复杂度分析,虽然我们常说快速排序的时间复杂度是O(nlgn),却很少具体的分析一下什么情况下是O(nlgn),这个是快排最好的时间复杂度吗?
我们都知道快排的运行时间依赖于划分是否平衡,而平衡与否又依赖于用于划分的元素。如果划分是平衡的,那么快排算法性能与归并排序一样。如果划分不平衡,那么快排的性能就接近于插入排序了。
下面从三个不同的划分来分析一下快排的时间复杂性。

  1. 最坏情况划分

    当划分的两个字问题分别包含了n-1个元素和0个元素。我们假设每次递归调用都出现了这种不平衡的划分。划分操作的时间复杂度是O(n)。由于对大小为0的数组进行递归会直接返回,因此T(0) = O(1),于是算法运行的时间递归式可以表示成
    T(n) = T(n-1)+O(n)
    利用带入法可以得到T(n) = O(n)+O(n-1)+O(n-2)+…+O(1)+T(0)
    即: T(n) = O(n^2)
    可以看到,最坏情况下快速排序运行时间并不比插入排序更好。此外,当数组已经排好序时,时间复杂度依然为O(n^2),而插入排序的时间复杂度为O(n)。

  2. 最好情况划分

    在可能的最平衡的划分中,partition得到的两个子问题的规模都不大于n/2。这是因为其中一个子问题的规模为[n/2],而另一个子问题的规模为[n/2]-1。在这种情况下快速排序的性能非常好。此时,算法运行的时间递归式为:
    T(n) = 2T(n/2)+O(n)
    上述递归式的解为T(n) = O(nlgn)。
    事实上,任何一种常数比例的划分都会产生深度为O(lgn) 的递归树,其中每一层的时间代价都是O(n),因此,只要是常数比例的划分,算法的运行时间都是O(nlgn)。

  3. 平均情况

    在平均情况下,partition所产生的划分同时混合有“好”和“差”的划分。此时,在递归树中,好和差的划分是随机分布的。
    如下图a所示,显示了连续两层上的划分,根节点处划分的代价为n,划分产生两个子数组,大小分别为n-1和0。在下一层上,大小为n-1的子数组按最好情况划分。这一组合的划分代价为O(n)+O(n-1)= O(n)。该代价并不比(b)中所示的划分情况差。因此,当好的划分和差的划分交替出现时,快速排序的时间复杂度与全是好的划分时一样,依然是O(nlgn)。区别只是O符号中隐含的常数因子要略大一些。
    这里写图片描述

0 0
原创粉丝点击