一、將各種數據排序
只要實現了Comparable接口的數據類型就可以被排序。
但要使算法能夠靈活地用不同字段進行排序,則是后續需要考慮的問題。
1、指針排序
在Java中,指針操作是隱式的,排序算法操作的總是數據引用,而不是數據本身。
2、鍵不可變
如果在排序后,用例還可以改變鍵值,那么數組很可能就不是有序的了。類似,優先隊列也會亂套。
Java中,可以用不可變數據類型作為鍵來避免這個問題,如String,Integer,Double和File都是不可變的。
3、廉價交換
使用引用的另一個好處是算法不必移動整個元素,對於元素大鍵值小的數組來說將會省下很多操作成本。
因為比較只需要訪問元素的一小部分,在整個排序過程中,大部分不會被訪問到。
因此對於任意大小的元素,使用引用,使得一般情況下交換和比較的成本幾乎相同。
如果鍵值很長,那么交換的成本甚至會低於比較的成本。
4、多種排序方法
實際應用中,用戶希望根據情況對一組對象按照不同的方式進行排序。
Java的Comparator接口允許在一個類中實現多種排序方法。通過將Comparator接口對象傳遞給sort方法,再傳遞給less方法即可。
Comparator接口實現舉例如下:
public class WhoOrder implements Comparator<Transaction> { @Override public int compare(Transaction v, Transaction w) { //add code return 0; } }
sort舉例如下:
public static void sort(Object[] a, Comparator c) { int N = a.length; for(int i = 1; i < N; i++) for(int j = i; j > 0 && less(c, a[j], a[j-1]); j--) exch(a, j, j-1); } private static boolean less(Comparator c, Object v, Object w) { return c.compare(v, w) < 0; } private static void exch(Object[] a, int i, int j) { Object t = a[i]; a[i] = a[j]; a[j] = t; }
Comparator接口允許為任意數據定義任意多種排序方法。用Comparator替代Comparable接口可以更好地將數據類型的定義和兩個該數據類型對象應該如何比較的定義區分開來。
比如,相對字符串數組a,進行忽略大小寫排序,可以使用String類中的CASE_INSENSITIVE_ORDER比較器Comparator,並傳遞給sort函數:
Insertion.sort(a, String.CASE_INSENSITIVE_ORDER)
5、多鍵
在實際應用中,一個元素的多個屬性都可能被用作排序的鍵。
要實現這種靈活性,Comparator接口正合適,我們可以在數據類型中定義多種比較器(Comparator)。
例如:
public class Transaction implements Comparable<Transaction> { private final String who; // customer private final Date when; // date private final double amount; // amount /** * Compares two transactions by customer name. */ public static class WhoOrder implements Comparator<Transaction> { @Override public int compare(Transaction v, Transaction w) { return v.who.compareTo(w.who); } } /** * Compares two transactions by date. */ public static class WhenOrder implements Comparator<Transaction> { @Override public int compare(Transaction v, Transaction w) { return v.when.compareTo(w.when); } } /** * Compares two transactions by amount. */ public static class HowMuchOrder implements Comparator<Transaction> { @Override public int compare(Transaction v, Transaction w) { return Double.compare(v.amount, w.amount); } } }
這樣定義之后,想讓Transaction按照時間排序,可以調用
Insertion.sort(a, new Transaction.WhenOrder())
6、穩定性
如果一個排序算法能夠保留數組中重復元素的相對位置的話,則這個排序算法使穩定的。
插入排序和歸並排序是穩定的,選擇排序、希爾排序、快速排序和堆排序則不是穩定的。
二、排序算法的對比
1、插入排序
穩定、原地排序
時間復雜度:最壞~N2/2、最好~N、平均~N2/4
空間復雜度:1
交換次數:最壞~N2/2、最好0、平均~N2/4
注:取決於輸入元素的排列情況
http://www.cnblogs.com/songdechiu/p/6610515.html
2、選擇排序
不穩定、原地排序
時間復雜度:N2(~N2/2)
空間復雜度:1
交換次數:N
http://www.cnblogs.com/songdechiu/p/6609896.html
3、希爾排序
不穩定、原地排序
時間復雜度:達不到平方級別、最壞和N3/2成正比(未知)
空間復雜度:1
http://www.cnblogs.com/songdechiu/p/6611340.html
4、快速排序
不穩定、原地排序
時間復雜度:最好~NlogN、最壞~N2、平均1.39NlogN
空間復雜度:最好logN,最壞N,平均logN
運行效率由概率提供保證
http://www.cnblogs.com/songdechiu/p/6629539.html
5、三向切分快速排序
不穩定、原地排序
時間復雜度:最好N、最壞~N2、平均~NlogN
空間復雜度:最好1、最壞N、平均logN
運行效率由概率提供保證
http://www.cnblogs.com/songdechiu/p/6629539.html
6、歸並排序
穩定、非原地排序
時間復雜度:1/2NlgN至NlgN
空間復雜度:N
數組訪問次數:6NlgN
http://www.cnblogs.com/songdechiu/p/6607341.html
http://www.cnblogs.com/songdechiu/p/6607720.html
7、堆排序
不穩定、原地排序
時間復雜度:2NlogN + 2N(最壞)
空間復雜度:1
數組元素交換次數:NlogN + N(最壞)
http://www.cnblogs.com/songdechiu/p/6736502.html
結論:快速排序是最快的通用排序算法,因為其內循環指令少,而且還能利用緩存(順序訪問數據),其增長數量級為~cNlogN。特別是使用三切分之后,快排對某些特殊輸入其復雜度變為線性級別。
實際應用中,大多數情況下,快速排序是最好的選擇。
但是如果穩定性很重要而且空間又不是問題,那么歸並排序是最好的。
三、問題規約
1、找出重復元素
首先將數組排序,然后遍歷有序的數組,記錄連續出現的重復元素即可。
2、排列
一個排列就是一組N個整數的數組,其中0到N-1的每個數都只出現一次。
兩個排列之間的Kendall tau距離就是兩組排列中的逆序數對。如0 3 1 6 2 5 4和1 0 3 6 4 2 5的Kendall tau距離是4,因為0-1,3-1,2-4,5-4這四對數字的相對順序不同。
Kendall tau距離就是逆序數對的數量。
實現:http://algs4.cs.princeton.edu/25applications/KendallTau.java.html
public class KendallTau { // return Kendall tau distance between two permutations public static long distance(int[] a, int[] b) { if (a.length != b.length) { throw new IllegalArgumentException("Array dimensions disagree"); } int n = a.length; int[] ainv = new int[n]; for (int i = 0; i < n; i++) ainv[a[i]] = i; Integer[] bnew = new Integer[n]; for (int i = 0; i < n; i++) bnew[i] = ainv[b[i]]; return Inversions.count(bnew); }
}
求兩個排列a和b的逆序數對跟以前歸並排序中求一個未排好序的排列相對排好序的排列的逆序數對有點不同。
歸並排序中是默認跟標准序列(排好序)即0 1 2 3 4 5 6進行比較,這里是a跟b進行比較,我們以b為標准序列。
所以我們要轉換成一個序列跟標准序列之間的逆序數對,再將該序列傳給歸並排序中的求解方法。
以方便對上述代碼進行解釋,做出以下假設
a:0 3 1 6 2 5 4
b:1 0 3 6 4 2 5
其中將a[i]稱為key,i稱為index;b[j]也為key,j為index。
上述代碼的主要思路是轉化為index序列之間的比較,以b為標准序列,其index序列為0 1 2 3 4 5 6,再求出b[i](0<= i <= 6)在數組a中相應的index序列。
ainv是a的逆數組,即存儲着數組a的key所在的index,即ainv[k] = i代表着a[i] = k。
當key為b[0] = 1時,其索引為ainv[b[0]]即ainv[1] = 2,以此類推最后求出a相對於b的索引序列為2 0 1 3 6 4 5。
最后將這個相對索引序列2 0 1 3 6 4 5傳給歸並排序中的求解方法即可,求出為4。
以下為歸並排序中求逆序數對的方法:
http://algs4.cs.princeton.edu/22mergesort/Inversions.java.html
public class Inversions { // do not instantiate private Inversions() { } // merge and count private static long merge(int[] a, int[] aux, int lo, int mid, int hi) { long inversions = 0; // 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 (aux[j] < aux[i]) { a[k] = aux[j++]; inversions += (mid - i + 1); } else a[k] = aux[i++]; } return inversions; } // return the number of inversions in the subarray b[lo..hi] // side effect b[lo..hi] is rearranged in ascending order private static long count(int[] a, int[] b, int[] aux, int lo, int hi) { long inversions = 0; if (hi <= lo) return 0; int mid = lo + (hi - lo) / 2; inversions += count(a, b, aux, lo, mid); inversions += count(a, b, aux, mid+1, hi); inversions += merge(b, aux, lo, mid, hi); assert inversions == brute(a, lo, hi); return inversions; } /** * Returns the number of inversions in the integer array. * The argument array is not modified. * @param a the array * @return the number of inversions in the array. An inversion is a pair of * indicies {@code i} and {@code j} such that {@code i < j} * and {@code a[i]} > {@code a[j]}. */ public static long count(int[] a) { int[] b = new int[a.length]; int[] aux = new int[a.length]; for (int i = 0; i < a.length; i++) b[i] = a[i]; long inversions = count(a, b, aux, 0, a.length - 1); return inversions; } }
3、優先隊列規約
兩個規約為優先隊列操作問題得例子:
TopM,在輸入流中找到前M個最大的元素。
MultiWay,將M個有序輸入流歸並為一個有序輸入流。
4、中位數和順序統計
找到一組數中的第k小的元素(中位數是特殊情況)。
可以規約為快排中的切分方法。
public static <Key extends Comparable<Key>> Key select(Key[] a, int k) { if (k < 0 || k >= a.length) { throw new IndexOutOfBoundsException("Selected element out of bounds"); } StdRandom.shuffle(a); int lo = 0, hi = a.length - 1; while (hi > lo) { int i = partition(a, lo, hi); if (i > k) hi = i - 1; else if (i < k) lo = i + 1; else return a[i]; } return a[lo]; }
平均上,基於切分的選擇算法的運行時間是線性級別的。
四、排序應用
1、商業計算
一般大量的信息都被存儲在大型數據庫中,能夠按照多個鍵排序以提供搜索效率。
可以說沒有線性對數級別的排序算法就沒法將這些海量的數據進行排序,進一步處理這些數據也會變得極端困難,甚至是不可能的。
2、信息搜索
有序的信息可以確保我們可以用經典的二分查找法來進行高效的搜索。
3、運籌學
(1)排序算法在調度中的應用
問題:假設我們需要完成N個任務,第j個任務需要耗時tj秒,我么需要在完成任務的同時,將每個任務的平均完成時間最小化。
思路:只要將任務按照處理時間升序排列就可以達到目標。
實現:因此按照最短優先原則,將任務按耗時排序,或者插入最小優先隊列中,即可完成目標。
(2)負載均衡問題
問題:假設我們有M個相同的處理器以及N個任務,目標是在盡可能短的時間內在這些處理器上完成所有任務。
思路:這個問題是NP-困難的,因此實際上不可能算出最優的方法,一種較優調度方法是最大優先。將任務按照耗時降序排列,將每個任務依次分配給當前可用的處理器。
實現:先逆序排序這些任務,然后為M個處理器維護一個優先隊列,每個元素的優先級就是對應處理器上運行的任務耗時之和。每一步都刪去優先級最低的處理器,將下個任務分配這個處理器,然后重新插入優先隊列。
4、數值計算
一些數值計算算法采用優先隊列和排序來控制計算中的精確度。
數值積分的一個方法,就是使用優先隊列來存儲一組小間隔中每段的近似精度。積分的過程就是刪除精確度最低的間隔,並分為兩段,然后將兩段都重新加入優先隊列,直到達到預期的精度。
5、事件驅動模型
第6章
6、組合搜索
優先隊列。A*算法。
7、Prim算法和Dijkstra算法
優先隊列在這兩個算法中扮演了重要的角色。
8、Kruskal算法
算法的運行時間是由排序所需的時間所決定的。
9、霍夫曼壓縮
這是數據壓縮中的一個經典算法。
算法處理的數據中每個元素都有一個小整數作為權重,處理的過程就是將權重最小的兩個元素歸並為一個新的元素,權重相加得到新元素的權重。
使用優先隊列可以立即實現這個算法。
其他的幾種數據壓縮算法也是基於排序的。
10、字符串處理
字符串算法常常依賴於排序算法(常用特殊的字符串排序算法)。