码迷,mamicode.com
首页 > 编程语言 > 详细

第十二章·排序

时间:2016-05-20 06:18:48      阅读:336      评论:0      收藏:0      [点我收藏+]

标签:

在前面几章中,分别介绍过冒泡排序,插入排序,选择排序以及归并排序。而在介绍散列技术时,也曾介绍过桶排序、计数排序以及奇数排序。在讨论优先级队列时,也结合堆这种结构介绍过堆排序以及更为通用的锦标赛排序。

这一章将学习若干种更高级的排序算法并讨论与之相关的几个衍生问题。

快速排序

快速排序的核心思想是分治法。对于任何一个待排序序列,将它们分为前后两个子序列,并对这两个规模更小的子序列递归的实施排序。

快速排序和归并排序都是分治法的典型案例,但二者又有很大的区别。对于快速排序来说,子问题之间的独立性更为鲜明,要求前一序列中的任何元素在数值上都不得超过后一序列中的任意元素。如果这条件的确满足,那么在分别递归的对前一序列和后一序列进行排序之后,只要将二者简单的串接起来,也就自然的得到了整体的有序序列,从而完成最初的排序任务。

快速排序核心的任务与难点在于如何完成子任务或子序列的划分。归并排序恰好相反,其计算量以及难点都在于如何将子任务的解进行合并。

轴点

为了对子序列进行划分,我们需要一个“轴点”,这个轴点是一个已经就位的元素,它将待排序序列划分为两个待排序子序列。
技术分享
构造轴点的步骤如下:

  1. 选取序列的首元素m作为轴点的“候选”。
  2. 指定两个指针“lo”和”hi”。将序列分为三个部分:L、U、G。其中L的元素都小于等于m,G都大于等于m。U中元素大小未知,初始状态时,整个序列即为U,L、G为空。
  3. 将lo和hi依次向内侧移动从而彼此靠近。lo和hi每移动一次,都会将U中的一个元素与m进行比较,如果小于m,则将其放入L,否则放入G。循环此过程,直至lo和hi重合,此时U为空,其所有元素归于L或G中。
  4. 将m置于lo和hi重合处。此时,m成为一个真正的轴点,其所处位置就是排序之后的位置。

排序过程

技术分享
排序过程主要在于lo和hi的移动以及伴随的L、G的扩充。

首先选取6为轴点候选,此时首元素在逻辑上视为空。然后扩充G,我们看到hi指向的元素7大于6,因此将7归入G中,hi指向新元素1。因为1小于6,我们将1归入L中,lo指向新的元素3。因为3小于6,将3归入L,lo指向8。8大于6,因此将8归入G,hi左移指向5。继续重复此过程,直至lo和hi都指向同一个空元素,此时把6放入次处。排序过程完成。可以看到,6之前的所有元素都不大于6,其后的元素也不小于6。

递归

在初次构造轴点完成后,依次对子序列进行又一次轴点构造。递归此过程,直至子序列为单个元素,此时整个序列排序完成,递归退出。由此可以看到,快速排序可以视作不断找出就位元素“轴点”的过程。

性能分析

  1. 由排序过程可以看出,快速排序是不稳定的,重复元素在排序后可能顺序颠倒。
  2. 快排所需的空间是常数,只需两个指针和一个轴点候选的存储空间。
  3. 归并排序的时间复杂度为O(nlogn),因为归并排序划分的子任务在规模上都相当,接近N/2.因此只需logn次子过程。而快排并不能保证划分的子任务都为N/2,因为轴点的选择是随机的。在最坏情况下,每次划分的子序列都有一个大小为0,另外一个为N-1。这样,时间复杂度是O(n^2)。平均情况而言,快排的时间复杂度也为O(nlogn)。

算法实现

快排有很多实现方式,这里选取的实现与前面不同的是,不是按照L、U、G的排列方式,而是L、G、U的排列。当然基本步骤是一样的。
技术分享
代码:
技术分享

快速排序GIF演示(来源Wikipedia
技术分享

选取

选取Selection是由排序衍生和推广而来的一类问题,这类问题的共同特点是需要从一组大小可以相互比较的元素中找到某一个特殊的元素。比如找到其中由小而大列于特定次序位置上的一个元素,或者找出其中在数值大小上恰巧列于中间的那个元素。如果已经得到了整个数据集所对应的排序序列,以上问题自然都可以迎刃而解。然而正因为排序计算自身的高复杂度,我们在此不得不绕开它并转而寻找更为有效的算法。

  • K-selection:在任意一组可比较的元素中,由小到大,找到次序为k者。
  • median:长度为n的有序序列S中,找到被称为中位数的元素S[?\n/2?](n/2向下取整)。
    median是K-selection的一种特例,也是其中最难的一种情况。
  • majority:众数。在无序向量中,若有一半以上的元素m相同,则该元素m称为众数。
    元素m首先必定是该向量中的中位数。若我们先取到中位数,则可遍历该序列统计数目以判断是否也是众数。

选取众数

技术分享
我们设定一个前缀P,它满足一个条件:其中一半的元素x是相同的。那么在去掉这个前缀P之后,向量A剩下的部分A-P与原向量A具有相同的众数。那么,我们可以通过寻找A-P的众数来达到缩减序列规模的目的。

具体实现思路是:从A的首元素x出发不断扫描,一旦达到满足条件的P,则将P删除。并反复迭代这个过程,得到的最终得x就是原向量的众数。

算法实现如下
技术分享
这种算法的时间复杂度为O(n),空间复杂度为O(1)。

K-selection

quickSelect

借用快速排序中的思路,在序列中构造一个“轴点”。该轴点的位置是序列已排序之后的位置。如果该轴点大于指定的k,说明S[k]在L中,我们可以删掉G。反之,S[k]在G中,我们可以删掉L。这也是一个使得问题规模缩减的“减而治之”的过程。
技术分享
此算法的时间复杂度在最坏情况下为O(nlogn),显然不能满足要求。我们需要对其进行优化。

linearSelect

linearSelect是quickSelect的优化算法,它的效率可以达到线性。这个算法设定了一个比较小的常数Q。其选取步骤如下:

  1. 递归基:如果向量A的长度小于Q,则选用其他平凡的选取算法选取S[k]。
  2. 将向量A等分为n/Q个子序列
  3. 对每个子序列进行排序并得到其中位数
  4. 递归调用linearSelect获得这n/Q个中位数的中位数M
  5. 将向量A分为三部分:小于M的元素放入子集L,等于M的元素放入子集E,大于M的元素放入子集G
  6. 如果要查找的S[k]位于L或G,则删除E、G(L),并递归调用linearSelect
  7. 如果要查找的S[k]位于E,则直接返回E中的元素M,查找结束。

技术分享
linearSelect性能分析:
第2步为O(n)
第3步为O(n)= O(1)*O(n/Q),每一个子序列长度为常数Q,其排序的时间复杂度为O(1)。
第4步为T(n/Q),这一步为递归,问题规模缩减到了n/Q
第5步为O(n),对向量A进行一遍扫描即可
第6步为T(3/4n),这一步也是递归,问题规模缩减到了最多为原来的3/4。

因此,T(n) = cn + T(n/Q) + T(3/4n) ,c为常数。
若取c = 5 ,则T(n)= cn + T(n/5) + T(3/4n) = O(20cn) = O(n)

希尔排序

希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。希尔排序是非稳定排序算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:

  • 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率
  • 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位
    ——Wikipedia

希尔排序的特点是将序列视为一个矩阵,逐列进行排序,并不断缩减列的规模,同时增加行的规模。最终得到一个列数为1的矩阵。
技术分享

  1. 每一次逐列排序称为w-sorting,排序完成的列称为w-ordered。最后只有一列的排序称为1-sorting
  2. 此过程中一系列矩阵的宽度Wk,Wk-1以及一直到W3,W2和W1。这些宽度在一起构成了步长序列。步长序列必须满足单调递增性,以及首项必须为1.
  3. 除了单调递增性以及首项必须为1,步长序列没有其他要求。因此,采用不同的步长序列,希尔排序的性能也有所变化。因此,希尔排序可以视为一类算法。

排序过程

以步长序列为{8,5,3,2,1}为例。每一次先将序列转换成列长为Wk的矩阵,并对矩阵逐列排序。排序完成后再按行转换回单行序列。
技术分享
技术分享
技术分享
技术分享
技术分享
可以看到,在经过每一步之后,序列的有序性都得到了提高,并最终得到一个排序完成的序列。

我们也可以发现,最后一次相当于对整个序列进行排序,那么前k-1次排序的意义何在呢?这其实就是希尔排序的核心思想所在。

矩阵转换

在矩阵转换过程中,我们并不需要引入二维向量的数据结构。只需在逻辑上通过向量元素的秩的计算,即可将一维向量转换成i维向量。
技术分享

w-sorting:插入排序

技术分享
每一次w-sorting都使得序列的有序性得到了提高,采用插入排序对矩阵逐列排序是最合适的。因为插入排序对输入序列的敏感性:输入序列逆序对越少,性能越高。

SHELL序列的缺点

SHELL序列是希尔排序的发明者提出的步长序列,这个序列的项是以2为倍数的等比序列。
技术分享
这个序列在最坏情况下的时间复杂度为O(n^2)。其低效原因在于:由于每一个Wk都是偶数,那么在每一次排序过程(除了最后一次)中,处于奇数列和偶数列的元素都不会有交集。也就是说,秩为奇数的元素只和同为奇数的元素排序,秩为偶数的元素也只和同为偶数的元素排序,这样必然导致在最后一次排序中,存在大量的逆序对,从而降低了效率。

从SHELL序列的分析中,我们看到低效的原因是步长序列的设计。什么样的序列才是好序列呢?答案是相邻项要互素,这样可以最大程度上避免上一次排序过程的重复。

邮资问题

假设在一个国家,寄送一封平信需要五毛钱,而寄送一张明信片只需三毛五。这个国家所发行的邮票只有面值为四分以及一毛三的两种。如果你需要邮寄一封平信或者明信片,你是否能够恰好用这两类邮票凑出所对应的邮资?

我们很简单的计算就可以得到答案:只需六张面额为四分的邮票外加两张面额为一毛三的邮票,就恰好可以凑出平信所对应的邮资五毛钱。然而对于明信片,没有方案可以贴出明信片所对应的邮资三毛五。由此我们可以看到使用特定的一组邮票,对于有些邮资可以恰好的凑出,而有些邮资无论如何都不能凑出。

线性组合

在上面的例子中,我们发现所有邮票可能的组合方式为4m + 13n。这个表达式称为“线性组合”,由这个组合可以得出所有可以凑出的邮资。

我们用更具一般意义的表达式:f = mg + nh。这里我们更关注不能由线性组合得出的数,将这些数的集合定义为N(g,h),且g和h互素。此集合中的最大值记为x(g,h)。

由数论的结论,我们可以得出h和g互素时,x(g,h) = (g - 1 ) * (h - 1 ) -1 = gh - g - h。

比如,x(4,13) = 35。我们就知道,三毛五的邮资是无法由四分和一毛三的邮票凑齐的,而所有大于三毛五的邮资都可以凑齐。

定理K

  • h-ordered:在某个序列中,任何一对距离为h的元素都保持前小后大的次序。也就是以h为间隔是有序的。

可以看出,希尔排序对Wk = h的矩阵逐列排序之后产生的序列,即符合h-ordered.而这个过程在希尔排序中被称为h-sorting。因此,任何一个序列在h-sorting之后,必然是h-ordered.

假设在前一轮的排序中,序列达到了h-rodered,在后一轮的排序中,序列又达到了g-ordered。那么,在g-ordered中,是否仍然保持h-ordered呢?答案是肯定的。Knuth在著名的ACP书中给出了证明(Vol.3 p.90)。

如果一个序列既是h-ordered,也是g-ordered,那么也被称为(g,h)-ordered。这意味着:序列中,任意一对距离为(g + h)的元素都保持有序性。更进一步来说,g和h的线性组合也满足:(mg + nh)-ordered。由此我们得到一个结论:凡是间距可以表示为线性组合的任何一对元素必然是顺序的。

接下来我们分析序列中秩为i的元素
技术分享
如果序列中已经是(g,h)-ordered,并且g,h互素。那么对于i而言,与其距离(g - 1)(h - 1)以外的元素都是有序的。反过来说,可能存在逆序的元素,只存在于与i距离(g - 1)(h - 1)以内。

而随着希尔排序的进行,g,h都不断减小,(g - 1)*(h - 1)也不断减小,也就是说逆序对的数目不断减少。这也是我们采用插入排序的核心原因。

由以上分析可以得到结论,步长序列项互素可以使得序列中逆序对尽可能的少,从而提高了希尔排序的效率。

针对希尔排序设计的更为优化的步长序列有:PS序列,Pratt序列以及Sedgewick序列等。

希尔排序动画演示:http://student.zjzk.cn/course_ware/data_structure/web/flashhtml/shell.htm
希尔排序实现代码参考:https://zh.wikipedia.org/wiki/%E5%B8%8C%E5%B0%94%E6%8E%92%E5%BA%8F#.E6.AD.A5.E9.95.BF.E5.BA.8F.E5.88.97

第十二章·排序

标签:

原文地址:http://blog.csdn.net/xiang_freedom/article/details/51458682

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!