即使你只是使用标准库中的排序函数,学习排序算法仍然有三大实际意义:
1.对排序算法的分析将有助于你全面理解本书中比较算法性能的方法;2.类似的技术也能有效解决其他类型的问题;3.排序算法常常是我们解决其他问题的第一步;更重要的是这些算法都很经典、优雅和高效;排序算法类的模板
package algorithm;import stdLib.In;import stdLib.StdOut;/** * 〈排序算法类的模板〉 */public class Example { public static void sort(Comparable[] a){ /* 请见后续算法 */ } /** * 判断v是否小于w */ private static boolean less(Comparable v, Comparable w){ return v.compareTo(w) < 0; } /** * 将数组中a[i]和a[j]交换 */ private static void exch(Comparable[] a, int i, int j){ Comparable t = a[i]; a[i] = a[j]; a[j] = t; } private static void show(Comparable[] a){ //在单行中打印数组 for(int i=0; i
这个类展示的是数组排序实现的框架。但是这段代码使我们的排序算法适用于任意实现了Comparable接口的数据类型。
排序成本模型:在研究排序算法时,我们需要计算比较和交换的数量。对于不交换元素的算法,我们会计算访问数组的次数。
算法2.1选择排序:
一种最简单的排序算法是这样的:首先,找到数组中最小的那个元素,其次,将它和数组的第一个元素交换位置(如果第一个元素就是最小元素那么它就和自己交换)。再次,在剩下的元素中找到最小的元素,将它与数组的第二个元素交换位置。如此往复,直到将整个数组排序。这种方法叫做选择排序,因为它在不断的选择剩余元素之中的最小者。
package algorithm;/** * 〈算法2.1 选择排序〉 */public class Selection { /** * 将a[]按升序排列 * @param a */ public static void sort(Comparable[] a){ int N = a.length; for(int i=0; i
命题A:对于长度为N的数组,选择排序需要大约N²/2次比较和N次交换。证明:通过查看代码我们可以更精确的得到,0到N-1的任意i都会进行一次交换和N-1-i次比较,因此总共有N次交换以及(N-1)+(N-2)+...+2+1=N(N-1)/2 ~ N²/2次比较。
总的来说,选择排序是一种很容易理解和实现的简单排序算法,它有两个很鲜明的特点:
1)运行时间和输入无关。为了找出最小的元素而扫描一遍数组并不能为下一遍扫描提供什么信息。这种性质在某些情况下是缺点,因为可能使用选择排序的人可能惊讶的发现,一个已经有序的数组或是主键全部相等的数组和一个元素随机排列的数组所用的排序时间竟然一样长!我们将会看到,其他算法会更善于利用输入的初始状态。2)数据移动是最少的。每次交换都会改变两个数组元素的值,因此选择排序用了N次交换——交换次数和数组的大小是线性关系。我们将研究的其他任何算法都不具备这个特征(大部分的增长数量级都是线性对数或是平方级别)
算法2.2插入排序
通常人们整理桥牌的方法是一张一张的来,将每一张牌插入到其他已经有序的牌中的适当位置。在计算机的实现中,为了给要插入的元素腾出空间,我们需要将奇遇所有元素在插入之前都向右移动一位。这种算法叫插入排序。
和选择排序不同的是,插入排序所需的时间取决于输入中元素的初始顺序。例如,对一个很大且其中的元素已经有序(或接近有序)的数组进行排序将会比对随机顺序的数组或是逆序数组进行排序要快得多。
package algorithm;/** * 〈算法2.2 插入排序〉 */public class Insertion { /** * 将a[]按升序排列 * @param a */ public static void sort(Comparable[] a){ int N = a.length; for(int i=1; i0 && less(a[j], a[j-1]); j--){ exch(a, j, j-1); } } } /** * 判断v是否小于w */ private static boolean less(Comparable v, Comparable w){ return v.compareTo(w) < 0; } /** * 将数组中a[i]和a[j]交换 */ private static void exch(Comparable[] a, int i, int j){ Comparable t = a[i]; a[i] = a[j]; a[j] = t; }}
命题B:对于随机排列的长度为N且主键不重复的数组,平均情况下插入排序需要~N²/4次比较以及~N²/4次交换。最坏情况下需要~N²/2次比较和~N²/2次交换,最好情况下需要N-1次比较和0次交换。证明:最坏情况下所有元素都需要移动位置,最好情况下都不需要。对于随机排列的数组,在平均情况下每个元素都可能向后移动半个数组的长度,因此交换总数是对角线之下的元素总数的二分之一。
总的来说,插入排序对于部分有序的数组十分高效,也很适合小规模数组。这很重要,因为这些类型的数组在实际应用中经常出现,而且它们也是高级排序算法的中间过程。我们会在学习高级排序算法时再次接触到插入排序。
算法2.3 希尔排序
接下来我们将学习一种基于插入排序的快速的排序算法。对于大规模乱序数组插入排序很慢, 因为它只会交换相邻的元素,因此元素只能一点一点地从数组的一端移动到另一端。例如,如果主键最小的元素正好在数组的尽头,要将它挪到正确的位置就需要N-1次移动。希尔排序为了加快速度简单地改进了插入排序,交换不相邻的元素以对数组的局部进行排序,并最终用插入排序将局部有序的数组排序。
package algorithm;/** * 〈算法2.3 希尔排序〉 */public class Shell { /** * 将a[]按升序排列 * @param a */ public static void sort(Comparable[] a){ int N = a.length; int h = 1; while(h < N/3){ h = 3*h + 1; } while(h >= 1){ //将数组变为h有序 for(int i=h; i=h && less(a[j], a[j-h]); j-=h){ exch(a, j, j-h); } } h = h/3; } } /** * 判断v是否小于w */ private static boolean less(Comparable v, Comparable w){ return v.compareTo(w) < 0; } /** * 将数组中a[i]和a[j]交换 */ private static void exch(Comparable[] a, int i, int j){ Comparable t = a[i]; a[i] = a[j]; a[j] = t; }}
希尔排序的思想是使数组中任意间隔为h的元素都是有序的。这样的数组被称为h有序数组。换句话说,一个h有序数组就是h个互相独立的有序数组编织在一起组成的一个数组。在进行排序时,如果h很大,我们就能将元素移动到很远的地方。为实现更小的h有序创造方便。用这种方式,对于任意以1结尾的h序列,我们都能够将数组排序。这就是希尔排序。
希尔排序更搞笑的原因是它权衡了子数组的规模和有序性。排序之初,各个子数组都很短,排序之后子数组都是部分有序的,这两种情况都很适合插入排序。子数组部分有序的程度取决于递增序列的选择。透彻理解希尔排序的性能至今仍然是一项挑战。
如何选择递增序列呢?要回答这个问题并不简单。算法的性能不仅取决于h,还取决于h之间的数学性质,比如它们的公因子等。有很多论文研究了各种不同的递增序列,但都无法证明某个序列是"最好的"。算法2.3中递增序列的计算和使用都很简单,和复杂递增序列的性能接近。但可以证明复杂的递增序列在最坏情况下的性能要好于我们所使用的递增序列。更加优秀的递增序列有待我们去发现。
通过sortCompare可以看到,希尔排序比插入排序和选择排序要快的多,并且数组越大,优势越大。在继续学习之前,请在你的计算机上用sortCompare比较一下希尔排序和插入排序以及选择排序的性能,数组的大小按照2的幂次递增。你会看到希尔排序能够解决一些初级排序算法无能为力的问题。这个例子是我们第一次用实际应用说明一个贯穿本书的重要理念:通过提升速度来解决其他方式无法解决的问题是研究算法的设计和性能的主要原因之一。
算法2.3的性能,目前最重要的结论是它的运行时间达不到平方级别。有意思的是,由插入排序到希尔排序,一个小小的改变就突破了平方级别的运行时间的屏障。
---