您的位置:首页 > 其它

快速排序<优化>

2013-03-20 10:22 239 查看
欢迎Java爱好者品读其他算法详解:

简单比较排序:/article/1523578.html

冒泡排序: /article/1523577.html

选择排序: /article/1523576.html

直接插入排序:/article/1523575.html

快速排序: /article/1523574.html

快速排序<优化>
前面介绍过快速排序算法的实现原理,主要是通过函数的递归调用,将一个序列分成基本有序的两个部分,再对分开的两个部分分部进行一样的区分排序,这样知道整个序列达到有序,那么在实现这个,每一次区分就需要一个枢轴值,前面我们直接使用一个待排序序列或子序列的第一个元素作为枢轴值,这种选择是不科学的,比如我们看看下面一组数:
int[] array = { 9, 5, 6, 8, 4, 3, 2, 7, 1 };

此时我们选择 int povit = 9,这样一趟排序下来的结果是:
int[] array = { 1, 5, 6, 8, 4, 3, 2, 7, 9 };

一趟排序:我们发现,只是交换了1和9两个数的位置,这样的效率是不高的,因为我们选取的枢轴值是这个序列中最大的那一个,问题来了,有人说:“如果选取的是这个序列中大小处于中间的那个记录作为枢轴值效率是最高的”,是的,但怎么选取大小处于中间的那个记录呢。当然,如果事先知道顺序,就可定知道了,要是这样,还要排什么序呢,所以这里就是我们要决绝的问题之一,就是怎么样选取一个大小处于整个序列中间或者中间附近的记录呢?

1.枢轴选取的优化:
概率统计学告诉我们,由于序列的随机性,我们也可以采用统计学来解决,因为每一个数处于中间值的概率是相等的,一方面,为了能够选到较靠近中间的,另一方面,又不依赖于序列的长度,即不会造成排序算法的时间复杂度的提升,所以我们可以认为选取几个数,然后比较取中间值,然而这几个数并不能随意取,最好是在序列中的位置有规律,性,通常有两种方法:

三数取中(Median-of-three):取三个关键字先进行排序,将中间数作为枢轴,这三个数是序列的 左,中,右三个位置的记录。
比如刚才那个序列:{ 9, 5, 6, 8, 4, 3, 2, 7, 1 };我们去9,4,1三个数进行比较,然后4作为枢轴记录,这个比9做枢轴记录会更合理。
三数取中的核心优化代码:
int mid = (low + high) / 2;
		//枢轴记录三取一,优化的代码,用于待排序数组元素不是很大的时候
		{
			if (array[low] > array[high]) {
				//交换左端与右端的记录,保证左端较小
				swap(low, high, array);
			}
			if (array[mid] > array[high]) {
				//交换中间与右端的记录,保证中间较小
				swap(mid, high, array);
			}

			if (array[mid] > array[low]) {
				//交换左端与中间的记录,保证左端较小
				swap(mid, low, array);
			}
		}
		int pivotKey = array[low]; // 将三取一后的中间值作为枢轴记录

@用了三个判断,对选取出来三个候选数进行交换排序,然后将中间的那个数赋给pivotKey

整个核心的代码:
/**
* 优化后快速排序的核心程序,枢轴记录用了三取一,去前中后三个数中间那个
*
* @param low
* @param high
* @param array
* @return 返回枢轴记录
*/
private int partition(int low, int high, int... array) {
int mid = (low + high) / 2; //枢轴记录三取一,优化的代码,用于待排序数组元素不是很大的时候 { if (array[low] > array[high]) { //交换左端与右端的记录,保证左端较小 swap(low, high, array); } if (array[mid] > array[high]) { //交换中间与右端的记录,保证中间较小 swap(mid, high, array); } if (array[mid] > array[low]) { //交换左端与中间的记录,保证左端较小 swap(mid, low, array); } } int pivotKey = array[low]; // 将三取一后的中间值作为枢轴记录
while (low < high) {
while (low < high && array[high] >= pivotKey) {
high--;
}
swap(low, high, array); // 把比枢轴记录小的值交换到低端
while (low < high && array[low] <= pivotKey) {
low++;
}
swap(low, high, array); // 把比枢轴记录大的值交换到高端
}
return low; // 返回枢轴记录的下标
}
@就在原来的基础上加上选取左中右三个记录,并进行排序后去中间的数赋给枢轴记录;
三数取中小结:这种方法的随机性也是挺大的,由于选取的数个数比较小,统计出来的数据也是叫不准确的,一般用于待排序序列个数较小时使用,还有一种是九数取中,这个统计的数据更加准确了,原理是一样的,这里不赘述了;

2.交换的优化:
原始的快速排序算中有这样一段代码,也是核心的代码:
int pivotKey = array[low]; // 将数组的第一个元素作为枢轴记录
		while (low < high) {
			while (low < high && array[high] >= pivotKey) {
				high--;
			}
			swap(low, high, array);// 把比枢轴记录小的值交换到低端
			while (low < high && array[low] <= pivotKey) {
				low++;
			}
			swap(low, high, array);// 把比枢轴记录大的值交换到高端
		}


@上面的代码中,从右端开始,只要有比povitKey值小的记录,就交换左右的值,一趟下来,其实仔细挖掘一下这个交换过程,就知道,这种交换是可以省略的,这里引入一个思路:
每一次将枢轴值用一个标记保存起来,然后在后面的比较中:
a.从右端开始,如果有比枢轴记录小的数,此时右端循环终止,不采取交换low和high位置的值,而是直接将high位置的值赋给low位置,high位置的记录没有变,后面进入左端循环;
b.从左端开始,如果有比枢轴记录大的数,此时左端循环终止,同样不采取交换low和high位置的值,而是直接将low位置的值赋给high位置,这时high位置的值是交大的记录,而此刻的low位置的值没有变;
c.一轮左右循环完毕,此时low位置的值是之前那个交大的值,它被赋给high处,这里就需要加一个小步骤,将之前存起来的枢轴记录赋给low位置,然后进入下一轮循环;

优化代码:
int pivotKey = array[low]; // 将三取一后的中间值作为枢轴记录
		while (low < high) {
			while (low < high && array[high] >= pivotKey) {
				high--;
			}
			array[low] = array[high]; // 把比枢轴记录小的值赋给low下标的记录
			while (low < high && array[low] <= pivotKey) {
				low++;
			}
			array[high] = array[low]; // 把比枢轴记录大的值赋给high下标的记录
			array[low] = pivotKey; // 然后将枢轴记录赋给low
		}

@关键处之一:array[low] = array[high]; // 把比枢轴记录小的值赋给low下标的记录;
@关键处之二:array[high] = array[low]; // 把比枢轴记录大的值赋给high下标的记录;
@关键处之三:array[low] = pivotKey; // 然后将枢轴记录赋给low;

整合两种优化:
/**
* 优化后快速排序的核心程序,枢轴记录用了三取一,去前中后三个数中间那个
*
* @param low
* @param high
* @param array
* @return 返回枢轴记录
*/
private int partition(int low, int high, int... array) {
int mid = (low + high) / 2;
//枢轴记录三取一,优化的代码,用于待排序数组元素不是很大的时候
{
if (array[low] > array[high]) {
swap(low, high, array);
}

if (array[mid] > array[high]) {
swap(mid, high, array);
}

if (array[mid] > array[low]) {
swap(mid, low, array);
}
}
int pivotKey = array[low]; // 将三取一后的中间值作为枢轴记录 while (low < high) { while (low < high && array[high] >= pivotKey) { high--; } array[low] = array[high]; // 把比枢轴记录小的值赋给low下标的记录 while (low < high && array[low] <= pivotKey) { low++; } array[high] = array[low]; // 把比枢轴记录大的值赋给high下标的记录 array[low] = pivotKey; // 然后将枢轴记录赋给low }
return low; // 返回枢轴记录的下标
}
@上面采取三数取中和省略不必要的交换两种优化;

3.递归操作的优化:
在数据结构中,有一种结构是栈,函数的递归调用实则是将每一次调用压到栈中,然后按后进先出的原则执行函数中的代码,所以递归对性能有一定的影响,在快速排序中,用到了两次递归,加入待排序的序列是及不平衡的,递归深度趋近于n,与平衡时的log2n的慢得多,另一个栈的大小也是有限的,每一次递归调用都会耗费一定的栈空间,函数的参数也会是空间耗费加大,所以这里可以减少递归的调用,能提高性能:

双递归调用代码:
/**
	 * 快速排序的递归调用
	 * @param low
	 * @param high
	 * @param array
	 */
	private void quickSort(int low, int high, int... array) {
		if (low < high) {
			int pivot = partition(low, high, array); // 找到枢轴记录的下标
			quickSort(low, pivot - 1, array); // 对低子表进行递归排序
			quickSort(pivot + 1, high, array); // 对高子表进行递归排序
		}
	}
@判断条件是if(low < high) 然后,获取枢轴记录,分别对两端进行递归调用,这里可以优化:

尾递归:
/**
	 * 快速排序的尾递归调用
	 * 
	 * @param low
	 * @param high
	 * @param array
	 */
	private void quickSort(int low, int high, int... array) {
		while (low < high) {
			int pivot = partition(low, high, array); // 找到枢轴记录的下标
			quickSort(low, pivot - 1, array); // 对低子表进行递归排序
			low = pivot + 1; // 尾递归
		}
	}
@这一这里相当于方法quickSor在执行一次时,用了while(low < high)这个循环条件,这里就是关键所在
a.第一次进入循环:这个循环执行了一次递归后,然后执行low = pivot + 1;
b.再次进入循环:执行int pivot = partition(low, high, array);,对右端进行递归,因为(low = povit + 1) > (high = povit -1 ),所以后面的quickSort(low, pivot - 1, array);将会终止,然后重复执行a,b;

4.小数组的优化:
当数组较小时,用快速排序似乎有点牛刀杀鸡的感觉,所以灵活运用之前学过的排序算法,引入直接插入排序吧,那么,什么是较小的数组,没有一个统一的标准,我们可以认为给定一个标准,比如50被认为为小数组(也有7的),那么当数组长度小于50时,调用直接插入排序,其余调用快速排序,这个很好理解的,只需加一个判断条件就可以了:

代码实现:
/**
	 * 快速排序的尾递归调用
	 * 
	 * @param low
	 * @param high
	 * @param array
	 */
	private void quickSort(int low, int high, int... array) {
		if (high - low > DIVISION) {//当长度小于分界点时调用快速排序
			while (low < high) {
				int pivot = partition(low, high, array); // 找到枢轴记录的下标
				quickSort(low, pivot - 1, array); // 对低子表进行递归排序
				low = pivot + 1; // 尾递归
			}
		}else {
			insertSort(array);
		}
	}
@DIVISION是一个分界点常量:
/**
	 * 定义一个分界点常量
	 */
	private final int DIVISION = 50;
@insertSort(array):
/**
	 * 直接插入排序的核心程序
	 * @param array
	 */
	public void insertSort(int... array) {
		int length = array.length;
		// 此循环从1开始,就是将0下标的元素当做一个参照
		for (int i = 1; i < length; i++) {
			if (array[i] < array[i - 1]) { // 将当前下标的值与参照元素比较,如果小于就进入里面
				int vacancy = i; // 用于记录比较过程中那个空缺出来的位置
				int sentry = array[i]; // 设置哨兵,将当前下标对应的值赋给哨兵
				// 这个循环很关键,从当前下标之前一个元素开始倒序遍历,比较结果如果比当前大的,就后移
				for (int j = i - 1; j >= 0 && array[j] > sentry; j--) {
					vacancy = j;
					array[j + 1] = array[j]; // 后移比当前元素大的元素
				}
				array[vacancy] = sentry; // 将哨兵,也就是当前下标对应的值置入空缺出来的位置
			}
		}
	}


下面是四种优化版本的完整代码实现:

1.枢轴选取优化:
/**
* 优化枢轴值的快速排序法
*
* @author PingCX
*
*/
public class QuickSortOptPivot {

public static void main(String[] args) {

QuickSortOptPivot qComplete = new QuickSortOptPivot();
int[] array = { 9, 5, 6, 8, 4, 3, 2, 7, 1 };
System.out.println(Arrays.toString(array));
qComplete.quickSort(array);// 调用快速排序的方法
System.out.println(Arrays.toString(array));// 打印排序后的数组元素
System.out.println(qComplete.getMid(25, 90, 13));

}

/**
* 快速排序的入口
*
* @param array
*/
public void quickSort(int... array) {
quickSort(0, array.length - 1, array);
}

/**
* 快速排序的递归调用
*
* @param low
* @param high
* @param array
*/
private void quickSort(int low, int high, int... array) {
if (low < high) {
int pivot = partition(low, high, array); // 找到枢轴记录的下标
quickSort(low, pivot - 1, array); // 对低子表进行递归排序
quickSort(pivot + 1, high, array); // 对高子表进行递归排序
}
}

/**
* 优化后快速排序的核心程序,枢轴记录用了三取一,去前中后三个数中间那个
*
* @param low
* @param high
* @param array
* @return 返回枢轴记录
*/
private int partition(int low, int high, int... array) {
int mid = (low + high) / 2; //枢轴记录三取一,优化的代码,用于待排序数组元素不是很大的时候 { if (array[low] > array[high]) { //交换左端与右端的记录,保证左端较小 swap(low, high, array); } if (array[mid] > array[high]) { //交换中间与右端的记录,保证中间较小 swap(mid, high, array); } if (array[mid] > array[low]) { //交换左端与中间的记录,保证左端较小 swap(mid, low, array); } } int pivotKey = array[low]; // 将三取一后的中间值作为枢轴记录
while (low < high) {
while (low < high && array[high] >= pivotKey) {
high--;
}
swap(low, high, array); // 把比枢轴记录小的值交换到低端
while (low < high && array[low] <= pivotKey) {
low++;
}
swap(low, high, array); // 把比枢轴记录大的值交换到高端
}
return low; // 返回枢轴记录的下标
}

/**
* 内部实现,用于交换数组的两个引用值
*
* @param beforeIndex
* @param afterIndex
* @param arr
*/
private void swap(int oneIndex, int anotherIndex, int[] array) {
int temp = array[oneIndex];
array[oneIndex] = array[anotherIndex];
array[anotherIndex] = temp;
}


2.交换优化:
/**
* 交换优化的快速排序法
*
* @author PingCX
*
*/
public class QuickSortOptExchange {

public static void main(String[] args) {
QuickSortOptExchange qComplete = new QuickSortOptExchange();
int[] array = { 25, 36, 21, 45, 13};
System.out.println(Arrays.toString(array));
qComplete.quickSort(array);// 调用快速排序的方法
System.out.println(Arrays.toString(array));// 打印排序后的数组元素
System.out.println(qComplete.getMid(25, 90, 13));
}

/**
* 快速排序的入口
*
* @param array
*/
public void quickSort(int... array) {
quickSort(0, array.length - 1, array);
}

/**
* 快速排序的递归调用
*
* @param low
* @param high
* @param array
*/
private void quickSort(int low, int high, int... array) {
if (low < high) {
int pivot = partition(low, high, array); // 找到枢轴记录的下标
quickSort(low, pivot - 1, array); // 对低子表进行递归排序
quickSort(pivot + 1, high, array); // 对高子表进行递归排序
}
}

/**
* 优化后快速排序的核心程序,枢轴记录用了三取一,去前中后三个数中间那个
*
* @param low
* @param high
* @param array
* @return 返回枢轴记录
*/
private int partition(int low, int high, int... array) {
int mid = (low + high) / 2;
//枢轴记录三取一,优化的代码,用于待排序数组元素不是很大的时候
{
if (array[low] > array[high]) {
swap(low, high, array);
}

if (array[mid] > array[high]) {
swap(mid, high, array);
}

if (array[mid] > array[low]) {
swap(mid, low, array);
}
}
int pivotKey = array[low]; // 将三取一后的中间值作为枢轴记录 while (low < high) { while (low < high && array[high] >= pivotKey) { high--; } array[low] = array[high]; // 把比枢轴记录小的值赋给low下标的记录 while (low < high && array[low] <= pivotKey) { low++; } array[high] = array[low]; // 把比枢轴记录大的值赋给high下标的记录 array[low] = pivotKey; // 然后将枢轴记录赋给low }
return low; // 返回枢轴记录的下标
}

/**
* 内部实现,用于交换数组的两个引用值
*
* @param beforeIndex
* @param afterIndex
* @param arr
*/
private void swap(int oneIndex, int anotherIndex, int[] array) {
int temp = array[oneIndex];
array[oneIndex] = array[anotherIndex];
array[anotherIndex] = temp;
}


3.递归操作优化:
/**
* 递归优化的快速排序法
*
* @author PingCX
*
*/
public class QuickSortOptRec {

private final int DIVISION = 50;

public static void main(String[] args) {
QuickSortOptRec qComplete = new QuickSortOptRec();
int[] array = { 25, 36, 21, 45, 13 };
System.out.println(Arrays.toString(array));
qComplete.quickSort(array);// 调用快速排序的方法
System.out.println(Arrays.toString(array));// 打印排序后的数组元素
System.out.println(qComplete.getMid(25, 90, 13));
}

/**
* 快速排序的入口
*
* @param array
*/
public void quickSort(int... array) {
quickSort(0, array.length - 1, array);
}

/**
* 快速排序的尾递归调用
*
* @param low
* @param high
* @param array
*/
private void quickSort(int low, int high, int... array) {
if (high - low < DIVISION) {//当长度小于分界点时调用快速排序
while (low < high) {
int pivot = partition(low, high, array); // 找到枢轴记录的下标
quickSort(low, pivot - 1, array); // 对低子表进行递归排序
low = pivot + 1; // 尾递归
}
}
}
/**
* 优化后快速排序的核心程序,枢轴记录用了三取一,去前中后三个数中间那个
*
* @param low
* @param high
* @param array
* @return 返回枢轴记录
*/
private int partition(int low, int high, int... array) {
int mid = (low + high) / 2;
// 枢轴记录三取一,优化的代码,用于待排序数组元素不是很大的时候
{
if (array[low] > array[high]) {
swap(low, high, array);
}

if (array[mid] > array[high]) {
swap(mid, high, array);
}

if (array[mid] > array[low]) {
swap(mid, low, array);
}
}
int pivotKey = array[low]; // 将三取一后的中间值作为枢轴记录 while (low < high) { while (low < high && array[high] >= pivotKey) { high--; } array[low] = array[high]; // 把比枢轴记录小的值赋给low下标的记录 while (low < high && array[low] <= pivotKey) { low++; } array[high] = array[low]; // 把比枢轴记录大的值赋给high下标的记录 array[low] = pivotKey; // 然后将枢轴记录赋给low }
return low; // 返回枢轴记录的下标
}

/**
* 内部实现,用于交换数组的两个引用值
*
* @param beforeIndex
* @param afterIndex
* @param arr
*/
private void swap(int oneIndex, int anotherIndex, int[] array) {
int temp = array[oneIndex];
array[oneIndex] = array[anotherIndex];
array[anotherIndex] = temp;
}


4.最终优化:
/**
* 最终优化的快速排序法
*
* @author PingCX
*
*/
public class QuickSortOptAll {

/** * 定义一个分界点常量 */ private final int DIVISION = 50;

public static void main(String[] args) {
QuickSortOptAll qComplete = new QuickSortOptAll();
int[] array = { 25, 36, 21, 45, 13 };
System.out.println(Arrays.toString(array));
qComplete.quickSort(array);// 调用快速排序的方法
System.out.println(Arrays.toString(array));// 打印排序后的数组元素
System.out.println(qComplete.getMid(25, 90, 13));
}

/**
* 快速排序的入口
*
* @param array
*/
public void quickSort(int... array) {
quickSort(0, array.length - 1, array);
}

/** * 快速排序的尾递归调用 * * @param low * @param high * @param array */ private void quickSort(int low, int high, int... array) { if (high - low > DIVISION) {//当长度小于分界点时调用快速排序 while (low < high) { int pivot = partition(low, high, array); // 找到枢轴记录的下标 quickSort(low, pivot - 1, array); // 对低子表进行递归排序 low = pivot + 1; // 尾递归 } }else { insertSort(array); } }

/**
* 优化后快速排序的核心程序,枢轴记录用了三取一,去前中后三个数中间那个
*
* @param low
* @param high
* @param array
* @return 返回枢轴记录
*/
private int partition(int low, int high, int... array) {
int mid = (low + high) / 2;
// 枢轴记录三取一,优化的代码,用于待排序数组元素不是很大的时候
{
if (array[low] > array[high]) {
swap(low, high, array);
}

if (array[mid] > array[high]) {
swap(mid, high, array);
}

if (array[mid] > array[low]) {
swap(mid, low, array);
}
}
int pivotKey = array[low]; // 将三取一后的中间值作为枢轴记录 while (low < high) { while (low < high && array[high] >= pivotKey) { high--; } array[low] = array[high]; // 把比枢轴记录小的值赋给low下标的记录 while (low < high && array[low] <= pivotKey) { low++; } array[high] = array[low]; // 把比枢轴记录大的值赋给high下标的记录 array[low] = pivotKey; // 然后将枢轴记录赋给low }
return low; // 返回枢轴记录的下标
}

/** * 直接插入排序的核心程序 * @param array */ public void insertSort(int... array) { int length = array.length; // 此循环从1开始,就是将0下标的元素当做一个参照 for (int i = 1; i < length; i++) { if (array[i] < array[i - 1]) { // 将当前下标的值与参照元素比较,如果小于就进入里面 int vacancy = i; // 用于记录比较过程中那个空缺出来的位置 int sentry = array[i]; // 设置哨兵,将当前下标对应的值赋给哨兵 // 这个循环很关键,从当前下标之前一个元素开始倒序遍历,比较结果如果比当前大的,就后移 for (int j = i - 1; j >= 0 && array[j] > sentry; j--) { vacancy = j; array[j + 1] = array[j]; // 后移比当前元素大的元素 } array[vacancy] = sentry; // 将哨兵,也就是当前下标对应的值置入空缺出来的位置 } } }
/**
* 内部实现,用于交换数组的两个引用值
*
* @param beforeIndex
* @param afterIndex
* @param arr
*/
private void swap(int oneIndex, int anotherIndex, int[] array) {
int temp = array[oneIndex];
array[oneIndex] = array[anotherIndex];
array[anotherIndex] = temp;
}


尾声:人生是一场赛跑,你有超过他人的时候,也有被他人超过的时候,关键看谁能跑到终点,起点低点没有关系,即使暂时落后与别人,只要静下心来思索,探索出一条适合自己的路,并快速前进...

欢迎Java爱好者品读其他算法详解:

简单比较排序:/article/1523578.html

冒泡排序: /article/1523577.html

选择排序: /article/1523576.html

直接插入排序:/article/1523575.html

快速排序: /article/1523574.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: