思路
歸并排序的思想是先將數(shù)組分散為小數(shù)組分別排序,然后將結(jié)果歸并起來(lái)。
原地歸并的抽象方法
將兩個(gè)已經(jīng)排序好的數(shù)組歸并為一個(gè)數(shù)組這一操作對(duì)于歸并排序的意義不言而喻,以下是歸并方法的實(shí)現(xiàn):
public static void merge(Comparable[] a, int low, int mid, int high){
int i = low, j = mid+1;
for (int k = low; k <= high; k++)
indexA[k] = a[k];
for (int k = low; k <= high; k++){
if (i > mid) a[k] = indexA[j++];
else if (j > high) a[k] = indexA[i++];
else if (less(indexA[j], indexA[i])) a[k] = indexA[j++];
else a[k] = indexA[i++];
}
}
自頂向下的歸并排序
基于原地歸并的抽象方法實(shí)現(xiàn)了另一種遞歸歸并,這是應(yīng)用高效算法設(shè)計(jì)中分治思想的典型例子:
public class Merge {
private static Comparable[] indexA;
public static void merge(Comparable[] a, int low, int mid, int high){
int i = low, j = mid+1;
for (int k = low; k <= high; k++)
indexA[k] = a[k];
for (int k = low; k <= high; k++){
if (i > mid) a[k] = indexA[j++];
else if (j > high) a[k] = indexA[i++];
else if (less(indexA[j], indexA[i])) a[k] = indexA[j++];
else a[k] = indexA[i++];
}
}
public static void sort(Comparable[] a, int low, int high){
if (indexA==null) indexA = new Comparable[a.length];
if (high <= low) return;
int mid = low + (high-low)/2;
sort(a, low, mid);
sort(a, mid+1, high);
merge(a, low, mid, high);
}
private static boolean less(Comparable v, Comparable w){
return v.compareTo(w) < 0;
}
private static void exchange(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++)
System.out.print(a[i]);
System.out.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;
}
}
在實(shí)際運(yùn)用中,希爾排序和歸并排序的運(yùn)行時(shí)間之差在常數(shù)范圍內(nèi)
自底向上的歸并排序
實(shí)現(xiàn)歸并排序的另一種方式是從小數(shù)組開(kāi)始?xì)w并:首先我們將數(shù)組的每一個(gè)元素都當(dāng)做一個(gè)只有一個(gè)元素的數(shù)組,然后將其兩兩歸并。然后我們將整個(gè)數(shù)組的每?jī)蓚€(gè)元素都當(dāng)做一個(gè)小數(shù)組,然后將其兩兩歸并,然后四個(gè)四個(gè)歸并,依次類推,直到最后歸并成一個(gè)大數(shù)組,排序就完成了。
完整實(shí)現(xiàn)代碼如下:
public class MergeBU {
private static Comparable[] indexA;
public static void merge(Comparable[] a, int low, int mid, int high){
int i = low, j = mid+1;
for (int k = low; k <= high; k++)
indexA[k] = a[k];
for (int k = low; k <= high; k++){
if (i > mid) a[k] = indexA[j++];
else if (j > high) a[k] = indexA[i++];
else if (less(indexA[j], indexA[i])) a[k] = indexA[j++];
else a[k] = indexA[i++];
}
}
public static void sort(Comparable[] a){
if (indexA == null) indexA = new Comparable[a.length];
for (int sz = 1; sz<a.length; sz = sz+sz)
for (int low = 0; low < a.length-sz; low += sz+sz)
merge(a,low,low+sz-1,Math.min(low+sz+sz-1, a.length-1));
}
public static void main(String[] args){
Integer[] a = {9,8,7,6,5,4,3,2,1};
sort(a);
for (Integer i: a){
System.out.println(i);
}
}
private static boolean less(Comparable v, Comparable w){
return v.compareTo(w) < 0;
}
private static void exchange(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++)
System.out.print(a[i]);
System.out.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;
}
}
兩種實(shí)現(xiàn)方式的速度比較(給一千個(gè)大小為一萬(wàn)的數(shù)組排序):
快速排序
快速排序可能是應(yīng)用的最為廣泛的一種算法,它流行的原因是實(shí)現(xiàn)簡(jiǎn)單,適用于各種不同的輸入數(shù)據(jù)且在一般的應(yīng)用中比其他排序算法都要快的多。快速排序的優(yōu)點(diǎn):
是原地排序(只需要一個(gè)很小的輔助棧)。
所需時(shí)間跟NlgN成正比。
快速排序思路:
快速排序和歸并排序是互補(bǔ)的,歸并排序?qū)⒄麄€(gè)數(shù)組分成小數(shù)組,然后將排好序的小數(shù)組歸并以將整個(gè)數(shù)組排序;而快速排序是在將大數(shù)組分成小數(shù)組的時(shí)候排序,當(dāng)小數(shù)組小到不可再分的時(shí)候,排序也就完成了。
1.首先選擇一個(gè)中間元素(一般選左端或者右端)。
2.分別獲取除中間元素外的左右兩端的索引。
3.由左右兩端逐漸向中間迭代,每迭代一步比較一下索引中的元素和中間元素,當(dāng)左邊出現(xiàn)比中間元素大的元素的時(shí)候,暫停左邊的迭代,當(dāng)右邊迭代出比中間元素小的元素的時(shí)候,右邊迭代也暫停,交換左右兩邊的元素。
4.重復(fù)步驟3,直到左右兩邊的索引相遇,然后將中間元素移動(dòng)到中間,這時(shí)中間元素左邊的元素都比它小,右邊的元素都比它大。
5.將上面的中間元素左右兩邊當(dāng)成兩個(gè)數(shù)組,分別進(jìn)行上述過(guò)程。
6.重復(fù)以上步驟直到數(shù)組不可再分。
完整代碼
public class Quick {
public static void sort(Comparable[] a){
sort(a,0,a.length-1);
}
private static void sort(Comparable[] a,int low, int high){
if (high <= low) return;
int j = partition(a,low,high);
sort(a,low,j-1);
sort(a,j+1,high);
}
private static int partition(Comparable[] a, int low, int high){
//將數(shù)組切分為a[lo..i-1], a[i], a[i+1..hi]
int i= lo, j = hi+1; //左右掃描指針
Comparable v = a[lo];//切分元素
while (true)
{//掃描左右,檢查掃描是否結(jié)束并交換元素
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); //將v = a[j]放入正確位置
return j; //a[lo..j-1] <= a[j] <= a[j+1..hi]達(dá)成
}
public static void main(String[] args){
Integer[] a = {9,8,7,6,5,4,3,2,1};
sort(a, 0, a.length-1);
for (Integer i: a){
System.out.println(i);
}
}
private static boolean less(Comparable v, Comparable w){
return v.compareTo(w) < 0;
}
private static void exchange(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++)
System.out.print(a[i]);
System.out.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;
}
}
比較快速排序和歸并排序的速度
命令:
% java Main Merge Quick 10000 1000
快速排序算法的改進(jìn)
快速排序自被C.A.R Hoare在1960年發(fā)明后,就不斷的有人試圖改進(jìn)它,但是由于快速排序已經(jīng)“so well-balanced”,改進(jìn)所帶來(lái)的優(yōu)化往往都被副作用抵消了,比如《算法》一書(shū)中對(duì)快速排序的實(shí)現(xiàn)中在排序之前會(huì)先隨機(jī)打亂數(shù)組來(lái)避免一種極端情況——當(dāng)我們每一次選擇的中間元素都恰好是最小元素時(shí),該算法會(huì)變的像選擇排序,從而導(dǎo)致時(shí)間復(fù)雜度變成N的平方。但是筆者在測(cè)試的時(shí)候發(fā)現(xiàn)運(yùn)行上面的指令時(shí)隨機(jī)打亂數(shù)組所花掉的時(shí)間幾乎使得運(yùn)行時(shí)間加倍,而事實(shí)上出現(xiàn)這種極端情況的概率比你的電腦在排序時(shí)突然被閃電擊中的概率都要小的多(這個(gè)flag不是我立的,我以后不隨隨便便排序了)。
但是依然有人找到了一些有用的改進(jìn)方式:
1.第一種改進(jìn)方案是說(shuō)由于插入排序在小數(shù)組的時(shí)候會(huì)比快速排序快,所以在分成小數(shù)組的時(shí)候使用插入排序,然而筆者在自己的電腦上測(cè)試的時(shí)候發(fā)現(xiàn)無(wú)論是大數(shù)組還是小數(shù)組,快速排序都比插入排序要快得多,按照這種方式修改的快速排序也變慢了,所以存疑。
2.實(shí)際應(yīng)用中我們排序的數(shù)組常常含有大量的重復(fù)元素,例如將上千萬(wàn)人員的資料按照生日排序,那就必然會(huì)有大量的重復(fù)的數(shù)值(畢竟一百年里面也就四萬(wàn)多天,分配給上千萬(wàn)人作生日,自然有大量重復(fù)),于是有人提出與其將數(shù)組二分,不如分成三部分,一部分小于中間值,一部分大于中間值,一部分等于中間值,此算法被稱為三向切分的快速排序,以下是代碼:
public class Quick3Way {
public static void sort(Comparable[] a){
sort(a,0,a.length-1);
}
private static void sort(Comparable[] a,int low, int high){
if (high <= low) return;
int lt = low, i = low+1,gt = high;
Comparable v = a[low];
while (i <= gt){
int cmp = a[i].compareTo(v);
if (cmp < 0)exchange(a,lt++,i++);
else if (cmp > 0)exchange(a,i,gt--);
else i++;
}
sort(a,low,lt-1);
if (gt<high) sort(a,gt+1,high);
}
public static void main(String[] args){
Integer[] a = {9,8,7,6,5,4,3,2,1};
sort(a, 0, a.length-1);
for (Integer i: a){
System.out.println(i);
}
}
private static boolean less(Comparable v, Comparable w){
return v.compareTo(w) < 0;
}
private static void exchange(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++)
System.out.print(a[i]);
System.out.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;
}
}
當(dāng)使用隨機(jī)生成的由0~100組成的大小為一萬(wàn)的數(shù)組排序一千次時(shí)改進(jìn)方法與原方法所花費(fèi)的時(shí)間如下:
對(duì)于標(biāo)準(zhǔn)的快速排序,隨著數(shù)組規(guī)模的增大其運(yùn)行時(shí)間會(huì)趨于平均運(yùn)行時(shí)間,大幅度偏離的情況非常罕見(jiàn)。而三向切分的快速排序?qū)τ诖罅恐貜?fù)元素的數(shù)組來(lái)說(shuō)運(yùn)行時(shí)間由線性對(duì)數(shù)級(jí)別降低到了線性級(jí)別,并且和元素的排列沒(méi)有關(guān)系。由于在平常使用中對(duì)含有大量重復(fù)元素的數(shù)組排序的情況很常見(jiàn),所以擁有對(duì)重復(fù)元素的適應(yīng)性的三向分切的快速排序成為了排序庫(kù)函數(shù)的最佳選擇。
經(jīng)過(guò)精心優(yōu)化的快速排序在絕大多數(shù)計(jì)算機(jī)的絕大多數(shù)應(yīng)用中都比其他算法要快,它在當(dāng)前業(yè)界的廣泛使用正說(shuō)明了這一點(diǎn)。
總結(jié)一下當(dāng)前學(xué)習(xí)過(guò)的排序算法的速度:
在給千萬(wàn)級(jí)別的數(shù)組排序的情況下:
Quick > Merge > Shell > Insertion > Selection
資源以及參考
普林斯頓大學(xué)算法課程以其教材《算法》第四版