快速排序

来源:互联网 发布:电魂网络客服电话 编辑:程序博客网 时间:2024/06/15 21:16

快速排序的基本思想:通过一趟排序将待排记录分割为独立的两个部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分继续进行排序,以达到整个序列有序的目的。

快速排序与归并排序一样,也使用了分治思想。 下面对一个典型的子数组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]已经有序。
//根据上面对快速排序的实现描述可以写出快速排序的整体框架。void QSort(int arr[],int low,int high){    int pivot;    if(low < high)    {        //其中,如何正确的选取枢纽值是这里的关键        pivot = Partition(arr,low,high);//算出枢纽值,将当前数组一分为二。        QSort(arr,low,pivot-1);//对低位子表递归排序        QSort(arr,pivot+1,high);//对高位子表递归排序    }}

Partition函数实现了对数组A[p..r]的原址重排。要做的是,先选取数组其中一个关键字(这里先暂时不讨论关键字的合理性),然后想办法将其放到一个位置,使得它左边的值都比它小,右边的值都比它大。

//对数组进行原址排序,使枢纽记录到位,并返回其位置//完成函数后,在它之前(后)的记录的不大(小)于它int Partition(int arr[],int low,int high){    int pivotkey = arr[low];//使用第一个元素作为枢纽值(暂时不考虑合理性)    while(low < high)    {        while(low < high && arr[high] >= pivotkey)//确定是否交换            high--;//后者确实比枢纽值大,不需要交换,比较下一个元素        arr[low] = arr[high]; //将比枢纽元素小的元素交换到前端。        while(low < high && arr[low] <= pivotkey)            low++;        arr[high] = arr[low];//将比枢纽元素小的元素交换到后端。    }    arr[low] = pivotkey;//最终low=high,为数组的最终分割点      return low;//返回枢纽值的位置}

在最优的情况下,快速排序算法的时间复杂度为O(nlogn)。在最平衡的划分中,将每个问题都划分为两个规模都不大于n/2的子问题,然后接着向下划分,在这种情况下,快速排序的性能很好。
在最坏的情况下,时间复杂度为O(n*n),其平均复杂度为O(nlogn)。待排序的序列为正序或逆序时,每次划分只得到一个比上一次划分少一个记录的子序列(另一个序列为空),此时需要n-1次递归调用,而第i次划分需要经过n-i次关键字的比较才能找到第i个记录,也就是枢纽的位置,因此比较次数为[n-1…1]=n(n-1)/2,时间复杂度为O(n*n)。
同时快速排序的平均运行时间更接近于其最好情况,而非最坏情况,即快速排序算法的平均时间复杂度为O(nlogn)。事实上,对于任意一种常数比例的划分都会产生深度为O(logn)的递归树,其中每一层的时间代价为O(n)。因此,只要划分是常数比例的,算法的运行时间总是O(nlogn)。(详细介绍参见《算法导论》第七章第二节 <快速排序的性能>)
空间复杂度:O(logn)~O(n)(递归占用的栈空间)。最优的情况下空间复杂度为O(logn) :每一次都平分数组的情况。最差的情况下空间复杂度为:O( n ) :退化为冒泡排序的情况。

优化选取枢纽

这里我们重新谈谈选取枢纽的问题。正如上面代码的选取方式,选取序列的第一个元素存在着潜在的性能瓶颈。如果待排序的序列为基本有序,将会导致上面描述的最坏情况发生,因此进行改进。其中较为常见为三数取中法。即取三个关键字先进行排序,再将中间数作为枢纽。一般是取左端、右端和中间三个数。

int Partition(int arr[],int low,int high){    int pivotkey;    int mid = (low + (high - low) / 2);//计算中间数下标    if(arr[low] > arr[high])        swap(arr,low,high);    if(arr[mid] > arr[high])        swap(arr,mid,high);    if(arr[mid] > arr[low])        swap(arr,mid,low);    //将三个数的中间值放到第一个位置    pivotkey = arr[low];    //........}
void swap(int arr[],int i,int j)//交换{       arr[i] ^= arr[j];    arr[j] ^= arr[i];    arr[i] ^= arr[j];}

对QSort实现尾递归优化

void QSort(int arr[],int low,int high){    int pivot;    while(low < high)    {        pivot = Partition(arr,low,high);        QSort(arr,low,pivot-1);        low = pivot + 1;    }}

将if部分改为while后,由于第一次递归后,变量low就没有用处了,所以可以将 pivot+1赋给low。再次进入循环后,即 Paratition(arr,low,high)效果等同于“QSort(arr,pivot+1,high)”,结果相同。但采用迭代而非递归的方法可以缩短堆栈深度(一次计算中会用到的栈空间的最大值),从而提高了整体性能。

这里写图片描述

这里写图片描述

参考书籍:《算法导论》 《大话数据结构》

原创粉丝点击