快速排序
对于包含n个数的输入数组来说,快速排序是一种最坏情况时间复杂度为 的排序算法,虽然最坏情况时间复杂度很差,但是快速排序通常是实际排序应用中最好的选择,因为它的平均性能非常好:它的期望时间复杂度是 ,另外,它能够进行原址排序,甚至在虚拟环境中也能很好地工作。随机化版本在任何的输入情况(包括最坏情况)下获得均匀的运行时间。
算法描述
分解:数组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)
QUICKSORT(A, q+1, r)
为了排序一个数组A的全部元素,初始调用时QUICKSORT(A, 1, A.length)
数组的划分:
算法的关键部分是PARTITION过程,实现了对子数组A[p..r]的原址重排
PARTITION(A, p, r)
x = A[r]
i = p – 1
for j = p to r – 1
if A[j] <= x
i = i +1
exchange A[i] with A[j]
exchange A[i+1] with A[r]
return i+1
性能分析
时间的复杂度可以表示为:T(n) = T(k) + T(n-k-1) + (n)
其中 T(k), T(n-k-1) 对应程序中的递归调用,k是在划分后比基准元素小的元素个数。 (n) 就是partition函数的复杂度。
算法的效率对不同的输入数据会有所不同,可以分为下面3中情况:
1) 最坏的情况。当分区过程总是挑选最大或最小的元素作为支点。如果我们考虑以上的分区策略,其中最后一个元素总是被挑为支点,当数组已经排序的递增或递减顺序时,会发生最坏的情况:
T(n) = T(0) + T(n-1) +(n), 即 T(n) = T(n-1) +(n)
此时的复杂度为 O(n^2)
2) 最好的情况。发生在最好的情况下,当分区过程总是挑选最中间元素为支点。
T(n) = 2 T(n/2) +(n)
此时的递归表达式和归并排序是一样的。时间时间复杂度为 O(n lg n).
3) 平均情况。
要做到平均情况分析,我们需要考虑数组中的所有可能的排列,并计算每一个排列所需要的时间,这种计算是比较麻烦的。
可以这么分析,如果划分的结果是这样的两部分 O(n/10) 和 O(9n/10)。递归式为: T(n) = T(n/10) + T(n9/10) +(n)
最终的计算结果也为:O(n lg n).
虽然快速排序的最坏情况下复杂度为O(N^2),这比很多其他的排序算法,如归并排序和堆排序,在实践中更快,因为它的内部循环可以在大多数架构有效地实现,
代码实现
#include<stdio.h> void swap(int *, int ,int); int partion_1(int *, int, int); int partion_2(int *, int, int); void quicksort(int *, int, int); main(){ int array[]={23,3,35,5,75,2,34,45,73,21,9,33}; int i, len; for (i = 0; i < 12; i++) printf("%d\t",array[i]); putchar('\n'); quicksort(array, 0, 11); for (i = 0; i < 12; i++) printf("%d\t",array[i]); putchar('\n'); } void swap(int *array, int p, int q){ int temp; temp = array[p]; array[p] = array[q]; array[q] = temp; } int partion_1(int *array, int p, int q){ //选择第一个元素作为主元 int i, last; int piviot; piviot = array[p]; for (last = p, i = p+1; i <= q; i++){ if (array[i] <= piviot) swap(array, ++last, i); } swap(array, p, last); return last; } int partion_2(int *array, int p, int q){ //选择最后一个元素作为主元 int piviot; int i,j; piviot = array[q]; for (i = p, j = p-1; i < q; i++){ if (array[i] <= piviot){ swap(array, i, ++j); } } swap(array, q, ++j); return j; } void quicksort(int *array, int begin, int end){ int medium; if (begin < end){ medium = partion_1(array, begin, end); //medium = partion_2(array, begin, end); quicksort(array, begin, medium-1); quicksort(array, medium+1, end); } }
改进版本
如果要排列的数组本身就有序,那么上述的程序会发生较多次自己和自己交换的情况,此时会造成效率低下问题。下面的程序使用两个“指针”(i和j),一前一后地同时进行扫描交换,效率会更好
int partion(int *array, int p, int q){ int i, j, piviot; i = p; j = q+1; piviot = array[p]; while(1){ while(i < q && array[++i] < piviot );//略过比piviot小的数 while(j > p &&array[--j] > piviot); //略过比piviot大的数 if (i >= j) break; swap(array, i, j); } array[p] = array[j]; array[j] = piviot; return j; }
随机化版本伪代码(算法改进)
为了对所有输入的情况都获得平均运行时间,在算法中引入随机性,从而使得算法对于所有的输入都能获得较好的期望性能。在这里采用一种随机抽样的随机化技术,与之前始终采用A[r]作为主元的方法不同,随机抽样时从子数组A[p..r]中随机选择一个元素作为主元,为了达到这一目的,首先将A[r]与从A[p..r]中随机选取的一个元素交换。因为主元元素时随机选取的,我们期望在平均情况下对输入数组的划分是比较平衡的。
对PARTITION和QUICKSORT的代码的改动非常小,在新的划分程序中,我们只是在真正进行划分前进行一次交换。
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)
RANDOMIZED-QUICKSORT(A, p, q-1)
RANDOMIZED-QUICKSORT(A, q+1, r)