java實現幾種常見排序算法
本文介紹幾種常見排序算法(選擇排序,插入排序,希爾排序,歸并排序,快速排序,堆排序),對算法的思路、性質、特點、具體步驟、java實現以及trace圖解進行了全面的說明。最后對幾種排序算法進行了比較和總結。
寫在前面
-
本文所有圖片均截圖自coursera上普林斯頓的課程 《Algorithms, Part I》 中的Slides
-
相關命題的證明可參考 《算法(第4版)》
-
源碼可在 官網 下載,也可以在我的github倉庫 algorithms-learning 下載,已經使用maven構建
-
倉庫下載: git clone git@github.com:brianway/algorithms-learning.git
基礎介紹
java: Interface Comparable<T>
Java中很多類已經實現了 Comparable<T> 接口,用戶也可自定義類型實現該接口
total order:
-
Antisymmetry(反對稱性): if v ≤ w and w ≤ v, then v = w.
-
Transitivity(傳遞性): if v ≤ w and w ≤ x, then v ≤ x.
-
Totality: either v ≤ w or w ≤ v or both.
注意: The <= operator for double is not a total order ,violates totality: (Double.NaN <= Double.NaN) is false
通用代碼:
// Less. Is item v less than w ?
private static boolean less(Comparable v, Comparable w) {
return v.compareTo(w) < 0;
}
//Exchange. Swap item in array a[] at index i with the one at index j
private static void exch(Comparable[] a,, int i, int j) {
Comparable swap = a[i];
a[i] = a[j];
a[j] = swap;
}
初級排序算法
selection sort(選擇排序)
思路:
-
在第i次迭代中,在剩下的(即未排序的)元素中找到最小的元素
-
將第i個元素與最小的元素交換位置
現象:
-
設已排序的和未排序的交界處為 ↑,則每次循環, ↑ 從左往右移動一個位置
-
↑ 左邊的元素(包括↑)固定了,且升序
-
↑ 右邊的任一元素全部比左邊的所有元素都大
步驟:
-
move the pointer to the right
-
indentify index of minimun entry on right
-
exchange into positon
java實現:
public static void sort(Comparable[] a) {
int N = a.length;
for (int i = 0; i < N; i++) {
int min = i;
for (int j = i+1; j < N; j++) {
if (less(a[j], a[min])) min = j;
}
exch(a, i, min);
}
}
特點:
-
運行時間和輸入無關,無論輸入是已排序,時間復雜度都是O(n^2)
-
數據移動最少,交換的次數和數組大小是線性關系
insertion sort(插入排序)
思路:
-
在第i次迭代中,將第i個元素與每一個它左邊且比它大的的元素交換位置
現象:
-
設已排序的和未排序的交界處為 ↑,則每次循環, ↑ 從左往右移動一個位置
-
↑ 左邊的元素(包括↑)且升序,但位置不固定(因為后續可能會因插入而移動)
-
↑ 右邊的元素還不可見
步驟:
-
Move the pointer to the right.
-
Moving from right to left, exchange a[i] with each larger entry to its left.
java實現:
public static void sort(Comparable[] a) {
int N = a.length;
for (int i = 0; i < N; i++) {
for (int j = i; j > 0 && less(a[j], a[j-1]); j--) {
exch(a, j, j-1);
}
}
}
inversion(倒置):An inversion is a pair of keys that are out of order
部分有序:An array is partially sorted if the number of inversions is ≤ c N.
特點:
-
運行時間和輸入有關,當輸入已排序時,時間復雜度是O(n);
-
For partially-sorted arrays, insertion sort runs in linear time.(交換的次數等于輸入中倒置(inversion)的個數)
-
插入排序適合部分有序數組,也適合小規模數組
ShellSort(希爾排序)
希爾排序是基于插入排序的。
思路:
-
Move entries more than one position at a time by h-sorting the array
-
按照h的步長進行插入排序
現象:
-
數組中任意間隔為h的元素都是有序的
-
A g-sorted array remains g-sorted after h-sorting it.
性質:
-
遞增數列一般采用3x+1:1,4,13,40,121,364.....,使用這種遞增數列的希爾排序所需的比較次數不會超過N的若干倍乘以遞增數列的長度。
-
最壞情況下,使用3x+1遞增數列的希爾排序的比較次數是O(N^(3/2))
java實現:
public static void sort(Comparable[] a) {
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;
}
}
shuffing(不是排序算法)
目標:Rearrange array so that result is a uniformly random permutation
shuffle sort思路
-
為數組的每一個位置生成一個隨機實數
-
排序這個生成的數組
Knuth shuffle demo
-
In iteration i, pick integer r between 0 and i uniformly at random.
-
Swap a[i] and a[r] .
correct variant: between i and N – 1
-
Mergesort--Java sort for objects.
-
Quicksort--Java sort for primitive types.
下面看看這兩種排序算法
merge sort(歸并排序)
思路:
-
Divide array into two halves.
-
Recursivelysort each half.
-
Merge two halves.
Abstract in-place merge(原地歸并的抽象方法)
Given two sorted subarrays a[lo] to a[mid] and a[mid+1] to a[hi],replace with sorted subarray a[lo] to a[hi]
步驟:
-
先將所有元素復制到 aux[] 中,再歸并回 a[] 中。
-
歸并時的四個判斷:
-
左半邊用盡(取右半邊元素)
-
右半邊用盡(取左半邊元素)
-
右半邊的當前元素 小于 左半邊的當前元素(取右半邊的元素)
-
右半邊的當前元素 大于/等于 左半邊的當前元素(取左半邊的元素)
-
merging java實現:
// 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) {
// precondition: a[lo .. mid] and a[mid+1 .. hi] are sorted subarrays
// 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++];
}
}
Top-down mergesort(自頂向下的歸并排序)
mergesort java實現:
// 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); //歸并結果
}
自頂向下的歸并排序的軌跡圖
由圖可知,原地歸并排序的大致趨勢是,先局部排序,再擴大規模;先左邊排序,再右邊排序;每次都是左邊一半局部排完且merge了,右邊一半才開始從最局部的地方開始排序。
改進
-
對小規模子數組使用插入排序
-
測試數組是否已經有序(左邊最大<右邊最小時,直接返回)
-
不將元素復制到輔助數組(節省時間而非空間)
Bottom-up mergesort(自底向上的歸并排序)
思路:
-
先歸并微型數組,從兩兩歸并開始(每個元素理解為大小為1的數組)
-
重復上述步驟,逐步擴大歸并的規模,2,4,8.....
java實現:
public class MergeBU{
private static void merge(...){ /* as before */ }
public static void sort(Comparable[] a){
int N = a.length;
Comparable[] aux = new Comparable[N];
for (int sz = 1; sz < N; sz = sz+sz)
for (int lo = 0; lo < N-sz; lo += sz+sz)
merge(a, aux, lo, lo+sz-1, Math.min(lo+sz+sz-1, N-1));
}
}
自底向上的歸并排序的軌跡圖
由圖可知,自底向上歸并排序的大致趨勢是,先局部排序,逐步擴大到全局排序;步調均勻,穩步擴大
quicksort
思路:
-
Shufflethe array.
-
Partition(切分)so that, for some j
-
entry a[j] is in place
-
no larger entry to the left of j
-
no smaller entry to the right of j
-
Sorteach piece recursively.
其中很重要的一步就是 Partition(切分) ,這個過程使得滿足以下三個條件:
-
對于某個j,a[j]已經排定;
-
a[lo]到a[j-1]中的所有元素都不大于a[j];
-
a[j+1]到a[hi]中的所有元素都不小于a[j];
partition java實現
// 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;
}
快排java實現:
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);
assert isSorted(a, lo, hi);
}
快排的軌跡圖
由圖可知,和歸并排序不同,快排的大致趨勢是,先全局大體有個走勢——左邊比右邊小,逐步細化到局部;也是先左后右;局部完成時全部排序也就完成了。
一些實現的細節:
-
原地切分:不使用輔助數組
-
別越界:測試條件(j == lo)是冗余的(a[lo]不可能比自己小);
-
保持隨機性:初始時的隨機打亂跟重要
-
終止循環
-
處理切分元素值有重復的情況:這里可能出問題
性質:
-
快排是in-place的
-
快排不穩定
改進
-
對小規模子數組使用插入排序
-
三取樣切分
三向切分的快速排序
思路:
-
Let v be partitioning item a[lo].
-
Scan i from left to right.
-
(a[i] < v): exchange a[lt] with a[i]; increment both lt and i
-
(a[i] > v): exchange a[gt] with a[i]; decrement gt
-
(a[i] == v): increment i
主要是通過增加一個指針來實現的。普通的快拍只有lo和high兩個指針,故只能記錄 大于 (high右邊)和 小于 (lo左邊)兩個區間, 等于 只能并入其中一個;這里增加了使用了lt,i,gt三個指針,從而達到記錄 大于 (gt右邊)、 小于 (lt左邊)和 等于 (lt和i之間)三個區間。
三切分的示意圖
三向切分的java實現:
// 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, gt = hi;
Comparable v = a[lo];
int i = 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);
}
Heapsort(堆排序)
思路:
-
Create max-heap with all N keys.
-
Repeatedly remove the maximum key.
-
swim:由下至上的堆有序化
-
sink:由上至下的對有序化
堆排序主要分為兩個階段:
-
堆的構造
-
下沉排序
java實現如下:
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);
}
}
堆排序的軌跡圖
由圖看出,堆排序的趨勢是,堆構造階段,大致是降序的走勢,到了下沉階段,從右到左(或者說從后往前)逐步有序
Significance: In-place sorting algorithm with N log N worst-case.
-
Mergesort: no, linear extra space.
-
Quicksort: no, quadratic time in worst case
缺點
-
Inner loop longer than quicksort’s.
-
Makes poor use of cache memory.
-
Not stable(不穩定)
總結和比較
排序算法總結表
最好情況和最壞情況:參見上面的表格
關于穩定性:
-
穩定性,插入排序,歸并排序
-
不穩定:選擇排序,快排,希爾排序,堆排序
-
原因: Long-distance exchange
關于額外空間:除了歸并排序需要線性的額外空間,其他都是in-place的
命題
-
對于長度為N的數組,選擇排序需要N^2/2次比較和N次交換(pf見P156)
-
對于隨機排列的長度為N的且主鍵不重復的數組(pf見P157)
-
平均情況下插入排序需要~N^2/4次比較和~N^2/4次交換
-
最壞情況下需要~N^2/2次比較和~N^2/2次交換,
-
最好情況下需要N-1次比較和0次交換。
-
-
Mergesort uses at most N lg N compares and 6 N lg N array accesses to sort any array of size N. (pf見P173)
-
Mergesort uses extra space proportional to N.(The array aux[] needs to be of size N for the last merge.)
-
Any compare-based sorting algorithm must use at least lg ( N ! ) ~ N lg N compares in the worst-case.(pf見P177)
-
長度為N的無重復數組排序,快速排序平均需要~2N ln N 次比較(以及1/6即1/3 N ln N的交換)
-
最多需要約N^2/2次比較
-
最少需要~N lg N 次比較
-
-
用下沉操作由N個元素構造堆只需少于2N次比較以及少于N次交換(pf見P206)
-
將N個元素排序,堆排序只需少于(2NlgN+2N)次比較以及一半次數的交換(pf見P208)