快速排序算法原理及实现(单轴快速排序、三向切分快速排序、双轴快速排序)

欢迎探讨,若有错误敬请指正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

相关文章
相关标签/搜索