快速排序算法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);
}

此外,我们还可以消除递归、用内嵌代码代替函数、使用观察哨等方式继续对程序进行改进,这里就不详细介绍了。


全文完。

  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值