本文共 16795 字,大约阅读时间需要 55 分钟。
排序就是将一组对象按照某种逻辑顺序重新排列的过程。这里我们主要关注重新排列含有元素的数组 (arrays of items)的算法,其中每个元素都有一个主键 (key)。排序算法的目的是重新排列所有元素,使得元素的主键能以某种方式排列。以下代码是本章通用的排序算法模板:
public class Example{ public static void sort(Comparable[] a) { /*see text*/ } 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); }}
此接口强行对实现它的每个类的对象进行整体排序。Java中的封装数字的类型Integer和Double,以及String和其他许多高级数据类型都实现了Comparable接口,因此可以直接用这些类型的数组作为参数调用排序算法,如下图快速排序调用数组a:
在创建自己的数据类型时,只要实现Comparable接口就能保证用例代码可以将其排序。要做到这一点,需要实现一个compareTo ()方法来定义目标类型对象的自然次序 。如下图的Date数据类型:
compareTo() 方法 实现了对主键 (key) 的抽象 —— 对于任何实现了Comparable接口的数据类型,该方法定义了对象的大小顺序。
public class Selection{ public static void sort(Comparable[] a) { int N = a.length; for (i = 0; i < N; i++) { int min = i; for (j = i+1; j < N; j++) if (less(a[j], a[min])) min = j; exch(a, i, min); } }}
对于长度为N的数组,选择排序需要大约 \(~N^2/2\)次比较和N次交换。
选择排序的特点:
public class Insertion{ public static void sort(Comparable[] a) { int N = a.length; for (int i = 1; i < N; i++) { for (int j = i; j > 0 && less(a[j], a[j-1]); j--) exch(a, j, j-1); } }}
public class InsertionX{ public static void sort(Comparable[] a) { int N = a.length; for (int i = 1; i < N; i++) { Comparable temp = a[i]; for (j = i; j > 0 && less(temp, a[j-1]); j--) a[j] = a[j-1]; } a[j] = temp; }}// alternative waypublic class InsertionX{ public static void sort(Comparable[] a) { int N = a.length; for (int i = 1; i < N; i++) { Comparable temp = a[i]; int j = i; while (j > 0 && less(temp, a[j-1])) { a[j] = a[j-1]; j--; } a[j] = temp; } }}
public class SortCompare{ public static double time(String alg, Comparable[] 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) { 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); double t2 = timeRandomInput(alg2, N, T); 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 100For 1000 random DoublesInsertion is 1.7 times faster than Selection**********************************************/
希尔排序是插入排序的改进,插入排序的缺点是每次只能交换相邻的元素,若主键最小的元素恰好在数组的末尾,则需要N-1次移动。希尔排序的思想是通过交换不相邻的元素使数组中任意间隔为h的元素都是有序的,并最终用插入排序将局部有序的数组排序。
public class Shell{ public static void sort(Comparable[] a) { int N = a.length; int h = 1; while (h < N/3) h = 3*h + 1; while (h >= 1) { for (int i = h; i < N; i++) { for (int j = i; j >= h && less(a[j], a[j-h]); j -= h) exch(a, j, j-h) } h = h/3; } }}/********************************************% java SortCompare Shell Insertion 100000 100For 100000 random DoublesShell is 600 times faster than Insertion*********************************************/
归并排序一般是 (递归地) 将数组分成两半分别排序,然后将结果归并起来。归并排序的优点是所需的时间和 \(NlgN\) 成正比,这使得其优于选择排序和插入排序。缺点是辅助数组所需的额外空间与N的大小成正比。归并排序是分治 (divide-and-conquer) 思想的典型体现。
public class Merge{ private static Comparable[] aux; public static void sort(Comparable[] a) { aux = new Comparable[a.length]; sort(a, 0, a.length - 1); } private static void sort(Comparable[] a, int lo, int hi) { if (hi <= lo) return; int mid = lo + (hi - lo) / 2; sort(a, lo, mid); sort(a, mid+1, hi); merge(a, lo, mid, hi); } public static boid merge(Comparable[] a, int lo, int mid, int hi) { int i = lo, j = mid+1; for (int k = lo; k <= hi; k++) aux[k] = a[k]; for (int k = lo; k <= hi; k++) if (i > mid) a[k] = aux[j++]; else if (j > hi) a[k] = aux[i++]; else if (less(aux[j], aux[i])) a[k] = aux[j++]; else a[k] = aux[i++]; } private static boolean less(Comparable v, Comparable w) { return v.compareTo(w) < 0; }}
public class MergeBU{ private static Comparable[] aux; public static void sort(Comparable[] a) { int N = a.length; aux = new Cmparable[N]; for ( int sz = 1; sz < N; sz = sz+sz) for (int lo = 0; lo < N-sz; lo += sz+sz) merge(a, lo, lo+sz-1, Math.min(lo+sz+sz-1, N-1)); } public staic boid merge(Comparable[] a, int lo, int mid, int hi) { int i = lo, j = mid+1; for (int k = lo; k <= hi; k++) aux[k] = a[k]; for (int k = lo; k <= hi; k++) if (i > mid) a[k] = aux[j++]; else if (j > hi) a[k] = aux[i++]; else if (less(aux[j], aux[i])) a[k] = aux[j++]; else a[k] = aux[i++]; } private static boolean less(Comparable v, Comparable w) { return v.compareTo(w) < 0; }}
在一般应用中快速排序都比其他排序算法快得多,除此之外快速排序是原地排序,所需时间和 \(NlgN\)成正比。主要缺点是非常脆弱,在实现时需要非常小心才能避免低劣的性能。
快速排序也是一种分治的排序算法。与归并排序的区别:归并排序将两个子数组分别排序后,归并前整个数组不是有序的;快速排序则是当子数组都有序时整个数组也就自然有序了。在归并排序中,一个数组被分成两半;快速排序中,切分 (partition) 的位置取决于数组的内容。
快速排序递归地将子数组 a[lo...hi] 排序,先用partition()方法将a[j] 放到一个合适位置,然后递归地将其他位置的元素排序。这里的关键在于切分,切分后数组满足三个条件:
public class Quick{ public static void sort(Comparable[] a) { StdRandom.shuffle(a); sort(a, 0, a.length - 1); } private static void sort(Comparable[] a, int lo, int hi) { if (hi <= lo) return; int j = partition(a, lo, hi); sort(a, lo, j-1); sort(a, j+1, hi); } private static int partition(Comparable[] a, int lo, int hi) { int i = lo, j = hi + 1; Comparable v = a[lo]; while (true) { while (less(a[++i], v)) if (i == hi) break; while (less(v, a[--j])) if (j == lo) break; if (i >= j) break; exch(a, i, j); } exch(a, lo, j); return j; }}
上述快速排序的实现有一个潜在的缺点:在切分不平衡时可能会极为低效,达到平方级别。例如如果从最小的元素切分,第二次从第二小的元素切分,如此这般每次只移除一个元素,这会导致一个大数组需要切分很多次。因此快速排序前需要现将数组随机排序。
切换到插入排序: 对于小数组,快速排序比插入排序慢,所以可将上述算法中sort() 方法从if (hi <= lo) return;
改为 if (hi <= lo + M) { Insertion.sort(a, lo, hi); return; }
当数组中存在大量重复元素时,快速排序的效率可以通过三向切分排序进一步改进,能将排序时间从线性对数级下降到线性级别。将数组分为三部分,分别对应小于、等于和大于切分元素的数组元素。使用Comparable 接口对a[i] 进行三向比较:
public class Quick3way{ private static void sort(Comparable[] a, int lo, int hi) { if (hi <= lo) return; int lt = lo, i = lo + 1, gt = hi; Comparable v = a[lo]; while (i <= gt) { int cmp = a[i].compareTo(v); if (cmp < 0) exch(a, lt++, i++); else if (cmp > 0) exch(a, i, gt--); else i++; } sort(a, lo, lt - 1); sort(a, gt + 1, hi); }}
普通的队列 (queue) 是一种先进先出的数据结构,元素在队列尾追加,而从队列头删除。在优先队列中,元素被赋予优先级。当访问元素时,具有最高优先级的元素最先删除。这种队列不是直接将新元素放在队列尾部,而是放在比它优先级低的元素前面。
优先队列最重要的操作是删除最大元素 (delMax()) 和插入元素 (insert()) ,见以下API:
以下是一个优先队列的测试用例,打印数字最大的M行:
public class TopM{ public static void main(String[] args) { int M = Integer.parseInt(args[0]); MinPQpq = new MinPQ (M+1); while (StdIn.hasNextLine()) { pq.insert(new Transaction(StdIn.readLine())); if (pq.size() > M) pd.delMin(); } Stack stack = new Stack (); while (!pq.isEmpty()) stack.push(pq.delMin()); for (Transaction t : stack) StdOut.println(t); }}
数据结构二叉堆 (binary heap) 能很好地实现优先队列的基本操作。二叉堆是一组能够用堆有序的完全二叉树排序的元素,并在数组中按照层级存储 (不使用数组的第一个位置)。堆有序 (heap-ordered) 指的是一颗二叉树的每个结点都大于等于它的两个子结点。
在一个堆中,位置为k的结点的父结点位置为\(\left\lfloor k/2 \right\rfloor\) ,两个子结点的位置分别为 \(2k\) 和 \(2k+1\)。一颗大小为N的完全二叉树的高度为\(\left\lfloor lgN \right\rfloor\)
自下而上的堆有序化 —— 上浮 (swim)
如果某个结点比其父结点大,则需要交换二者以保持堆的有序化,这样的循环过程为swim() 方法:
private void swim(int k){ while (k > 1 && less(k/2, k)) { exch(k/2, k); k = k/2; }}
自上而下的堆有序化 —— 下沉 (sink)
如果某个结点比其两个子结点的其中之一小,则需要将它和两个子结点的较大者交换以保持堆的有序化这样的循环过程为sink() 方法:
private void sink(int k){ while (2*k <= N) { int j = 2*k; if (j < N && less(j, j+1)) j++; if (!less(k, j)) break; exch(k, j); k = j; }}
插入元素:将新元素加到数组末尾,增加堆的大小并将这个元素上浮到合适位置。
删除最大元素:从数组顶端删去最大元素并将数组最后一个元素放到顶端,再让这个元素下沉到合适位置。
下列代码实现了优先队列,优先队列由一个基于堆的完全二叉树表示,存储于数组 pq[1...N] 中,pq[0] 没有使用。
public class MaxPQ>{ private Key[] pq; private int N = 0; public MaxPQ(int maxN) { pq = (Key[]) new Comparable[maxN+1]; } public boolean isEmpty() { return N == 0; } public int size() { return N; } public void insert(Key v) { pq[++N] = v; swim(N); } public Key delMax() { Key max = pq[1]; exch(1, N--); pq[N+1] = null; sink(1); return max; } private boolean less(int i, int j) { return pq[i].compareTo(pq[j]) < 0; } private void exch(int i, int j) { Key t = pq[i]; pq[i] = pq[j]; pq[j] = t; } private void swim(int k) { while (k > 1 && less(k/2, k)) { exch(k/2, k); k = k/2; } } private void sink(int k) { while (2*k <= N) { int j = 2*k; if (j < N && less(j, j+1)) j++; if (!less(k, j)) break; exch(k, j); k = j; } }}
索引优先队列即在优先队列的基础上给数组中每个元素添加一个索引。以下实现用pq[]
保存索引,qp[]
保存pq[]
的逆序,即pq[]
中元素的位置。举例:pq = [1,2,0], 则qp = [2,0,1],pq[qp[1]]=1
。数组keys[]
保存元素。
import edu.princeton.cs.algs4.StdOut;import edu.princeton.cs.algs4.In;public class IndexMinPQ>{ private int maxN; private int n; private int[] pq; private int[] qp; private Key[] keys; public IndexMinPQ(int maxN) { this.maxN = maxN; n = 0; keys = (Key[]) new Comparable[maxN + 1]; pq = new int[maxN + 1]; qp = new int[maxN + 1]; for (int i = 0; i <= maxN; i++) qp[i] = -1; } public boolean isEmpty() { return n == 0; } public boolean contains(int i) { return qp[i] != -1; } public int size() { return n; } public void insert(int i, Key key) { n++; qp[i] = n; pq[n] = i; keys[i] = key; swim(n); } public int minIndex() { return pq[1]; } public Key minKey() { return keys[pq[1]]; } public int delMin() { int min = pq[1]; exch(1, n--); sink(1); assert min == pq[n+1]; qp[min] = -1; keys[min] = null; pq[n+1] = -1; return min; } public void changeKey(int i, Key key) { keys[i] = key; swim(qp[i]); sink(qp[i]); } public void delete(int i) { int index = qp[i]; exch(index, n--); swim(index); sink(index); keys[i] = null; qp[i] = -1; } private boolean greater(int i, int j) { return keys[pq[i]].compareTo(keys[pq[j]]) > 0; } private void exch(int i, int j) { int swap = pq[i]; pq[i] = pq[j]; pq[j] = swap; qp[pq[i]] = i; qp[pq[j]] = j; } private void swim(int k) { while (k > 1 && greater(k/2, k)) { exch(k, k/2); k = k/2; } } private void sink(int k) { while (2*k <= n) { int j = 2*k; if (j < n && greater(j, j+1)) j++; if (!greater(k, j)) break; exch(k, j); k = j; } } /****************************************************** 以下为用例:多向归并问题,将多个有序的输入流归并成一个有序的输出流 ******************************************************/ public static void merge(In[] streams) { int N = streams.length; IndexMinPQ pq = new IndexMinPQ (N); for (int i = 0; i < N; i++) if (!streams[i].isEmpty()) pq.insert(i, streams[i].readString()); while (!pq.isEmpty()) { StdOut.printf(pq.minKey() + " "); int i = pq.delMin(); if (!streams[i].isEmpty()) pq.insert(i, streams[i].readString()); } } public static void main(String[] args) { int N = args.length; In[] streams = new In[N]; for (int i = 0; i < N; i++) streams[i] = new In(args[i]); merge(streams); StdOut.println(); }}/*************输入:% more m1.txtA B C F G I I Z% more m2.txtB D H P Q Q% more m3.txtA B E F J 输出:% java IndexMinPQ m1.txt m2.txt m3.txtA A B B B C D E F F G H I I J N P Q Q Z **************************************/
堆排序可分为两个阶段:
堆的构造通过从下到上递归地构建子堆来实现。如果一个结点的两个子结点都已经是堆了,那么在该结点上调用sink() 就能把它们变成一个堆,这样就能按数组逆序从下而上地构建堆。只需对数组中一半元素使用 sink()方法,因为剩下的元素都是叶结点。完成后堆的最大元素将位于数组的开头。
下沉排序阶段将顶端和末端元素进行交换,然后将最大元素从堆中删除并用 sink() 重新调整堆的结构,如此循环最后得到一个有序序列。
import edu.princeton.cs.algs4.StdIn;import edu.princeton.cs.algs4.StdOut;public class Heap{ private Heap() { } public static void sort(Comparable[] pq) { int n = pq.length; for (int k = n/2; k >= 1; k--) sink(pq, k, n); while (n > 1) { exch(pq, 1, n--); sink(pq, 1, n); } } private static void sink(Comparable[] pq, int k, int n) { while (2*k <= n) { int j = 2*k; if (j < n && less(pq, j, j+1)) j++; if (!less(pq, k, j)) break; exch(pq, k, j); k = j; } } private static boolean less(Comparable[] pq, int i, int j) { return pq[i-1].compareTo(pq[j-1]) < 0; } private static void exch(Object[] pq, int i, int j) { Object swap = pq[i-1]; pq[i-1] = pq[j-1]; pq[j-1] = swap; } private static void show(Comparable[] a) { for (int i = 0; i < a.length; i++) StdOut.println(a[i]); } public static void main(String[] args) { String[] a = StdIn.readAllStrings(); Heap.sort(a); show(a); }}
只需线性对数的时间,先将数组排序,再遍历数组,记录连续出现的重复元素。如果用平方级别的算法则要将所有元素相互比较一遍。
Quick.sort(a);int count = 1;for (int i = 1; i < a.length; i++) if (a[i].compareTo(a[i-1]) != 0) count++;
转载地址:http://ccwql.baihongyu.com/