算法学习笔记----快速排序

一、算法描述

  快速排序是一种最坏情况时间复杂度为Θ(n^2)的排序算法。虽然最坏情况时间复杂度很差,但是快速排序通常是实际排序应用中最好的选择,因为它的平均性能非常好:它的期望时间复杂度为Θ(nlgn),而且Θ(nlgn)中隐含的常数因子非常小。另外,它还能原址排序,甚至在虚存环境中也能很好地工作。

算法步骤如下所示:

1、将源数组A[p..r]划分为两两个子数组A[p..q-1]和A[q+1..r],使得A[p..q-1]中的每一个元素都小于等于A[q],而A[q+1..r]中的每一个元素都大于A[q],我们将这个过程称为PARTITION过程。

2、递归调用快速排序,对两个子数组分别进行快速排序

3、左右两个子数组都是排好序的,并且左边的元素总是小于等于右边的元素,所以不需要合并操作,数组A[p..r]已经是排好序的了。

二、算法实现

  快速排序算法最重要的部分是将源数组划分为两个子数组的算法的实现,这直接影响快速排序算法最终的时间复杂度。下面的PARTITION实现中只使用了一个游标,可以减少在执行过程中的比较次数。快速排序的完整实现如下所示:

[cpp]  view plain copy
  1. #include <stdio.h>  
  2.   
  3. static void exchange(int *a, int i, int j)  
  4. {  
  5.     if (i == j) {  
  6.         return;  
  7.     }  
  8.   
  9.     a[i] ^= a[j];  
  10.     a[j] ^= a[i];  
  11.     a[i] ^= a[j];  
  12. }  
  13.   
  14. static int partition(int *a, int p, int r)  
  15. {  
  16.     int i, j;  
  17.     int x;  
  18.   
  19.     x = a[r];  
  20.     i = p - 1;  
  21.     for (j = p; j < r; ++j) {  
  22.         if (a[j] <= x) {  
  23.             ++i;  
  24.             exchange(a, i, j);  
  25.         }  
  26.     }  
  27.     exchange(a, i + 1, r);  
  28.     return i + 1;  
  29. }  
  30.   
  31. static void quicksort(int *a, int p, int r)  
  32. {  
  33.     int q;  
  34.   
  35.     if (p >= r) {  
  36.         return;  
  37.     }  
  38.   
  39.     q = partition(a, p, r);  
  40.     quicksort(a, p, q - 1);  
  41.     quicksort(a, q + 1, r);  
  42. }  
  43.   
  44. #define ARRAY_SIZE(a) (sizeof(a) / sizeof(a[0]))  
  45.   
  46. int main(void)  
  47. {  
  48.     int a[] = {2, 8, 7, 1, 3, 5, 6, 4};  
  49.     int i;  
  50.       
  51.     quicksort(a, 0, ARRAY_SIZE(a) - 1);  
  52.   
  53.     for (i = 0; i < ARRAY_SIZE(a); ++i) {  
  54.         printf("%d ", a[i]);  
  55.     }  
  56.     printf("\n");  
  57.     return 0;  
  58. }  
  上面的PARTITION实现在数组元素都相同时,会将源数组划分为元素个数分别为n-1和0的子数组,这种情况就是快速排序算法时间复杂度最差的情况,即时间复杂度为Θ(n^2)。为了避免这种情况,可以使用两个游标,其过程是:每次循环,如果左游标指向的元素小于主元,则将左游标加1;如果右游标指向的元素大于主元元素,则右游标加1;如果左游标小于右游标,并且左游标指向的元素大于等于主元元素并且右游标指向的元素小于等于住院元素时交换左右游标指向的元素。这样划分的子数组个数分别为⌊n/2⌋和⌈n/2⌉-1。其实现如下所示:
[cpp]  view plain copy
  1. static int partition(int *a, int p, int r)  
  2. {  
  3.     int left, right;  
  4.     int x = a[r];  
  5.   
  6.     left = p;  
  7.     right = r - 1;  
  8.     while (left <= right) {  
  9.         if (a[left] <= x) {  
  10.             left++;  
  11.         }  
  12.   
  13.         if ((left <= right) && (a[right] >= x)) {  
  14.             right--;  
  15.         }  
  16.   
  17.         if ((left < right) && (a[left] >= x) &&  
  18.                 (a[right] <= x)) {  
  19.             exchange(a, left, right);  
  20.         }  
  21.     }  
  22.     exchange(a, left, r);  
  23.     return left;  
  24. }  

三、算法分析
1、最坏情况划分
当划分产生的两个子数组分别包含了n-1个元素和0个元素时,快速排序的最坏情况发生。假设算法的每一次划分都产生这种不平衡划分。划分操作的时间复杂度是Θ(n)。对于一个大小为0的数组递归调用会直接返回,因此T(0)=Θ(1),于是算法的时间复杂度可以表示为:

该递归式的解为T(n)= Θ(n^2).可以通过画递归树来证明,也可以使用代入法来证明。我们假设T(n)=Θ(n^2)是递归式的解,证明如下:


2、最好情况分析

最好的情况是每个子数组的大小都接近于数组元素个数的一半,即其中一个子数组的规模为⌊n/2⌋,另一个子数组的规模为⌈n/2⌉-1,这种情况下快速排序的性能非常好。此时的算法复杂度的递归式为:

根据主方法的情况2,递归式的解为T(n)=Θ(nlgn)。

快速排序算法的平均运行时间更接近于其最好情况,只要划分是常数比例的,算法的运行时间总是Θ(nlgn)。即使划分产生9:1的划分,也是如此,通过画递归树很容易证明该结论。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值