第1次遍历,将索引为0的元素与索引大于0的所有元素进行比较找到最小的元素,将最小元素与索引为0的元素交换位置(若索引为0的元素就是最小元素那么它就和自己交换)
第2次遍历,将索引为1的元素与索引大于1的所有元素进行比较找到最小的元素,将最小元素与索引为1的元素交换位置(若索引为1的元素就是最小元素那么它就和自己交换)。
如此往复,直到将整个数组排好序。
优点:数据交换次数最少的,与数组大小是线性关系,只有N次,别的排序算法大部分的增长数量级都是线性对数或平方级别
缺点:运行时间和输入无关。如,两个长度都为N的数组,一个元素顺序随机的数组,另一个已排好序或所有元素相等的数组,他们的排序时间一样长。
@SuppressWarnings("unchecked") public class Selection { // This class should not be instantiated. private Selection() { } public static void sort(Comparable[] a) { // in ascending order int N = a.length; for(int i = 0;i < N;i++) { // exchange elements int min = i; for(int j= i+1;j < N;j++) { // find the index of the smallest element if(less(a[j], a[min])) { min = j; } } exch(a, i, min); } } // is v < w ? private static boolean less(Comparable v, Comparable w) { return v.compareTo(w) < 0; } // exchange a[i] and a[j] private static void exch(Object[] a, int i, int j) { Object swap = a[i]; a[i] = a[j]; a[j] = swap; } // print array to standard output private static void show(Comparable[] a) { for (int i = 0; i < a.length; i++) { System.out.print(a[i]+" "); } System.out.println(""); } public static void main(String[] args) { Comparable[] a = {3,5,2,1,7,4,6}; Selection.sort(a); show(a); } }
原理:就像现实中玩扑克牌一样,一张一张的来,将每一张牌插入到其他已经有序的牌中的适当的位置。为了给要插入的元素腾出空间,需将其余所有元素在插入前向右移动一位。
原始数据有序性越高,插入排序越快。
平均情况下:插入排序需要N*N/4次比较和N*N/4次交换
最坏情况下:插入排序需要N*N/2次比较和N*N/2次交换(逆序数组)
最好情况下:插入排序需要N-1次比较和0次交换(已排好序的数组)
优点:插入排序运行时间取决于数组元素初始顺序。如,使用插入排序对一个很大且其中的元素已有序(或接近有序)的数组进行排序将会比对随机顺序的数组或逆序数组进行排序要快的多
缺点:比较次数越少,插入点后的数据移动越多,特别是当数据总量庞大的时候
适用场景:插入排序对部分有序的数组十分高效,也很适合小规模数组
@SuppressWarnings("unchecked") public class Insertion { // This class should not be instantiated. private Insertion() { } public static void sort(Comparable[] a) { // in ascending order int N = a.length; for(int i = 1;i < N;i++) { // exchange elements for(int j= i;j > 0 && less(a[j],a[j -1]);j--) { exch(a, j, j-1); } } } // less()、show()、exch()和main()方法见Selection.java }
缺点:h乘以的常数因子只能凭经验来设置,不太稳定
适用场景:大规模乱序数组
@SuppressWarnings("unchecked") public class Shell { // This class should not be instantiated. private Shell() { } public static void sort(Comparable[] a) { // in ascending order int N = a.length; // 3x+1 increment sequence: 1, 4, 13, 40, 121, 364, 1093, ... int h = 1; while (h < N/3) { h = 3*h + 1; } while (h >= 1) { // h-sort the array 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 /= 3; } } // less()、show()、exch()和main()方法见Selection.java }
原理:要将一个数组排序,可先(递归的)将它等分成两半分别排序,然后将结果归并起来。递归发生在处理整个数据之前。
缺点:实现比较复杂,排序需要额外空间且它所需的额外空间和N成正比
适用场景:稳定性很重要而空间不是问题时,归并排序可能是最好的
@SuppressWarnings("unchecked") public class Merge { // This class should not be instantiated. private Merge() { } /** * Rearranges the array in ascending order, using the natural order. * @param a the array to be sorted */ public static void sort(Comparable[] a) { Comparable[] aux = new Comparable[a.length]; sort(a, aux, 0, a.length-1); } // mergesort a[lo..hi] using auxiliary array aux[lo..hi] private static void sort(Comparable[] a, Comparable[] aux, int lo, int hi) { if (hi <= lo) return; int mid = lo + (hi - lo) / 2; sort(a, aux, lo, mid); sort(a, aux, mid + 1, hi); merge(a, aux, lo, mid, hi); } // stably merge a[lo .. mid] with a[mid+1 ..hi] using aux[lo .. hi] private static void merge(Comparable[] a, Comparable[] aux, int lo, int mid, int hi) { // copy to aux[] for (int k = lo; k <= hi; k++) { aux[k] = a[k]; } // merge back to a[] int i = lo, j = mid+1; 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++]; } } } // less()、show()、exch()和main()方法见Selection.java }
原理:与归并排序不同之处是快速排序切分(partition)的位置取决于数组的内容而归并排序是等分成两半。先将一个数组切分成两个子数组,然后将两部分独立排序。递归发生在处理整个数组之后。
优点:实现简单,原地排序且将长度为N的数组排序所需的时间和NlgN成正比
缺点:非常脆弱,实现时要非常小心才能避免低劣的性能,多种错误能使它在实际中的性能只有平方级别
适用场景:适用无重复元素的数组排序。运行时间至关重要而稳定性要求不是很高时,快速排序可能是最好的
@SuppressWarnings("unchecked") public class Quick { // This class should not be instantiated. private Quick() { } /** * Rearranges the array in ascending order, using the natural order. * @param a the array to be sorted */ public static void sort(Comparable[] a) {
// 打乱数组,保证随机性。这对预测算法的运行时间很重要 StdRandom.shuffle(a); sort(a, 0, a.length - 1); } // quicksort the subarray from a[lo] to a[hi] 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); } // partition the subarray a[lo..hi] so that a[lo..j-1] <= a[j] <= a[j+1..hi] // and return the index j. private static int partition(Comparable[] a, int lo, int hi) { int i = lo; int j = hi + 1; Comparable v = a[lo]; while (true) { // find item on lo to swap while (less(a[++i], v)) { if (i == hi) break; } // find item on hi to swap while (less(v, a[--j])) { if (j == lo) break; // redundant since a[lo] acts as sentinel } // check if pointers cross if (i >= j) break; exch(a, i, j); } // put partitioning item v at a[j] exch(a, lo, j); // now, a[lo .. j-1] <= a[j] <= a[j+1 .. hi] return j; } // less()、show()、exch()和main()方法见Selection.java }
import java.util.Random; public final class StdRandom { private static Random random; // pseudo-random number generator private static long seed; // pseudo-random number generator seed // static initializer static { // this is how the seed was set in Java 1.4 seed = System.currentTimeMillis(); random = new Random(seed); } // don't instantiate private StdRandom() { } /** * Returns a random real number uniformly in [0, 1). * * @return a random real number uniformly in [0, 1) */ public static double uniform() { return random.nextDouble(); } /** * Returns a random integer uniformly in [0, n). * * @param n number of possible integers * @return a random integer uniformly between 0 (inclusive) and {@code n} (exclusive) * @throws IllegalArgumentException if {@code n <= 0} */ public static int uniform(int n) { if (n <= 0) throw new IllegalArgumentException("argument must be positive: " + n); return random.nextInt(n); } /** * Returns a random long integer uniformly in [0, n). * * @param n number of possible {@code long} integers * @return a random long integer uniformly between 0 (inclusive) and {@code n} (exclusive) * @throws IllegalArgumentException if {@code n <= 0} */ public static long uniform(long n) { if (n <= 0L) throw new IllegalArgumentException("argument must be positive: " + n); long r = random.nextLong(); long m = n - 1; // power of two if ((n & m) == 0L) { return r & m; } // reject over-represented candidates long u = r >>> 1; while (u + m - (r = u % n) < 0L) { u = random.nextLong() >>> 1; } return r; } /** * Returns a random integer uniformly in [a, b). * * @param a the left endpoint * @param b the right endpoint * @return a random integer uniformly in [a, b) * @throws IllegalArgumentException if {@code b <= a} * @throws IllegalArgumentException if {@code b - a >= Integer.MAX_VALUE} */ public static int uniform(int a, int b) { if ((b <= a) || ((long) b - a >= Integer.MAX_VALUE)) { throw new IllegalArgumentException("invalid range: [" + a + ", " + b + ")"); } return a + uniform(b - a); } /** * Returns a random real number uniformly in [a, b). * * @param a the left endpoint * @param b the right endpoint * @return a random real number uniformly in [a, b) * @throws IllegalArgumentException unless {@code a < b} */ public static double uniform(double a, double b) { if (!(a < b)) { throw new IllegalArgumentException("invalid range: [" + a + ", " + b + ")"); } return a + uniform() * (b-a); } /** * Rearranges the elements of the specified array in uniformly random order. * * @param a the array to shuffle * @throws IllegalArgumentException if {@code a} is {@code null} */ public static void shuffle(Object[] a) { validateNotNull(a); int n = a.length; for (int i = 0; i < n; i++) { int r = i + uniform(n-i); // between i and n-1 Object temp = a[i]; a[i] = a[r]; a[r] = temp; } } /** * Rearranges the elements of the specified array in uniformly random order. * * @param a the array to shuffle * @throws IllegalArgumentException if {@code a} is {@code null} */ public static void shuffle(double[] a) { validateNotNull(a); int n = a.length; for (int i = 0; i < n; i++) { int r = i + uniform(n-i); // between i and n-1 double temp = a[i]; a[i] = a[r]; a[r] = temp; } } /** * Rearranges the elements of the specified array in uniformly random order. * * @param a the array to shuffle * @throws IllegalArgumentException if {@code a} is {@code null} */ public static void shuffle(int[] a) { validateNotNull(a); int n = a.length; for (int i = 0; i < n; i++) { int r = i + uniform(n-i); // between i and n-1 int temp = a[i]; a[i] = a[r]; a[r] = temp; } } /** * Rearranges the elements of the specified array in uniformly random order. * * @param a the array to shuffle * @throws IllegalArgumentException if {@code a} is {@code null} */ public static void shuffle(char[] a) { validateNotNull(a); int n = a.length; for (int i = 0; i < n; i++) { int r = i + uniform(n-i); // between i and n-1 char temp = a[i]; a[i] = a[r]; a[r] = temp; } } // throw an IllegalArgumentException if x is null // (x can be of type Object[], double[], int[], ...) private static void validateNotNull(Object x) { if (x == null) { throw new IllegalArgumentException("argument is null"); } } /** * Unit tests the methods in this class. * * @param args the command-line arguments */ public static void main(String[] args) { String[] a = "A B C D E F G".split(" "); StdRandom.shuffle(a); for (String s : a){ System.out.print(" "+s); } System.out.println(""); } }
原理:从左到右遍历数组一次,维护3个指针。一个指针lt,使得a[lo..lt-1]中的元素都小于v;一个指针gt,使得a[gt+1..hi]中的元素都大于v;一个指针i,使得a[lt..i-1]中的元素都等于v;a[i..gt]中的元素都还未确定。
缺点:数组中重复元素不多时比标准的二分法多使用很多次交换,最坏的情况是所有元素均不重复时
适用场景:适用大量重复元素的数组排序,如企业可能需要将大量人员资料按照生日或性别排序
@SuppressWarnings("unchecked") public class Quick3Way { // This class should not be instantiated. private Quick3Way() { } /** * Rearranges the array in ascending order, using the natural order. * @param a the array to be sorted */ public static void sort(Comparable[] a) { StdRandom.shuffle(a); sort(a, 0, a.length - 1); } // quicksort the subarray a[lo .. hi] using 3-way partitioning 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++; } } // a[lo..lt-1] < v = a[lt..gt] < a[gt+1..hi]. sort(a, lo, lt-1); sort(a, gt+1, hi); } // less()、show()、exch()和main()方法见Selection.java }
原理:堆排序分两个阶段:堆的构造阶段和下沉排序阶段。
缺点:无法利用缓存,数组元素很少和相邻的其他元素进行比较,因此缓存未命中的次数要远高于大多数比较都在相邻元素间进行的排序算法(如快速排序、归并排序、希尔排序等)
适用场景:插入操作和删除最大元素操作混合的动态场景(此时运行时间能保证是对数级别的)
@SuppressWarnings("unchecked") public class Heap { // This class should not be instantiated. private Heap() { } /** * Rearranges the array in ascending order, using the natural order. * @param pq the array to be sorted */ public static void sort(final 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); } } /*************************************************************************** * Helper functions to restore the heap invariant. ***************************************************************************/ private static void sink(Comparable[] pq, int k, int n) { while (2*k <= n) { int j = 2*k; if (j < n && less(pq[j-1], pq[j])){ j++; } if (!less(pq[k-1], pq[j-1])) { break; } exch(pq, k, j); k = j; } } // less()、show()、exch()和main()方法见Selection.java }
二.实际应用中如何选择何种算法
Java的系统程序员对原始数据类型使用(三向切分的)快速排序(速度快),对引用类型使用归并排序(稳定性高)。见Array.sort()实现
总结:实际应用中如何选择使用何种排序
———————————————————————————————————————————————————————————————————————
算法 是否稳定 是否为原地排序 N个元素排序的复杂度 备注 适用场景
时间复杂度 空间复杂度
———————————————————————————————————————————————————————————————————————
选择排序 否 是 N*N 1 适合小规模数组
插入排序 是 是 介于N和N*N之间 1 取决于输入元素的排列情况 适合插入排序对部分有序的数组十分高效,也很适合小规模数组
希尔排序 否 是 NlogN? 1 适合大规模乱序数组
快速排序 否 是 NlogN lgN 运行效率由概率保证 适用无重复元素的数组排序。运行时间至关重要而稳定性要求不是很高时,快速排序可能是最好的
三向快速排序 否 是 介于N和NlogN之间 lgN 运行效率由概率提供保证,也取决于输入元素的分布情况 适用大量重复元素的数组排序,如企业可能需要将大量人员资料按照生日或性别排序
归并排序 是 否 NlogN N 稳定性很重要而空间不是问题时,归并排序可能是最好的
堆排序 否 是 NlogN 1 插入操作和删除最大元素操作混合的动态场景(此时运行时间能保证是对数级别的)
三.各常用算法耗时比较
见SortCompare.java
Stopwatch.java为耗时类
/****************************************************************************** * Compilation: javac SortCompare.java * Execution: java SortCompare Insertion Selection 1000 100 * 1000 means the Length of automatically generating arrays * 100 means repeats times * Compare the running time of these algorithms. * * * % java SortCompare Insertion Selection 1000 100 * For 1000 random Doubles Insertion takes 0.125 seconds * For 1000 random Doubles Selection takes 0.125 seconds * * * % java SortCompare Insertion Selection Shell Merge Quick Heap 1000 100 * For 1000 random Doubles Insertion takes 0.094 seconds * For 1000 random Doubles Selection takes 0.094 seconds * For 1000 random Doubles Shell takes 0.109 seconds * For 1000 random Doubles Merge takes 0.062 seconds * For 1000 random Doubles Quick takes 0.078 seconds * For 1000 random Doubles Heap takes 0.032 seconds * * * % java SortCompare Insertion Selection Shell Merge Quick Heap 100000 100 * For 100000 random Doubles Insertion takes 1447.799 seconds * For 100000 random Doubles Selection takes 1217.424 seconds * For 100000 random Doubles Shell takes 3.613 seconds * For 100000 random Doubles Merge takes 2.467 seconds * For 100000 random Doubles Quick takes 1.973 seconds * For 100000 random Doubles Heap takes 2.812 seconds * ******************************************************************************/ import java.util.Arrays; import java.util.ArrayList; import java.util.List; public class SortCompare { private static final String[] ALG = {"Insertion", "Selection", "Shell", "Merge", "Quick", "Heap"}; /** * time * @param alg algorithm name * @param a the array */ public static double time(String alg, Double[] a){ Stopwatch timer = new Stopwatch(); // printBeforeSort(alg, a); // Print the original array if(alg.equals("Insertion")){ Insertion.sort(a); } else if(alg.equals("Selection")){ Selection.sort(a); } else if(alg.equals("Shell")){ Shell.sort(a); } else if(alg.equals("Merge")){ Merge.sort(a); } else if(alg.equals("Quick")){ Quick.sort(a); } else if(alg.equals("Heap")){ Heap.sort(a); } // printAfterSort(alg, a); // Print the sorted array return timer.elapsedTime(); } /** * total elapsed time * @param alg algorithm name * @param N the length of the array * @param T repeats times */ public static double timeRandomInput(String alg, int N, int T) { // Sort T arrays of length N by using algorithm double total = 0.0; Double[] a = new Double[N]; for(int t = 0; t < T; t++) { // generate and sort for(int i = 0; i < N; i++) { a[i] = StdRandom.uniform(); } total += time(alg, a); } return total; } public static void main(String[] args) { testDataFromArgs(args); //testDataFromFile(args); } /** * * % java SortCompare Insertion Selection 1000 100 */ private static void testDataFromArgs(String[] args){ if(args == null || args.length < 3){ System.out.println("Incorrect number of arguments,the mininum mumber is 3!!!"); return; } int len = args.length -2; List<String> al = new ArrayList<String>(Arrays.asList(ALG)); for(int i = 0; i < len;i++){ if(!al.contains(args[i])) { System.out.println("Contains unknown algorithm names!!!"); return; } } int N = Integer.parseInt(args[len]); int T = Integer.parseInt(args[len + 1]); double t = 0.0; for(int i = 0; i < len;i++){ t = timeRandomInput(args[i], N, T);// total time of algorithm System.out.format("For %d random Doubles %s takes %.3f seconds \n", N, args[i], t); } } /** * * % more test.txt * * % java SortCompare < test.txt */ private static void testDataFromFile(String[] args){ double[] b = StdIn.readAllDoubles(); int N = b.length; int T = 100; Double[] a = new Double[N]; for(int i = 0;i < N;i++){ a[i] = b[i]; } int len = ALG.length; double t = 0.0; for(int i = 0; i < len;i++){ t = timeRandomInput(ALG[i], a, T);// total time of algorithm System.out.format("For %d random Doubles %s takes %.3f seconds \n", N, ALG[i], t); } } private static double timeRandomInput(String alg, Double[] a, int T) { // Sort T arrays of length N by using algorithm double total = 0.0; for(int t = 0; t < T; t++) { total += time(alg, a); } return total; } private static void printBeforeSort(String alg, Double[] a){ if(a.length > 50){ return; } System.out.format("\n%s --- before sort\n", alg); show(a); } private static void printAfterSort(String alg, Double[] a){ if(a.length > 50){ return; } System.out.format("\n%s --- after sort\n", alg); show(a); } private static void show(Comparable[] a) { for (int i = 0; i < a.length; i++) { System.out.print(a[i]+" "); } System.out.println(); } }
Stopwatch.java源码如下:
* Initializes a new stopwatch.
*/
public Stopwatch() {
start = System.currentTimeMillis();
}
/**
* Returns the elapsed CPU time (in seconds) since the stopwatch was created.
*
* @return elapsed CPU time (in seconds) since the stopwatch was created
*/
public double elapsedTime() {
long now = System.currentTimeMillis();
return (now - start) / 1000.0;
}
/**
* Unit tests the {@code Stopwatch} data type.
* Takes a command-line argument {@code n} and computes the
* sum of the square roots of the first {@code n} positive integers,
* first using {@code Math.sqrt()}, then using {@code Math.pow()}.
* It prints to standard output the sum and the amount of time to
* compute the sum. Note that the discrete sum can be approximated by
* an integral - the sum should be approximately 2/3 * (n^(3/2) - 1).
*
* @param args the command-line arguments
*/
public static void main(String[] args) {
int n = Integer.parseInt(args[0]);
Stopwatch timer1 = new Stopwatch();
double sum1 = 0.0;
for (int i = 1; i <= n; i++) {
sum1 += Math.sqrt(i);
}
double time1 = timer1.elapsedTime();
System.out.format("%e (%.2f seconds)\n", sum1, time1);
// sum of square roots of integers from 1 to n using Math.pow(x, 0.5).
Stopwatch timer2 = new Stopwatch();
double sum2 = 0.0;
for (int i = 1; i <= n; i++) {
sum2 += Math.pow(i, 0.5);
}
double time2 = timer2.elapsedTime();
System.out.format("%e (%.2f seconds)\n", sum2, time2);
}
}