快速排序算法原理及實現(單軸快速排序、三向切分快速排序、雙軸快速排序)

歡迎探討,若有錯誤敬請指正html

如需轉載,請註明出處http://www.cnblogs.com/nullzx/java

1. 單軸快速排序的基本原理

快速排序的基本思想就是從一個數組中任意挑選一個元素(一般來講會選擇最左邊的元素)做爲中軸元素,將剩下的元素以中軸元素做爲比較的標準,將小於等於中軸元素的放到中軸元素的左邊,將大於中軸元素的放到中軸元素的右邊,而後以當前中軸元素的位置爲界,將左半部分子數組和右半部分子數組當作兩個新的數組,重複上述操做,直到子數組的元素個數小於等於1(由於一個元素的數組一定是有序的)。算法

如下的代碼中會經常使用交換數組中兩個元素值的Swap方法,其代碼以下數組

public static void Swap(int[] A, int i, int j){
	int tmp;
	tmp = A[i];
	A[i] = A[j];
	A[j] = tmp;
}

2. 快速排序中元素切分的方式

快速排序中最重要的就是步驟就是將小於等於中軸元素的放到中軸元素的左邊,將大於中軸元素的放到中軸元素的右邊,咱們暫時把這個步驟定義爲切分。而剩下的步驟就是進行遞歸而已,遞歸的邊界條件爲數組的元素個數小於等於1。以首元素做爲中軸,看看常見的切分方式。單元測試

2.1 從兩端掃描交換的方式

基本思想,使用兩個變量i和j,i指向首元素的元素下一個元素(最左邊的首元素爲中軸元素),j指向最後一個元素,咱們從前日後找,直到找到一個比中軸元素大的,而後從後往前找,直到找到一個比中軸元素小的,而後交換這兩個元素,直到這兩個變量交錯(i > j)(注意不是相遇 i == j,由於相遇的元素還未和中軸元素比較)。最後對左半數組和右半數組重複上述操做。測試

	public static void QuickSort1(int[] A, int L, int R){
		if(L < R){//遞歸的邊界條件,當 L == R時數組的元素個數爲1個
			int pivot = A[L];//最左邊的元素做爲中軸,L表示left, R表示right
			int i = L+1, j = R;
			//當i == j時,i和j同時指向的元素尚未與中軸元素判斷,
			//小於等於中軸元素,i++,大於中軸元素j--,
			//當循環結束時,必定有i = j+1, 且i指向的元素大於中軸,j指向的元素小於等於中軸
			while(i <= j){
				while(i <= j && A[i] <= pivot){
					i++;
				}
				while(i <= j && A[j] > pivot){
					j--;
				}
				//當 i > j 時整個切分過程就應該中止了,不能進行交換操做
				//這個能夠改爲 i < j, 這裏 i 永遠不會等於j, 由於有上述兩個循環的做用
				if(i <= j){
					Swap(A, i, j);
					i++;
					j--;
				}
			}
			//當循環結束時,j指向的元素是最後一個(從左邊算起)小於等於中軸的元素
			Swap(A, L, j);//將中軸元素和j所指的元素互換
			QuickSort1(A, L, j-1);//遞歸左半部分
			QuickSort1(A, j+1, R);//遞歸右半部分
		}
	}

 

2.2 兩端掃描,一端挖坑,另外一端填補

基本思想,使用兩個變量i和j,i指向最左邊的元素,j指向最右邊的元素,咱們將首元素做爲中軸,將首元素複製到變量pivot中,這時咱們能夠將首元素i所在的位置當作一個坑,咱們從j的位置從右向左掃描,找一個小於等於中軸的元素A[j],來填補A[i]這個坑,填補完成後,拿去填坑的元素所在的位置j又能夠看作一個坑,這時咱們在以i的位置從前日後找一個大於中軸的元素來填補A[j]這個新的坑,如此往復,直到i和j相遇(i == j,此時i和j指向同一個坑)。最後咱們將中軸元素放到這個坑中。最後對左半數組和右半數組重複上述操做。ui

	public static void QuickSort2(int[] A, int L, int R){
		if(L < R){
			//最左邊的元素做爲中軸複製到pivot,這時最左邊的元素能夠看作一個坑
			int pivot = A[L];
			//注意這裏 i = L,而不是 i = L+1, 由於i表明坑的位置,當前坑的位置位於最左邊
			int i = L, j = R;
			while(i < j){
				//下面面兩個循環的位置不能顛倒,由於第一次坑的位置在最左邊
				while(i < j && A[j] > pivot){
					j--;
				}
				//填A[i]這個坑,填完後A[j]是個坑
				//注意不能是A[i++] = A[j],當因i==j時跳出上面的循環時
				//坑爲i和j共同指向的位置,執行A[i++] = A[j],會致使i比j大1,
				//但此時i並不能表示坑的位置
				A[i] = A[j];
				
				while(i < j && A[i] <= pivot){
					i++;
				}
				//填A[j]這個坑,填完後A[i]是個坑,
				//同理不能是A[j--] = A[i]				
				A[j] = A[i];
			}
			//循環結束後i和j相等,都指向坑的位置,將中軸填入到這個位置
			A[i] = pivot;
			
			QuickSort2(A, L, i-1);//遞歸左邊的數組
			QuickSort2(A, i+1, R);//遞歸右邊的數組
		}
	}

 

2.3 單端掃描方式

j從左向右掃描,A[1,i]表示小於等於pivot的部分,A[i+1,j-1]表示大於pivot的部分,A[j, R]表示未知元素spa

image

初始化時,選取最左邊的元素做爲中軸元素,A[1,i]表示小於等於pivot的部分,i指向中軸元素(i < 1),表示小於等於pivot的元素個數爲0,j之後的都是未知元素(即不知道比pivot大,仍是比中軸元素小),j初始化指向第一個未知元素。3d

image

當A[j]大於pivot時,j繼續向前,此時大於pivot的部分就增長一個元素htm

image

上圖中假設對A[j]與pivot比較後發現A[j]大於pivot時,j的變化

當A[j]小於等於pivot時,咱們注意注意i的位置,i的下一個就是大於pivot的元素,咱們將i增長1而後交換A[i]和A[j],交換後小於等於pivot的部分增長1,j增長1,繼續掃描下一個。而i的下一個元素仍然大於pivot,又回到了先前的狀態。

image

上圖中假設對A[j]與pivot比較後發現A[j] <= pivot時,i,j的變化

	public static void QuickSort3(int[] A, int L, int R){
		if(L < R){
			int pivot = A[L];//最左邊的元素做爲中軸元素
			//初始化時小於等於pivot的部分,元素個數爲0
			//大於pivot的部分,元素個數也爲0
			int i = L, j = L+1;
			while(j <= R){
				if(A[j] <= pivot){
					i++;
					Swap(A, i, j);
					j++;//j繼續向前,掃描下一個
				}else{
					j++;//大於pivot的元素增長一個
				}
			}
			//A[i]及A[i]之前的都小於等於pivot
			//循環結束後A[i+1]及它之後的都大於pivot
			//因此交換A[L]和A[i],這樣咱們就將中軸元素放到了適當的位置
			Swap(A, L, i);
			QuickSort3(A, L, i-1);
			QuickSort3(A, i+1, R);
		}
	}

3. 三向切分的快速排序

三向切分快速排序的基本思想,用i,j,k三個將數組切分紅四部分,a[L, i-1]表示小於pivot的部分,a[i, k-1]表示等於pivot的部分,a[j+1]表示大於pivot的部分,而a[k, j]表示未斷定的元素(即不知道比pivot大,仍是比中軸元素小)。咱們要注意a[i]始終位於等於pivot部分的第一個元素,a[i]的左邊是小於pivot的部分。

image

咱們選取最左邊的元素做爲中軸元素,初始化時,i = L,k = L+1,j=R(L表示最左邊元素的索引,R表示最右邊元素的索引)

image

 

經過上一段的表述可知,初始化時<pivot部分的元素個數爲0,等於pivot部分元素的個數爲1,大於pivot部分的元素個數爲0,這顯然符合目前咱們對所掌握的狀況。k自左向右掃描直到k與j錯過爲止(k > j)。咱們掃描的目的就是逐個減小未知元素,並將每一個元素按照和pivot的大小關係放到不一樣的區間上去。

在k的掃描過程當中咱們能夠對a[k]分爲三種狀況討論

(1)a[k] < pivot 交換a[i]和a[k],而後i和k都自增1,k繼續掃描

(2)a[k] = pivot k自增1,k接着繼續掃描

(3)a[k] > pivot 這個時候顯然a[k]應該放到最右端,大於pivot的部分。可是咱們不能直接將a[k]與a[j]交換,由於目前a[j]和pivot的關係未知,因此咱們這個時候應該從j的位置自右向左掃描。而a[j]與pivot的關係能夠繼續分爲三種狀況討論

        3.1)a[j] > pivot j自減1,j接着繼續掃描

       3.2)a[j] == pivot 交換a[k]和a[j],k自增1,j自減1,k繼續掃描(注意此時j的掃描就結束了)

       3.3)a[j] < pivot: 此時咱們注意到a[j] < pivot, a[k] > pivot, a[i] == pivot,那麼咱們只須要將a[j]放到a[i]上,a[k]放到a[j]上,而a[i]放到a[k]上。而後i和k自增1,j自減1,k繼續掃描(注意此時j的掃描就結束了)

注意,當掃描結束時,i和j的表示了=等於pivot部分的起始位置和結束位置。咱們只須要對小於pivot的部分以及大於pivot的部分重複上述操做便可。

image

public static void QuickSort3Way(int[] A, int L, int R){
	if(L >= R){//遞歸終止條件,少於等於一個元素的數組已有序
		return;
	}
	
	int i,j,k,pivot;
	pivot = A[L]; //首元素做爲中軸
	i = L;
	k = L+1;
	j = R;
	
	OUT_LOOP:
	while(k <= j){
		if(A[k] < pivot){
			Swap(A, i, k);
			i++;
			k++;
		}else
		if(A[k] == pivot){
			k++;
		}else{// 遇到A[k]>pivot的狀況,j從右向左掃描
			while(A[j] > pivot){//A[j]>pivot的狀況,j繼續向左掃描
				j--;
				if(j < k){
					break OUT_LOOP;
				}
			}
			if(A[j] == pivot){//A[j]==pivot的狀況
				Swap(A, k, j);
				k++;
				j--;
			}else{//A[j]<pivot的狀況
				Swap(A, i, j);
				Swap(A, j, k);
				i++;
				k++;
				j--;
			}
		}
	}
	//A[i, j] 等於 pivot 且位置固定,不須要參與排序
	QuickSort3Way(A, L, i-1); // 對小於pivot的部分進行遞歸
	QuickSort3Way(A, j+1, R); // 對大於pivot的部分進行遞歸
} 

4. 雙軸快速排序

雙軸快速排序算法思路和三向切分快速排序算法的思路基本一致,雙軸快速排序算法使用兩個軸,一般選取最左邊的元素做爲pivot1和最右邊的元素做pivot2。首先要比較這兩個軸的大小,若是pivot1 > pivot2,則交換最左邊的元素和最右邊的元素,已保證pivot1 <= pivot2。雙軸快速排序一樣使用i,j,k三個變量將數組分紅四部分

image

 

A[L+1, i]是小於pivot1的部分,A[i+1, k-1]是大於等於pivot1且小於等於pivot2的部分,A[j, R]是大於pivot2的部分,而A[k, j-1]是未知部分。和三向切分的快速排序算法同樣,初始化i = L,k = L+1,j=R,k自左向右掃描直到k與j相交爲止(k == j)。咱們掃描的目的就是逐個減小未知元素,並將每一個元素按照和pivot1和pivot2的大小關係放到不一樣的區間上去。

在k的掃描過程當中咱們能夠對a[k]分爲三種狀況討論(注意咱們始終保持最左邊和最右邊的元素,即雙軸,不發生交換)

(1)a[k] < pivot1 i先自增,交換a[i]和a[k],k自增1,k接着繼續掃描

(2)a[k] >= pivot1 && a[k] <= pivot2 k自增1,k接着繼續掃描

(3)a[k] > pivot2: 這個時候顯然a[k]應該放到最右端大於pivot2的部分。但此時,咱們不能直接將a[k]與j的下一個位置a[--j]交換(能夠認爲A[j]與pivot1和pivot2的大小關係在上一次j自右向左的掃描過程當中就已經肯定了,這樣作主要是j首次掃描時避免pivot2參與其中),由於目前a[--j]和pivot1以及pivot2的關係未知,因此咱們這個時候應該從j的下一個位置(--j)自右向左掃描。而a[--j]與pivot1和pivot2的關係能夠繼續分爲三種狀況討論

       3.1)a[--j] > pivot2 j接着繼續掃描

       3.2)a[--j] >= pivot1且a[j] <= pivot2 交換a[k]和a[j],k自增1,k繼續掃描(注意此時j的掃描就結束了)

       3.3) a[--j] < pivot1 先將i自增1,此時咱們注意到a[j] < pivot1,  a[k] > pivot2,  pivot1 <= a[i] <=pivot2,那麼咱們只須要將a[j]放到a[i]上,a[k]放到a[j]上,而a[i]放到a[k]上。k自增1,而後k繼續掃描(此時j的掃描就結束了)

注意

1. pivot1和pivot2在始終不參與k,j掃描過程。

2. 掃描結束時,A[i]表示了小於pivot1部分的最後一個元素,A[j]表示了大於pivot2的第一個元素,這時咱們只須要交換pivot1(即A[L])和A[i],交換pivot2(即A[R])與A[j],同時咱們能夠肯定A[i]和A[j]所在的位置在後續的排序過程當中不會發生變化(這一步很是重要,不然可能引發無限遞歸致使的棧溢出),最後咱們只須要對A[L, i-1],A[i+1, j-1],A[j+1, R]這三個部分繼續遞歸上述操做便可。

 

image

	public static void QuickSortDualPivot(int[] A, int L, int R){
		if(L >= R){
			return;
		}
		
		if(A[L] > A[R]){
			Swap(A, L, R); //保證pivot1 <= pivot2
		}
		
		int pivot1 = A[L];
		int pivot2 = A[R];
		
		//若是這樣初始化 i = L+1, k = L+1, j = R-1,也能夠
		//但代碼中邊界條件, i,j先增減,循環截止條件,遞歸區間的邊界都要發生相應的改變
		int i = L;
		int k = L+1;
		int j = R;

		OUT_LOOP:
		while(k < j){
			if(A[k] < pivot1){
				i++;//i先增長,首次運行pivot1就不會發生改變
				Swap(A, i, k);
				k++;
			}else
			if(A[k] >= pivot1 && A[k] <= pivot2){
				k++;
			}else{
				while(A[--j] > pivot2){//j先增減,首次運行pivot2就不會發生改變
					if(j <= k){//當k和j相遇
						break OUT_LOOP;
					}
				}
				if(A[j] >= pivot1 && A[j] <= pivot2){
					Swap(A, k, j);
					k++;
				}else{
					i++;
					Swap(A, j, k);
					Swap(A, i, k);
					k++;
				}
			}
		}
		Swap(A, L, i);//將pivot1交換到適當位置
		Swap(A, R, j);//將pivot2交換到適當位置
		
		//一次雙軸切分至少肯定兩個元素的位置,這兩個元素將整個數組區間分紅三份
		QuickSortDualPivot(A, L, i-1);
		QuickSortDualPivot(A, i+1, j-1);
		QuickSortDualPivot(A, j+1, R);
	}

 

 

 

下面代碼初始化方式使得邊界條件更加容易肯定,在下面代碼的方式中A[L+1]~A[i-1]表示小於pivot1的部分,A[i]~A[j]表示兩軸之間的部分,A[j]~A[R-1]表示大於pivot2的部分。

當排序數組中不存在pivot1~pivot2中的部分時,一趟交換完成後i剛好比j大1;當排序數組中僅僅存在一個元素x使得pivot1 <=x <=pivot2時,一趟交換完成,i和j剛好相等。

 

	public static void QuickSortDualPivot(int[] A, int L, int R){
		
		if(L >= R){
			return;
		}
		
		if(A[L] > A[R]){
			Swap(A, L, R); //保證pivot1 <= pivot2
		}
		
		int pivot1 = A[L];
		int pivot2 = A[R];
		
		int i = L+1;
		int k = L+1;
		int j = R-1;

		OUT_LOOP:
		while(k <= j){
			if(A[k] < pivot1){
				Swap(A, i, k);
				k++;
				i++;
			}else
			if(A[k] >= pivot1 && A[k] <= pivot2){
				k++;
			}else{
				while(A[j] > pivot2){
					j--;
					if(j < k){//當k和j錯過
						break OUT_LOOP;
					}
				}
				if(A[j] >= pivot1 && A[j] <= pivot2){
					Swap(A, k, j);
					k++;
					j--;
				}else{//A[j] < pivot1
					Swap(A, j, k);//注意k不動
					j--;
				}
			}
		}
		i--;
		j++;
		Swap(A, L, i);//將pivot1交換到適當位置
		Swap(A, R, j);//將pivot2交換到適當位置
		
		//一次雙軸切分至少肯定兩個元素的位置,這兩個元素將整個數組區間分紅三份
		QuickSortDualPivot(A, L, i-1);
		QuickSortDualPivot(A, i+1, j-1);
		QuickSortDualPivot(A, j+1, R);
	}

 

補上一個單元測試

package test;

import java.util.Arrays;

import static datastruct.QuickSortForInt.*;

import org.junit.Assert;
import org.junit.Test;

public class QuickSortForIntTest {

	@Test
	public void testQuickSortDualPivot() {
		
		int[] a;
		int[] r;
		
		a = new int[]{1};
		r = a.clone();
		QuickSortDualPivot(a, 0, a.length-1);
		Arrays.sort(r);
		Assert.assertArrayEquals(a, r);
		
		a = new int[]{2, 1};
		r = a.clone();
		QuickSortDualPivot(a, 0, a.length-1);
		Arrays.sort(r);
		Assert.assertArrayEquals(a, r);
		
		a = new int[]{1, 1};
		r = a.clone();
		QuickSortDualPivot(a, 0, a.length-1);
		Arrays.sort(r);
		Assert.assertArrayEquals(a, r);
		
		a = new int[]{1, 1, 1};
		r = a.clone();
		QuickSortDualPivot(a, 0, a.length-1);
		Arrays.sort(r);
		Assert.assertArrayEquals(a, r);
		
		a = new int[]{2, 1, 2};
		r = a.clone();
		QuickSortDualPivot(a, 0, a.length-1);
		Arrays.sort(r);
		Assert.assertArrayEquals(a, r);
		
		a = new int[]{1, 2, 3};
		r = a.clone();
		QuickSortDualPivot(a, 0, a.length-1);
		Arrays.sort(r);
		Assert.assertArrayEquals(a, r);
		
		a = new int[]{3, 2, 1};
		r = a.clone();
		QuickSortDualPivot(a, 0, a.length-1);
		Arrays.sort(r);
		Assert.assertArrayEquals(a, r);
		
		/*pivot1 = 3, pivot2 = 5, 並且沒有3~5之間的元素*/
		a = new int[]{3, 1, 6, 2, 7, 5};
		r = a.clone();
		QuickSortDualPivot(a, 0, a.length-1);
		Arrays.sort(r);
		Assert.assertArrayEquals(a, r);
		
		/*pivot1 = 3, pivot2 = 5, 只有一個元素4在3到5之間*/
		a = new int[]{3, 1, 6, 4, 7, 5};
		r = a.clone();
		QuickSortDualPivot(a, 0, a.length-1);
		Arrays.sort(r);
		Assert.assertArrayEquals(a, r);
		
		/*pivot1 = 3, pivot2 = 5, 全部元素都大於5*/
		a = new int[]{3, 6, 7, 5};
		r = a.clone();
		QuickSortDualPivot(a, 0, a.length-1);
		Arrays.sort(r);
		Assert.assertArrayEquals(a, r);
		
		/*pivot1 = 3, pivot2 = 5, 全部元素都小於3*/
		a = new int[]{5, 2, 1, 3};
		r = a.clone();
		QuickSortDualPivot(a, 0, a.length-1);
		Arrays.sort(r);
		Assert.assertArrayEquals(a, r);
	}

}

 

5. 參考文章

[1] 算法(第四版)RobertSedgewick

[2] http://www.jianshu.com/p/6d26d525bb96

相關文章
相關標籤/搜索