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

欢迎探讨,如有错误敬请指正

如需转载,请注明出处http://www.cnblogs.com/nullzx/

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指向同一个坑)。最后我们将中轴元素放到这个坑中。最后对左半数组和右半数组重复上述操作。

	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]表示未知元素

初始化时,选取最左边的元素作为中轴元素,A[1,i]表示小于等于pivot的部分,i指向中轴元素(i < 1),表示小于等于pivot的元素个数为0,j以后的都是未知元素(即不知道比pivot大,还是比中轴元素小),j初始化指向第一个未知元素。

当A[j]大于pivot时,j继续向前,此时大于pivot的部分就增加一个元素

上图中假设对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,又回到了先前的状态。

上图中假设对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的部分。

我们选取最左边的元素作为中轴元素,初始化时,i = L,k = L+1,j=R(L表示最左边元素的索引,R表示最右边元素的索引)

通过上一段的表述可知,初始化时<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的部分重复上述操作即可。

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三个变量将数组分成四部分

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]这三个部分继续递归上述操作即可。

	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先增加,k扫描中pivot1就不参与其中
				Swap(A, i, k);
				k++;
			}else
			if(A[k] 大于等于pivot1 && A[k] 小于等于pivot2){
				k++;
			}else{
				while(A[--j] > pivot2){//j先增减,j首次扫描pivot2就不参与其中
					if(j <= k){//当i和j相遇
						break OUT_LOOP;
					}
				}
				if(A[j] 大于等于pivot1 && A[j] 小于等于pivot2){
					Swap(A, k, j);
					k++;
				}else{
					i++;
					Swap(A, i, j);
					Swap(A, j, 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);
	}

5.参考文章

[1] 算法(第四版)RobertSedgewick

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

时间: 2024-11-16 02:52:17

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

快速排序算法原理及其js实现

要说快排的原理,通俗点说就是把一个事情,分成很多小事情来处理,分治的思想. 假设我们现在对"6  1  2 7  9  3  4  5 10  8"这10个数进行排序.首先在这个序列中随便找一个数作为基准数(不要被这个名词吓到了,就是一个用来参照的数,待会你就知道它用来做啥的了).一般选第一个数6作为基准数.接下来,需要将这个序列中所有比基准数大的数放在6的右边,比基准数小的数放在6的左边,类似下面这种排列: 3  1  2 5  4  [6 ] 9 7  10  8 在初始状态下,数

排序算法原理及实现

算法一:直接插入排序 算法实现原理:就是计算一个新元素是应该放在哪里?每次进来一个都会进行和原来顺序进行重新组合. 代码实现:Java public int[] testInsertionSort(int[] data){ // this methord is very easy. for(int i = 1;i < data.length;i++){ int temp = data[i]; int j =i; while(j>0 && data[j-1]>temp){

排序算法系列:快速排序算法

概述 在前面说到了两个关于交换排序的算法:冒泡排序与奇偶排序. 本文就来说说交换排序的最后一拍:快速排序算法.之所以说它是快速的原因,不是因为它比其他的排序算法都要快.而是从实践中证明了快速排序在平均性能上的确是比其他算法要快一些,不然快速一说岂不是在乱说? 本文就其原理.过程及实现几个方面讲解一下快速排序算法. 版权声明 著作权归作者所有. 商业转载请联系作者获得授权,非商业转载请注明出处. 作者:Coding-Naga 发表日期:2016年3月1日 链接:http://blog.csdn.n

关于快速排序算法(一个90%的人都不懂其原理、99.9%的人都不能正常写出来的算法.)

一.奇怪的现象 研究快速排序很久了,发现一个古怪的实情:这算法描述起来很简单,写一个正确的出来实在不容易.写一个优秀的快速排序算法更是难上加难. 也难怪该算法提出来过了很久才有人写出一个正确的算法,过了很久才优秀的版本出来. 二.原理描述 从数列中挑出一个元素,称为 "基准"(pivot), 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边).在这个分区退出之后,该基准就处于数列的中间位置.这个称为分区(partition)操作

《机器学习算法原理与编程实践》学习笔记(三)

(上接第一章) 1.2.5 Linalg线性代数库 在矩阵的基本运算基础之上,NumPy的Linalg库可以满足大多数的线性代数运算. .矩阵的行列式 .矩阵的逆 .矩阵的对称 .矩阵的秩 .可逆矩阵求解线性方程 1.矩阵的行列式 In [4]: from numpy import * In [5]: #n阶矩阵的行列式运算 In [6]: A = mat([[1,2,3],[4,5,6],[7,8,9]]) In [7]: print "det(A):",linalg.det(A)

最全排序算法原理解析、java代码实现以及总结归纳

算法分类 十种常见排序算法可以分为两大类: 非线性时间比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此称为非线性时间比较类排序. 线性时间非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此称为线性时间非比较类排序. 详情如下: 算法评估 排序算法的性能依赖于以下三个标准: 稳定性:如果a原本在b前面,而a=b,排序之后a仍然在b的前面,则稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在

【特征匹配】RANSAC算法原理与源码解析

转载请注明出处:http://blog.csdn.net/luoshixian099/article/details/50217655 随机抽样一致性(RANSAC)算法,可以在一组包含"外点"的数据集中,采用不断迭代的方法,寻找最优参数模型,不符合最优模型的点,被定义为"外点".在图像配准以及拼接上得到广泛的应用,本文将对RANSAC算法在OpenCV中角点误匹配对的检测中进行解析. 1.RANSAC原理 OpenCV中滤除误匹配对采用RANSAC算法寻找一个最佳

最短路径A*算法原理及java代码实现(看不懂是我的失败)

算法只要懂原理了,代码都是小问题,先看下面理论,尤其是红色标注的(要源码请留下邮箱,有测试用例,直接运行即可) A*算法 百度上的解释: A*[1](A-Star)算法是一种静态路网中求解最短路最有效的直接搜索方法. 公式表示为: f(n)=g(n)+h(n), 其中 f(n) 是从初始点经由节点n到目标点的估价函数, g(n) 是在状态空间中从初始节点到n节点的实际代价, h(n) 是从n到目标节点最佳路径的估计代价. 保证找到最短路径(最优解的)条件,关键在于估价函数f(n)的选取: 估价值

【Oracle 集群】ORACLE DATABASE 11G RAC 知识图文详细教程之RAC 工作原理和相关组件(三)

RAC 工作原理和相关组件(三) 概述:写下本文档的初衷和动力,来源于上篇的<oracle基本操作手册>.oracle基本操作手册是作者研一假期对oracle基础知识学习的汇总.然后形成体系的总结,一则进行回顾复习,另则便于查询使用.本图文文档亦源于此.阅读Oracle RAC安装与使用教程前,笔者先对这篇文章整体构思和形成进行梳理.由于阅读者知识储备层次不同,我将从Oracle RAC安装前的准备与规划开始进行整体介绍安装部署Oracle RAC.始于唐博士指导,对数据库集群进行配置安装,前