算法导论笔记:07快速排序

来源:互联网 发布:mysql 多条select语句 编辑:程序博客网 时间:2024/05/20 20:21

1:快速排序

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

 

2:基本思想

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

 

       下面是对一个典型子数组A[p...r]排序的分治过程的三个步骤:
       a、分解:数组A[p..r]被划分为两个子数组A[p..q-1]和A[q+1..r]使得A[p..q-1]中的每个元素都小于等于A(q),而且,元素A(q)小于等于A[q+1..r]中的元素。下标q也在这个划分过程中进行计算。
       b、解决:通过递归调用快速排序,对子数组A[p..q-1]和A[q+1...r]排序。
       c、合并:快速排序对子数组采用就地排序,将两个子数组的合并并不需要操作。

 

       该算法的伪代码如下:

              QUICKSORT( A, p, r )
                     if p < r
                            then q =PARTITION( A, p, r );
                            QUICKSORT( A,p, q - 1);
                            QUCKSORT( A, q+ 1, r );

 

       该算法的关键部分就是PARTITION,PARTITION不仅将数组分为两个子数组,而且返回选择key元素的位置。

       partition子程序的伪代码如下,该过程的时间复杂度为Θ(n)

              PARTITION( A, p, r )
                     x = A[r];       //选取最后一个元素为比较值(主元,pivot element)
                     i = p - 1;        //i是分界点,i之前(包括i)的元素都小于x,i之后,j之前的元素都大于x

                     for( j = p; j <= r - 1;j ++ )
                            if ( A[j] <= x )
                            {

                                   i ++;
                                   exchange(A[i], A[j] );
                            }
                     exchange( A[i+1] ,A[r] );
                     return i + 1;

 

       在PARTITION中,首先选择一个主元,根据该主元对数组进行划分,划分为4部分,如下图所示:

      

对照代码中的部分,这4部分包括:

       a:范围pki中, A[k]x

       b:范围i+1kj-1中,A[k]

       c:范围jkr-1中,数组元素未划分,最终都要划分到上面俩范围之一;

       dk=r,则A[k]x

 

3:快速排序的性能

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

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

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

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

 

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

       该递归式可得T(n) = O(nlgn)。

       快速排序的平均运行时间更接近于其最好情况,而非最坏情况,事实上,任何一种常数比例的划分都会产生深度为Θ(nlg n)的递归树,其中每一层的代价都是O(n),因此,只要划分是常数比例的,算法的运行时间总是O(n lgn)

 

4:快速排序的随机化版本

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

       RANDOMIZED-PARTITION(A, p, r)

              I = RANDOM(p,r)

              exchange A[r] with A[i]

              return PARTITION(A, p, r)

 

       RANDOMIZED-QUICKSORT(A, p, r)

              if p < r
                     q = RANDOMIZED -PARTITION(A, p, r );
                     QUICKSORT( A, p, q -1);
                     QUCKSORT( A, q + 1, r);

 

5:尾递归

       普通的快速排序需要两次的递归调用,分别针对左子数组和右子数组,可以使用尾递归技术消除QUICKSORT中第二个递归调用,也就是用循环代替它,好的编译器都具有这种功能:

       TAIL-RECURSIVE-QUICKSORT(A, p, r)

              while p < r

                     q = PARTITION(A, p, r)

                     TAIL-RECURSIVE-QUICKSORT(A,p, q-1)

                     p = q+1

 

6:完整代码如下:

int  partition(int *set, int  begin, int  end)
{
       int i, j, k;
       int x;
       x = set[end];
       i = begin - 1;
       for(j = begin; j <= end - 1; j++)
       {
              if(set[j] <= x)
              {
                     i++;
                     exchange(set+i, set+j);
              }
       }

       exchange(set+i+1, set+end);
       return i+1;
}


void quicksort(int *set, int begin, int end)
{
       int index;
       if(begin < end)
       {
              index = partition(set, begin, end);
              quicksort(set, begin, index-1);
              quicksort(set, index+1, end);
       }
}

0 0