zoukankan      html  css  js  c++  java
  • 对快速排序的简单分析

    开篇

    在实际的过程中,总需要对一些数据进行排序,在众多的排序算法中,快速排序是较为常用的排序算法之一。而网上对于快速排序的中文资料还不是很全。写这篇博文主要记录一些自己对于快速排序的了解,以及对快速排序的性能的分析。我将在这里记录下我对快速排序的认识和学习过程 ,用尽可能简单明了的叙述来阐述我的理解。

    快速排序基于算法中很重要的思想是 分治。所以会先介绍一下分治思想,然后对算法原理进行介绍,接着会分析算法的性能并对算法作进一步的讨论。

     注:为了便于说明问题,本博文中会用到部分《introduction to algorithm》中的图片。

    关键词:快速排序、分治、递归

    “大事化小”——从分治说起

    分治? 

    分治法是算法中常用的策略之一,很多算法都是基于分治法的,今天要说的快速排序也一样。为了能更好的理解快速排序,先简单的介绍一下分治法。

    顾名思义,分治,可理解为分而治之。就是把原问题(递归地)分解为多个子问题(一般是和原问题本质相同的问题,只是规模上的缩小,如果现在不能理解请看后文解释),解决这些子问题,合并其结果,获得原问题的解。

    简单的说就是“大事化小”    把复杂的问题分为多个简单问题,解决了这些简单问题,原问题也就随之解决了。

    如何分治

    从上面的分析中可知道,用分治的思想解决问题的步骤大致为:

    分解(Divide):将原问题分为一系列子问题

    解决(Conquer):递归的解决子问题。如果子问题足够小,直接解决子问题

    合并(Combine):将子问题的结果合并为原问题的

    借助下图,可更清晰的了解分治的思想:

    如上图所示,原问题是规模为 n 的问题,在树的第一层,把问题分为规模为n/2的两个子问题,如果解决了这两个子问题,把它们合并就能得到原问题的解。

    现在来看其中的一个子问题,为了解决他们,又把它分为两个规模更小的问题n/4。解决了规模为n/4的问题,合并之就能得到规模 n/2 的问题的解。

    按照上面的思想,把原问题递归的分解为规模小的问题,然后合并之就能得到原问题的解。

    到现在对分治的思想应该有了一定的认识,其实分治思想可谓博大精深,不是三言两语能讨论清楚的,这里这说明一个基本思想,就不做深入讨论了。

    快速排序原理

    基本思想:

    上文已经说过快速排序是基于分治思想的,把问题的规模递归的变小,然后依次解决子问题,自后得到原问题的解。

    既然是基于分治思想,那么快速排序步骤也和分治一样:

    我们假设原问题是要对数组 A[p,r] 中的数据进行排序

    分解(Divide):

    将数组 A[p,-------,r] 分为两个数组 A[p,....,q-1 ] 和 A[q+1,.....r]  使得 数组 A[p,....,q-1 ]中的每一个数都小于q  ,数组 A[p,....,q-1 ]中的每一个数都大于q。

    其实这步的关键就是找到那个 q 然后遍历数组,把小于 q 的元素放在 q 的左边,大于q 的元素放在右边。这样就使得 q 左边的元素都小于 q  右边的元素。

    当问题被分解的足够小,当 q 左边只有一个元素 a ,q 右边也只有一个元素 b 的时候,那么  a  q   b 就是一个有序数组,其实这也就完成了一次排序。

    解决(conquer):

    (递归调用快速排序),对数组 A[p,....,q-1 ] 和 A[q+1,.....r]  进行排序。对于其中的一个数组将被分为更小的数组,直到数组内数据有序。

    随着问题规模的减小,数组内的元素也在减小,当数组内元素只有3 个的时候,它的下一次分解会产生两个规模为 1 的问题,这也就是上面说的“数组内部有序”的状态了。

    下面是一个图示:

    假设问题一直被分解,直到上图中数组有三个元素的状态 ,这个状态再分解就得到箭头下方所指的状态,可以发现,这个状态已经是有序的了,直接合并,就能得到有序序列。

    合并(combine):

    如上文所说,两个数组都是经过排序的(其实每个数组内只有一个元素了,所以也不存在什么排序),所以直接合并就能得到有序的数组。

    算法说明

    算法

    下面是快速排序的算法说明:

    快速排序的函数是"QUICKSORT()"该函数有三个参数,

    第一个参数A 表示要排序的数组,也就是给该函数传入要排序的数组的指针。

    第二个参数p 和 第三个参数 r 标记出了要排序的数组的范围,即:这函数将数组A 的第p个到第 r 个参数排序。

    下面是对这个算法的分析:

    算法的第1行判断要排序的数组是范围是否合法,p 表示的是开始的位置, r表示的是结束的位置,所以只有p<r 才能进行排序。

    第2 行:其实就是一个问题分解的过程,从数组中选一个元素q(可能是任意选择的,也可能存在其他的选择方式);

    然后将数组A中的元素分为两部分:小于q的部分[p....q-1]放在q的左边,和大于q的部分[q+1....r]放在q 的右边。至此,原来要排序的数组A[p...r]被分为了两部分。

    只要按照上面所做的,再对这两个新产生是数组进行排序就行了。也就是第3 和第4行所做的事情。

    分治思想的体现:

    从中也可以看出分治的思想,算法中的第2行通过q 把原问题分解为两个规模较小的问题,注意:只是规模缩小了,问题的本质并没有改变,对于被缩小后的问题,还是要进行排序。第3 、4 用同样的方法来处理问题。因为问题的本质没变,只是规模的缩小,所以还是可以调用解原问题的那个函数,只要修改参数就可以了。这时候我们就能更好的理解函数"QUICKSORT"了,它有三个参数,后面的两个参数正是用来控制问题规模的。可能有人已经看出来了,这里还体现出递归的思想:在解决的过程中调用自身。当然了 ,对于递归这里就不做深入讨论了。

    关键部分:

    在上面的算法中,其实最关键的还是第2 行的那个Partition()。正是这个函数,将问题分解成了问题本质不变而规模变小的子问题,这个函数的实现也是这个算法的关键。

    基本思路:

    Partition(A,p,r)的目的是将从数组A[p....r]中选一个数q,将小于q的元素放在q左边,大于q的放在右边。可以先自己思考 一下这个算法能怎样实现。

    一种简单的想法是:申请一个大小为(r-q)的空间B[  ],遍历数组A[p...r],将每一元素和q比较,如果小于q 就从左边放入新申请的空间中,如果大于,就从右边放入。

    最后把A[p...r]中的内容用b[  ]中的内容替换。当然,这是最直观的思维,这样做明显的空间和时间复杂度都不好。所以这不是快速排序中所采用的策略。

    下面是快速排序所使用的Partition(A,p,r)的实现:

    我的建议是:最好自己先分析一下这个算法,也很值得分析。我觉得它对空间和时间的处理真的很妙。画一个图会对分析很有帮助。

    下面对这个函数的实现做一些简单分析:

    第1行,函数选择x=A[r]来作为分界点,也就是上面所讨论的q。通过它把数组分为两部分。

    第2行,定义了变量i,i  是一个维护“小于区”的指针。i 左边的元素都是小于分界点x 的元素。每当发现一个小于x的元素,就把它放在i 的后面,同时 i++;

    第3行,for 循环并定义变量 j ,j 遍历整个数组,并和分界点x 进行比较(第4行)如果元素A[j]<=x,那么就把这个元素加入到小于分界点x的区域。同时i ++,

    具体的实现就是5、6行的功能。

    第7行,已经完成遍历,小于分界点x 的元素都在i 左边的区域中,右边的区域都是大于x 的,所以只要将分界点元素加入到他们中间即可。

    实例是学习知识的最好途径!

    本例将描述该算法对一个包含8个 元素的数组的操作过程。具体的操作过程如下图所示,函数中的变量在途中都已标出。

    可结合算法和上文的算法分析来看这个图,思路就会变得清晰了。

    算法性能分析

    通过上面的算法分析已经知道,如果能“尽快地”把原问题分为规模小的问题(可直接求解的问题),那么它的效率是比较好的。如果每次划分都能平均的将规模缩小一半,那么这种划分就是能最快到达目的的。而这种划分的结果直接和分界点 q 相关,如果每次划分时的q 选的足够好,也就是小于q 的元素个数等于 大于 q 的元素个数,那么这将是最好的情况。

    当然现实中的情况不可能是这样,因为q 的选择往往是随机的。而且如果专门为选择一个合适的 q 又用一个函数来实现,那么算法的效率将得不到保障。

    总结下上面所说的就是:快速排序的运行时间与划分是否对称有关。

    最坏情况:

    最坏情况也就是要划分最多次数。只要每次划分都把规模为n的问题分解为 n-1 和 1 。这种情况每一次的划分都出现这种极不对称的划分,它的效率将是最低的。

    假设对规模为n 的问题的划分代价为f(n).

    那么,对于规模为n 的问题的时间为:T(n)=T(n-1)+T(1)+f(n)。

    T(1): 数组中只有一个元素,已经是最小,不用继续分解规模,所以划分时间f(1)=0;

    所以 T(n)=T(n-1)+f(n)

    这样看来, 如果将每一层的递归代价加起来,每分解一次,其时间复杂度为f(n*n)   (n的平方)

    如果每一层的划分都是极不对称的,那么算法的运行时间就是:f(n*n) 。

    最佳情况:

    上文中已经说过最佳情况,对于规模为n 的问题,最佳情况分为两个规模为n/2 的问题。表达其运行时间的递归式为:

    T(n)<=2T(n/2)+f(n)

    该递归式的解为:T(n)=O(n lg n).

    划分的两端都是对称的,所以从渐进意义上看,算法运行的就更快了。

    平衡的划分:

    最佳情况和最坏情况都是实际情况中的两个极端,在实际中比较少见,所以这里讨论一下两者的折中。这里假设每次划分的过程总是产生9:1 的划分,应该比较“接近”实际情况了。

    这时,快速排序的时间递归式为:

    T(n)<=T(9n/10)+T(n/10)+cn

    这里,我们显示的写出了f(n)中的常数c。下图显示了这个递归过程的递归树:

    请注意这个树的每一层代价都是cn ,直到图中的倒数第三行未知,在此之前各层的代价至多为cn,后面的代价总是小于cn,

    所以才有T(n)<=T(9n/10)+T(n/10)+cn。

    这种情况下,快排的复杂度总为O(n lg n).从渐进意义上来说,这和平均划分的效果是一样的。

    小结

     关于快速排序的具体算法是现在网上有很多,这里就不写出来了,关键的是掌握其中的思想。

    即:递归、分治。

    参看资料:算法导论

    如有转载请注明出处:http://www.cnblogs.com/yanlingyin/

    一条鱼、尹雁铃@ 博客园 2012-4-16

     E-mail:yanlingyin@yeah.net

  • 相关阅读:
    ASP.NET在禁用视图状态的情况下仍然使用ViewState对象【转】
    Atcoder Regular Contest 061 D Card Game for Three(组合数学)
    Solution 「CERC 2016」「洛谷 P3684」机棚障碍
    Solution 「CF 599E」Sandy and Nuts
    Solution 「洛谷 P6021」洪水
    Solution 「ARC 058C」「AT 1975」Iroha and Haiku
    Solution 「POI 2011」「洛谷 P3527」METMeteors
    Solution 「CF 1023F」Mobile Phone Network
    Solution 「SP 6779」GSS7
    Solution 「LOCAL」大括号树
  • 原文地址:https://www.cnblogs.com/yanlingyin/p/2441979.html
Copyright © 2011-2022 走看看