zoukankan      html  css  js  c++  java
  • 排序算法之快速排序(Quicksort)解析

    一.快速排序算法的优点,为什么称之为快排?

    Quicksort是对归并排序算法的优化,继承了归并排序的优点,同样应用了分治思想。

    所谓的分治思想就是对一个问题“分而治之”,用分治思想来解决问题需要两个步骤:

    1.如何“分”?(如何缩小问题的规模)

    2.如何“治”?(如何解决子问题)

    快排的前身是归并,而正是因为归并存在不可忽视的缺点,才产生了快排。归并的最大问题是需要额外的存储空间,并且由于合并过程不确定,致使每个元素在序列中的最终位置上不可预知的。针对这一点,快速排序提出了新的思路:把更多的时间用在“分”上,而把较少的时间用在“治”上。从而解决了额外存储空间的问题,并提升了算法效率。

    快排之所以被称为“快”排,是因为它在平均时间上说最快的,主要原因是硬件方面的,每趟快排需要指定一个“支点”(也就是作为分界点的值),一趟中涉及的所有比较都是与这个“支点”来进行比较的,那么我们可以把这个“支点”放在寄存器里,如此这般,效率自然大大提高。除此之外,快排的高效率与分治思想也是分不开的。

    二.算法思想

    按照快排的思想,对一已知序列排序有如下步骤:

    1.指定“支点”

    注意,是“指定”,并没有明确的约束条件,也就是说这个支点是任意一个元素,一般我们选择两种支点:当前序列首元,或者随机选取

    两种方式各有优劣,前者胜在简单,但可能影响算法效率

    快排中,支点的最终位置越靠近中间位置效率越高,读起来可能有点怪怪的,注意支点是一个值(具体元素),而不是字面意思的位置,当支点在最终序列中的位置靠前或者靠后时算法效率都不高(类似于“最坏情况”)

    因此,后者在一定程度上减少了最坏情况的发生次数,但随机选取需要耗费额外的时间

    所以在具体应用中一般采用第一种方式来指定“支点”,也就是直接把当前序列的首元作为“支点”

    2.进行一趟快排

    快排中,一趟操作的最终目的是把“支点”放到它应该去的地方,举个例子,已知序列{7, -1, 5, 23, 100, 101},那么第一趟快排的结果是{_, _, 7, _, _, _}

    可以看到,首元(支点)已经去了它该去的地方(在最终的结果序列中,7就在中间位置,没错吧)

    3.对子序列进行快排

    第2步不仅确定了7的最终位置,还把原序列自然地划分为两个子序列{_, _}和{_, _, _},这里用"_"代替具体的数值,因为我们也不知道第2步的结果具体是什么,除非真正地做一趟快排,当然,在这里不必要,下面会有针对具体例子的详细解释

    很自然的我们想到了对子序列进行同样的操作,然后对子序列的子序列再进行同样的操作...递归

    当所有的子序列长度都为1的时候,排序结束

    三.具体实例

    现有一序列{9, 0, 8, 10, -5, 2, 13, 7},我们用快速排序算法来对其排序

    首先,声明一些特殊的记号,便于描述

    a, 数字后面跟的大写字母表示指针,例如2.5P表示指针P指向元素2.5所在的位置

    b, @表示垃圾数字,也就是说,当前位置是几都无所谓,不必纠结于此,后面会有具体解释

    c, _表示该位的元素与上一行一样(_表示不变)

    -------

    P.S.想要真正弄明白的话,现在拿出纸和笔吧,光靠眼睛是绝对不够的

    下面正式开始一趟快排的过程解析

    【1】9L  0  8  10  -5  2  13  7H

    【2】7  0L  _  __  __  _  __  @H

    【3】_  _  8L  __  __  _  __  __

    【4】_  _  _  10L  __  _  __  __

    【5】_  _  _  @L  __  _  13H  10

    【6】_  _  _  __  __  2H  13  __

    【7】_  _  _  2  -5L  @H  __  __

    【8】_  _  _  _  -5  @HL  __  __

    【9】_  _  _  _  __  9HL  __  __

    解释:

    1.第一行是初始状态,快排需要两个指针L和H(表示低位Low,高位High),一个临时变量temp

    初始时,低位指针L指向首元9,高位指针H指向尾元7,temp=首元9(temp就是所谓的”支点“)

    2.进行如下操作:(先不要问为什么)

    比较*H与temp,若*H大,则向前移动H继续比较,若*H小,则*L = *H,*H = @(H指向的值变成垃圾数字了),向后移动L

    因为7 < 9,所以把L指向的9变成7,把H指向的7变成垃圾数字,向后移动L指针,得到第二行的结果

    3.进行如下操作:(先不要问为什么)

    比较*L与temp,若*L小,则向后移动L继续比较,若*L大,则*H = *L,*L = @(L指向的值变成垃圾数字了),向前移动H

    因为0 < 9,所以向后移动L,得到第三行的结果

    4.因为8 < 9,同上

    5.因为10 > 9,所以把H指向的垃圾数字@变成10,把L指向的10变成垃圾数字,向前移动H指针,得到第5行的结果

    6.因为13 > 9,所以向前移动H指针,得到第6行的结果

    7.因为2 < 9,所以把L指向的垃圾数字@变成2,把H指向的2变成垃圾数字,并向后移动L指针,得到第7行的结果

    8.因为-5 < 9,所以向后移动L指针得到第8行的结果

    9.进行如下操作:(先不要问为什么)

    若L = H,则*L = *H = temp,一趟快排结束

    因为L指针与H指针重合了,所以把L指向的垃圾数字@变成temp的值9,一趟结束

    至此,我们确定了支点9的最终位置,给定序列也被自然的分为两个子序列{7, 0, 8, 2, -5}和{13, 10},对子序列进行相同的操作,最终能够得到有序序列

    -------

    下面来解释上面提到的三组操作

    简单的说,上面的三组操作上为了找出temp的最终位置,每一步都保证L前面都比temp小,H后面都比temp大。所以,H与L重合的位置上的元素只能是temp的值了

    上面提到的三组操作可以简化成下面的几条规则,便于记忆:

    1.L指向的值小则L移动,反之赋值并移动指针

    2.H指向的值大则H移动,反之同上

    3.若HL重合,则赋值temp

    4.H,L轮流与temp比较,规则是赋值一次后算一轮结束(所以一开始也可以从L与temp开始比较,下一轮H与temp比,再下一轮...)

    P.S.至于怎么移动,自然是低位指针只能向高位移动,反之亦然。至于赋值后移动哪个指针,当然是另一个指针(非当前指针)了

    四.总结

    排序算法的应用都需要结合具体环境来考虑,例如若给定序列部分有序,自然是折半插入算法最快...

    快速排序也并不是最好的,它的”快“是建立在综合考虑的基础上,具体情况则不一定

    快速排序也不是万能的,例如当给定序列规模很小时,选择排序就要比快排好很多

    另外,常见的排序算法有:

    1.桶排序/箱排序(Bucketsort)

    2.基数排序(Radixsort)

    3.插入排序(Insertsort)

    4.选择排序(Selectsort)

    5.归并排序(Mergesort)

    6.快速排序(Quicksort)

    7.堆排序(Heapsort)

  • 相关阅读:
    Codeforces Round #344 (Div. 2) C. Report 其他
    Codeforces Round #344 (Div. 2) B. Print Check 水题
    Codeforces Round #344 (Div. 2) A. Interview 水题
    8VC Venture Cup 2016
    CDOJ 1280 772002画马尾 每周一题 div1 矩阵快速幂 中二版
    CDOJ 1280 772002画马尾 每周一题 div1 矩阵快速幂
    CDOJ 1279 班委选举 每周一题 div2 暴力
    每周算法讲堂 快速幂
    8VC Venture Cup 2016
    Educational Codeforces Round 9 F. Magic Matrix 最小生成树
  • 原文地址:https://www.cnblogs.com/ayqy/p/3862938.html
Copyright © 2011-2022 走看看