算法(第4版)-2.1 初级排序算法
2.1.1 游戏规则
1. 排序成本模型:在研究排序算法时,我们需要计算比较和交换的数量。对于不交换元素的算法,我们会计算访问数组的次数。
2.
· 原地排序算法:除了函数调用所需的栈和固定数目的实例变量之外无需额外内存的原地排序算法;
· 其他排序算法:需要额外内存空间来储存另一份数组副本。
2.2.2 选择排序
public class Selection { public static void sort(Comparable[] a) { // 将a[]按升序排列 int N = a.length; // 数组长度 for (int i = 0; i < N; i++) { // 将a[i]和a[i+1..N]中最小的元素交换 int min = i; for (int j = i + 1; j < N; j++) if (less(a[j], a[min])) min = j; exch(a, i, min); } } private static boolean less(Comparable v, Comparable w) { return v.compareTo(w) < 0; } 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 < a.length; i++) StdOut.print(a[i] + " "); StdOut.println(); } public static boolean isSorted(Comparable[] a) { // 测试数组元素是否有序 for (int i = 1; i < a.length; i++) if (less(a[i], a[i - 1])) return false; return true; } public static void main(String[] args) { // 从标准输入读取字符串,将它们排序并输出 String[] a = In.readStrings(); sort(a); assert isSorted(a); show(a); } }
1. 定义:首先,找到数组中最小的那个元素,其次,将它和数组的第一个元素交换位置(如果第一个元素就是最小元素那么它就和自己交换)。再次,在剩下的元素中找到最小的元素,将它与数组的第二个元素交换位置。如此往复,直到将整个数组排序。
2. 对于长度为N的数组,选择排序需要大约N^2/2次比较和N次交换。
3. 选择排序有两个很鲜明的特点:
· 运行时间和输入无关(一个已经有序的数组或主键全部相等的数组和一个元素随机排列的数组所用的排序时间竟然一样长);
· 数据移动是最少的(选择排序用了N次交换--线性级别,大部分其他算法的交换次数--线性对数或是平方级别)。
2.2.3 插入排序
public class Insertion { public static void sort(Comparable[] a) { // 将a[]按升序排列 int N = a.length; for (int i = 1; i < N; i++) { // 将a[i]插入到a[i-1]、a[i-2]、a[i-3]...之中 for (int j = i; j > 0 && less(a[j], a[j - 1]); j--) exch(a, j, j - 1); } } private static boolean less(Comparable v, Comparable w) { return v.compareTo(w) < 0; } 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 < a.length; i++) StdOut.print(a[i] + " "); StdOut.println(); } public static boolean isSorted(Comparable[] a) { // 测试数组元素是否有序 for (int i = 1; i < a.length; i++) if (less(a[i], a[i - 1])) return false; return true; } public static void main(String[] args) { // 从标准输入读取字符串,将它们排序并输出 String[] a = In.readStrings(); sort(a); assert isSorted(a); show(a); } }
1. 与冒泡排序的区别:
· 插入排序:将每一个元素插入到其他已经有序的元素中的适当位置;
· 冒泡排序:每次将剩余最大的元素向右边推过去。
2. 插入排序所需的时间取决于输入中元素的初始顺序。
3. 对于随机排序的长度为N且主键不重复的数组,插入排序:
· 平均情况下需要~N^2/4次比较以及~N^2/4次交换
· 最坏情况下需要~N^2/2次比较以及~N^2/2次交换
· 最好情况下需要N-1次比较和0次交换
4. 下面是几种典型的部分有序的数组:
· 数组中每个元素距离它的最终位置都不远
· 一个有序的大数组接一个小数组
· 数组中只有几个元素的位置不正确
插入排序对这样的数组很有效,而选择排序则不然。
2.1.4 排序算法的可视化
1. 插入排序所需的比较次数平均只有选择排序的一半。
2.1.5 比较两种排序算法
public class SortCompare { public static double time(String alg, Double[] a) { Stopwatch timer = new Stopwatch(); if (alg.equals("Insertion")) Insertion.sort(a); if (alg.equals("Selection")) Selection.sort(a); // if (alg.equals("Shell")) Shell.sort(a); // if (alg.equals("Merge")) Merge.sort(a); // if (alg.equals("Quick")) Quick.sort(a); // if (alg.equals("Heap")) Heap.sort(a); return timer.elapsedTime(); } public static double timeRandomInput(String alg, int N, int T) { // 使用算法alg将T个长度为N的数组排序 double total = 0.0; Double[] a = new Double[N]; for (int t = 0; t < T; t++) { // 进行一次测试(生成一个数组并排序) for (int i = 0; i < N; i++) a[i] = StdRandom.uniform(); total += time(alg, a); } return total; } public static void main(String[] args) { String alg1 = args[0]; String alg2 = args[1]; int N = Integer.parseInt(args[2]); int T = Integer.parseInt(args[3]); double t1 = timeRandomInput(alg1, N, T); // 算法1的总时间 double t2 = timeRandomInput(alg2, N, T); // 算法2的总时间 StdOut.printf("For %d random Doubles\n %s is", N, alg1); StdOut.printf(" %.1f times faster than %s\n", t2 / t1, alg2); } } /* java SortCompare Insertion Selection 1000 100 For 1000 random Doubles Insertion is 1.7 times faster than Selection */
这个用例会运行由前两个命令行参数指定的排序算法,对长度为N(由第三个参数指定)的
1. 对于随机排序的无重复主键的数组,插入排序和选择排序的运行时间是平方级别的,两者之比应该是一个较小的常数。
2.1.6 希尔排序
public class Shell { public static void sort(Comparable[] a) { // 将a[]按升序排列 int N = a.length; int h = 1; while (h < N / 3) h = 3 * h + 1; // 1, 4, 13, 40, 121, 364, 1093, ... while (h >= 1) { // 将数组变为h有序 for (int i = h; i < N; i++) { // 将a[i]插入到a[i-h],a[i-2*h],a[i-3*h]...之中 for (int j = i; j >= h && less(a[j], a[j - h]); j -= h) exch(a, j, j - h); } h = h / 3; } } private static boolean less(Comparable v, Comparable w) { return v.compareTo(w) < 0; } 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 < a.length; i++) StdOut.print(a[i] + " "); StdOut.println(); } public static boolean isSorted(Comparable[] a) { // 测试数组元素是否有序 for (int i = 1; i < a.length; i++) if (less(a[i], a[i - 1])) return false; return true; } public static void main(String[] args) { // 从标准输入读取字符串,将它们排序并输出 String[] a = In.readStrings(); sort(a); assert isSorted(a); show(a); } }
1. 思想:使数组中任意间隔为h的元素都是有序的。
2. 通过提升速度来解决其他方式无法解决的问题是研究算法的设计和性能的主要原因之一。
3. 希尔排序的运行时间达不到平方级别。例如,已知在最坏的情况下算法2.3的比较次数和N^(3/2)成正比。
4. 希尔排序对于中等大小的数组它的运行时间是可以接受的。它的代码量很小,且不需要使用额外的内存空间。
5.如果你需要解决一个排序问题而又没有系统排序函数可用(例如直接接触硬件或是运行于嵌入式系统中的代码),可以先用希尔排序,然后再考虑是否值得将它替换为更加复杂的排序算法。