标签:
在前面几章中,分别介绍过冒泡排序,插入排序,选择排序以及归并排序。而在介绍散列技术时,也曾介绍过桶排序、计数排序以及奇数排序。在讨论优先级队列时,也结合堆这种结构介绍过堆排序以及更为通用的锦标赛排序。
这一章将学习若干种更高级的排序算法并讨论与之相关的几个衍生问题。
快速排序的核心思想是分治法。对于任何一个待排序序列,将它们分为前后两个子序列,并对这两个规模更小的子序列递归的实施排序。
快速排序和归并排序都是分治法的典型案例,但二者又有很大的区别。对于快速排序来说,子问题之间的独立性更为鲜明,要求前一序列中的任何元素在数值上都不得超过后一序列中的任意元素。如果这条件的确满足,那么在分别递归的对前一序列和后一序列进行排序之后,只要将二者简单的串接起来,也就自然的得到了整体的有序序列,从而完成最初的排序任务。
快速排序核心的任务与难点在于如何完成子任务或子序列的划分。归并排序恰好相反,其计算量以及难点都在于如何将子任务的解进行合并。
为了对子序列进行划分,我们需要一个“轴点”,这个轴点是一个已经就位的元素,它将待排序序列划分为两个待排序子序列。
构造轴点的步骤如下:
排序过程主要在于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。
在初次构造轴点完成后,依次对子序列进行又一次轴点构造。递归此过程,直至子序列为单个元素,此时整个序列排序完成,递归退出。由此可以看到,快速排序可以视作不断找出就位元素“轴点”的过程。
快排有很多实现方式,这里选取的实现与前面不同的是,不是按照L、U、G的排列方式,而是L、G、U的排列。当然基本步骤是一样的。
代码:
快速排序GIF演示(来源Wikipedia)
选取Selection是由排序衍生和推广而来的一类问题,这类问题的共同特点是需要从一组大小可以相互比较的元素中找到某一个特殊的元素。比如找到其中由小而大列于特定次序位置上的一个元素,或者找出其中在数值大小上恰巧列于中间的那个元素。如果已经得到了整个数据集所对应的排序序列,以上问题自然都可以迎刃而解。然而正因为排序计算自身的高复杂度,我们在此不得不绕开它并转而寻找更为有效的算法。
我们设定一个前缀P,它满足一个条件:其中一半的元素x是相同的。那么在去掉这个前缀P之后,向量A剩下的部分A-P与原向量A具有相同的众数。那么,我们可以通过寻找A-P的众数来达到缩减序列规模的目的。
具体实现思路是:从A的首元素x出发不断扫描,一旦达到满足条件的P,则将P删除。并反复迭代这个过程,得到的最终得x就是原向量的众数。
算法实现如下
这种算法的时间复杂度为O(n),空间复杂度为O(1)。
借用快速排序中的思路,在序列中构造一个“轴点”。该轴点的位置是序列已排序之后的位置。如果该轴点大于指定的k,说明S[k]在L中,我们可以删掉G。反之,S[k]在G中,我们可以删掉L。这也是一个使得问题规模缩减的“减而治之”的过程。
此算法的时间复杂度在最坏情况下为O(nlogn),显然不能满足要求。我们需要对其进行优化。
linearSelect是quickSelect的优化算法,它的效率可以达到线性。这个算法设定了一个比较小的常数Q。其选取步骤如下:
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的矩阵。
以步长序列为{8,5,3,2,1}为例。每一次先将序列转换成列长为Wk的矩阵,并对矩阵逐列排序。排序完成后再按行转换回单行序列。
可以看到,在经过每一步之后,序列的有序性都得到了提高,并最终得到一个排序完成的序列。
我们也可以发现,最后一次相当于对整个序列进行排序,那么前k-1次排序的意义何在呢?这其实就是希尔排序的核心思想所在。
在矩阵转换过程中,我们并不需要引入二维向量的数据结构。只需在逻辑上通过向量元素的秩的计算,即可将一维向量转换成i维向量。
每一次w-sorting都使得序列的有序性得到了提高,采用插入排序对矩阵逐列排序是最合适的。因为插入排序对输入序列的敏感性:输入序列逆序对越少,性能越高。
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。我们就知道,三毛五的邮资是无法由四分和一毛三的邮票凑齐的,而所有大于三毛五的邮资都可以凑齐。
可以看出,希尔排序对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