快速排序算法Java詳解

快速排序是一種分治排序的算法,將數組劃分爲兩個部分,然後分別對兩個部分進行排序。在實際應用中,一個經過仔細調整的快速排序算法應該在大多數計算機上運行的比其他排序算法要快的多,對於大型文件,快速排序的性能是希爾排序的5到10倍,它還能更搞笑的處理在實際問題中遇到的其他類型的文件。所以快速排序是在找工作面試中被問到的最多的一個排序算法,比如快速排序的基本思想、時間複雜度、穩定性、快速排序的改進等。

這篇文章主要介紹快速排序的基本算法及其優化等。關於其他基本的排序算法見:基本排序算法Java詳解


1 快速排序的基本算法

快速排序的基本思想是:通過一趟排序將待排序的記錄分隔成獨立的兩個部分,其中一部分記錄的關鍵字均比另一部分記錄的關鍵字小,接着分別對兩部分分別進行同樣的操作,最終得到有序的結果。

一趟快速排序的具體做法如下代碼:變量v作爲一個旗幟(樞軸),保存了元素items[r],i和j分別從左邊和右邊向內部掃描,掃描過程中保證:i的左邊沒有比v大的,j的右邊沒有比v小的。一旦兩個指針相遇,就交換a[i]和a[r],即將v賦值給a[i],這樣v左側的元素都小於等於v,v右邊的元素都大於等於v,結束了劃分過程。

private int partition(int[] items, int l, int r) {
	int i = l - 1, j = r;
	int v = items[r];
	while(true) {
		while(items[++i] < v);
		while(v < items[--j])
			if(j ==l) break; //防止劃分元素v是文件中最小的元素
		if(i >= j) break;
		exchange(items, i, j);
	}
	exchange(items, i, r);
	return i;
}

其元素下標形式如下圖所示:


根據上述代碼,一趟排序的示意圖如下圖所示:


上述只是爲一趟快速排序的過程,其整個快速排序的過程可以採用遞歸形式,遞歸形式的快速排序算法如下所示:

public void sort(int[] items, int l, int r) {
	if(l >= r) return; //返回,不用排序
	
	int i = partition(items, l, r);
	
	sort(items, l, i-1); //遞歸排序
	sort(items, i+1, r); //遞歸排序
}

快速排序最壞的時間複雜度爲O(n^2),平均時間複雜度爲O(nlogn)。


2 快速排序非遞歸算法(棧)

快速排序的遞歸算法使用一個由程序自動創建的隱式棧,非遞歸算法使用顯式棧。

快速排序過程中首先把數組的後部和前部的下標推入棧,如下圖的7和0兩個下標進棧。然後進入循環:取出棧的兩個元素,將這兩個 元素作爲數組下標,對這段數組中的數 進行一趟快速排序,排序後再把兩部分的前後下標壓入棧,如下圖的5、7、0、3進棧。


在實際應用中,爲了使棧的大小保證在lgN範圍內,在入棧時會檢測兩邊文件的大小,把較大的一邊優先入棧,較小的一邊後入棧(在下面代碼中有體現)。

利用棧的非遞歸快速快速排序代碼如下:

public void sort_stack(int[] items, int l, int r) {
	int i;
	push2(r, l); //向棧推入r和l
	
	while(!stackempty()) { //只要棧不空就一直循環
		l = pop(); r = pop();
		if(r <= l) continue;
		i = partition(items, l, r);
		//較大的一側先入棧,可以保證棧的最大深度在lgN以內
		if(i-l > r-i) {
			push2(i-1, l); push2(r, i+1);
		} else {
			push2(r, i+1); push2(i-1, l);
		}
	}
}


其完整代碼如下:

public class QuickSort {
	private Stack<Integer> stack = new Stack<Integer>();
	
	/**
	 * 利用棧的非遞歸快速排序
	 * @param items
	 * @param l
	 * @param r
	 */
	public void sort_stack(int[] items, int l, int r) {
		int i;
		push2(r, l); //向棧推入r和l
		
		while(!stackempty()) { //只要棧不空就一直循環
			l = pop(); r = pop();
			if(r <= l) continue;
			i = partition(items, l, r);
			//較大的一側先入棧,可以保證棧的最大深度在lgN以內
			if(i-l > r-i) {
				push2(i-1, l); push2(r, i+1);
			} else {
				push2(r, i+1); push2(i-1, l);
			}
		}
	}
	
	//依次向棧推入a和b
	private void push2(int a, int b) {
		stack.push(a);
		stack.push(b);
	}
	
	private boolean stackempty() {
		return stack.isEmpty();
	}
	
	private int pop() {
		return stack.pop();
	}

	//對下標從l到r之間的items進行處理:
	private int partition(int[] items, int l, int r) {
		int i = l - 1, j = r;
		int v = items[r];
		while(true) {
			while(items[++i] < v);
			while(v < items[--j])
				if(j ==l) break;
			if(i >= j) break;
			exchange(items, i, j);
		}
		exchange(items, i, r);
		return i;
	}
	
	//交換
	private void exchange(int[] items, int a, int b) {
		int t;
		t = items[a];
		items[a] = items[b];
		items[b] = t;
	}
	
	//測試
	public static void main(String[] args) {
		int[] items = {12, 21, 13, 12, 11, 15, 17, 22};
		
		QuickSort qs = new QuickSort();
		qs.sort_stack(items, 0, items.length-1);
		
		for(int i=0; i<items.length; i++) {
			System.out.print(items[i] + " ");
		}
	}
}


3 快速排序的改進

3.1 小的子文件

快速排序在針對大文件(數組長度很長)有很大的優勢,但是對於小文件其優勢將被削弱。對於基本的快速排序中,當遞歸到後面時程序會調用自身的許多小文件,因而在遇到子文件時儘可能使用好的方法,來對快速排序進行改進。一種方法是在遞歸開始前進行測試,當文件太小時就用其他排序方式,即將return改爲調用插入排序(小文件使用插入排序較好,根據自於《算法:C語言實現》)。如下:

if(r-l <= M) insertionSort(items, l, r);  //根據《算法:C語言實現》的實驗驗證,M取10爲宜,insertionSort()爲插入排序

考慮小的子文件後的優化的快速排序代碼爲:

private static final int M = 10;
private void quickSort(int[] items, int l, int r) {
	if(l >= r) return; //不用排序
	if(r - l <= M) return; //******小的文件,不用排序*****
	
	int i = partition(items, l, r);
	
	quickSort(items, l, i-1); //遞歸排序
	quickSort(items, i+1, r); //遞歸排序
}

public void sort(int[] items, int l, int r) {
	quickSort(items, l, r);
	insertionSort(items, l, r); //*****插入排序*****
}
(其中的插入排序算法可參見:基本排序算法Java詳解

3.2 三者取中算法改進

由於快速排序在記錄有序或基本有序時,將退化爲冒泡排序,其事件複雜度爲O(n^2)。解決辦法就是使用盡一個可能在文件中間劃分的元素。可採用”三者取中“的法則來選擇旗幟(樞軸)記錄,即比較數組的左邊元素(items[l])、中間元素(items[(l+r)/2])和右邊元素(item[r]),取三者中中間大小的元素作爲旗幟(樞軸)記錄。

三者取中算法在下面幾個方面進行了改進。首先,它使得最壞情況在實際排序中幾乎不可能發生。其次,它減少了劃分對觀察哨的需要。最後,它使總的平均運行時間大約減少了5%。(這段話來自於《算法:C語言實現》)

三者取中法和小的子文件優化結合起來可以將原始的遞歸實現的快速排序算法運行時間提高20%~25%(根據《算法:C語言實現》)。下面這段代碼就是三者取中和小的子文件相結合的優化算法:


private void quickSort(int[] items, int l, int r) {
	if(l >= r) return; //不用排序
	
	if(r - l <= M) return; //小的文件,被忽略
	exchange(items, (l+r)/2, r-1);
	compexch(items, l, r-1);
	compexch(items, l, r);
	compexch(items, r-1, r);
	//經過以上三步就完成了對l、(l+r)/2、r三個元素的排序。((l+r)/2元素放在r-1位置上)
	
	//與普通的快速排序也有不同,劃分的時候第l個和第r個不用考慮了
	//因爲第l個元素一定小於“旗幟(樞軸)元素”,第r個元素一定大於“旗幟(樞軸)元素”
	//“旗幟(樞軸)元素”在r-1上。
	int i = partition(items, l+1, r-1);
	
	quickSort(items, l, i-1); //遞歸排序
	quickSort(items, i+1, r); //遞歸排序
}
	
public void sort(int[] items, int l, int r) {
	quickSort(items, l, r);
	insertionSort(items, l, r); //插入排序
}
其中的輔助函數compexch()如下:

private void exchange(int[] items, int a, int b) {
	int t;
	t = items[a];
	items[a] = items[b];
	items[b] = t;
}

private void compexch(int[] items, int a, int b) {
	if(items[b] < items[a])
		exchange(items, a, b);
}

此外,我們還可以消除遞歸、用內嵌代碼代替函數、使用觀察哨等方式繼續對程序進行改進,這裏就不詳細介紹了。


全文完。

發佈了52 篇原創文章 · 獲贊 17 · 訪問量 19萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章