博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
快速排序
阅读量:3735 次
发布时间:2019-05-22

本文共 2756 字,大约阅读时间需要 9 分钟。

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

快速排序与归并排序一样,也使用了分治思想。 下面对一个典型的子数组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)”,结果相同。但采用迭代而非递归的方法可以缩短堆栈深度(一次计算中会用到的栈空间的最大值),从而提高了整体性能。

这里写图片描述

这里写图片描述

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

你可能感兴趣的文章
路由器的搭建虚拟机上网及DHCP服务、dns解析
查看>>
虚拟机多种管理方式及常用命令
查看>>
kickstart自动化安装管理虚拟机
查看>>
linux系统的定时、延迟任务管理
查看>>
linux系统的磁盘管理方式
查看>>
管理lvm(Logical Volume Manager)
查看>>
yum源的配置及第三方软件仓库的管理、yum命令、rpm命令的使用
查看>>
关于ftp服务
查看>>
日志的管理
查看>>
linux系统的selinux管理
查看>>
linux系统的网络桥接配置及链路聚合
查看>>
关于DNS部署
查看>>
关于数据库管理mariadb
查看>>
类的内存模型(二)
查看>>
生产者消费者模型
查看>>
#剑指Offer Day1 单向链表中倒数第k个节点
查看>>
#剑指offer Day2 一类可以用“框架”快速搞定的二叉树问题
查看>>
#剑指offer Day3 一类 “ 斐波那契 ”问题
查看>>
#剑指offer Day4 一类 “ 双指针 ”问题
查看>>
#剑指offer Day5 # 分享两个题的其他解法
查看>>