快速排序

就像归并之于归并排序,划分是快速排序的核心

划分数据即将数据项分为两组,使全部值小于特定值的数据项在一组,使全部值大于特定值的数据项在另一组

划分算法由两个指针开始,两个指针分别指向数组2端,相向移动,当左侧指针遇到大于特定值的数据项时,停止移动,当右侧指针遇到小于特定值的数据项时,停止移动,此时交换两个指针所指数据项的值,交换完成后,两个指针再次相向移动,直到两个指针之间没有其它数据项或两个指针之间只有一个数据项

public class Partition {
	private int[] data;
	
	public Partition(int[] data){
		this.data = data;
	}
	
	//返回右侧数组中第一个数据项在整个数组中的位置
	public int part(int left, int right, int keyNum){
		while(true){
			while(left < right &&
					data[left] < keyNum)
				left++;	
			while(right > left &&
					data[right] >= keyNum)
				right--;
			if(right <= left)
				break;
			swap(left, right);
		}
		return right;
	}
	
	private void swap(int left, int right){
		int temp = data[right];
		data[right] = data[left];
		data[left] = temp;
	}
	
	public void display(){
		for(int i = 0 ; i < data.length ; i++){
			System.out.print(data[i] + " ");
		}
		System.out.print("\n");
	}
	
	public static void main(String[] args){
		int[] ints = {17, 53, 44, 9, 12, 86, 144, 72, 22, 30};
		//int[] ints = {1,2,3};
		int keyNum = ints[ints.length-1];
		Partition p = new Partition(ints);
		System.out.println(p.part(0, ints.length-1, keyNum));
		p.display();
	}
}
4
17 22 12 9 44 86 144 72 53 30 

划分算法在数组长度很短(比如数组长度为1、2或3)的时候也可以正常工作

划分算法的效率:

数据比较次数:左右指针相遇时比较结束,故需比较N次

数据交换次数:交换次数小于比较次数,但确切的交换次数取决于数组的排列及keyNum的大小

故总的时间复杂度为O(N)

快速排序:把一个数组划分为两个子数组,然后递归地调用自身为每一个子数组进行快速排序

取每个待划分数组右端数据项的值作为本次划分的keyNum

public class QuickSort {
	private int[] data;
	
	public QuickSort(int[] data){
		this.data = data;
	}
	
	public void sort(){
		this.reSort(0, data.length - 1);
	}
	
	private void reSort(int left, int right){
		if(right <= left)
			return;
		int n = this.part(left, right);
		//显示局部排序结果
		this.display();
		//递归调用当前方法为划分出来的子数组进行排序
		//排序过程最后会将keyNum移动至右端数组的最左端
		//故在当前排序结束后keyNum是有序的
		//不需要再对keyNum进行排序
		reSort(left, n - 1);
		reSort(n + 1, right);
	}
	
	private int part(int left, int right){
		int tail = right;
		int keyNum = data[right];
		//当前数组最右端的数据已被选为keyNum
		//不需要参与比较过程
		right--;
		while(true){
			while(left < right && 
					data[left] < keyNum)
				left++;
			while(right > left &&
					data[right] > keyNum)
				right--;
			if(right == left)
				break;
			swap(left,right);
		}
		//整个当前数组都小于等于keyNum
		//这时候不需要移动keyNum的位置
		//因为整个右端数组只包含keyNum这一个数据项
		if(data[right] > keyNum)
			swap(right,tail);
		return right;
	}
	
	private void swap(int left, int right){
		int temp = data[right];
		data[right] = data[left];
		data[left] = temp;
	}
	
	public void display(){
		for(int i = 0 ; i < data.length ; i++){
			System.out.print(data[i] + " ");
		}
		System.out.print("\n");
	}

	public static void main(String[] args) {
		int[] data = {42, 89, 63, 12, 94, 27, 78, 3, 50, 36};
		//int[] data = {1};
		QuickSort qs = new QuickSort(data);
		qs.sort();
		System.out.println("result:");
		qs.display();
	}
}

排序过程如下图所示:

3 27 12 36 94 89 78 42 50 63 
3 12 27 36 94 89 78 42 50 63 
3 12 27 36 50 42 63 89 94 78 
3 12 27 36 42 50 63 89 94 78 
3 12 27 36 42 50 63 78 94 89 
3 12 27 36 42 50 63 78 89 94 
result:
3 12 27 36 42 50 63 78 89 94 

当每次划分都能将数组分为两个大小相等的子数组时,快速排序的效率是最高的,为O(N*logN),因为此时完成排序所需的总划分次数是最少的

考虑如下情况:如果数组为逆序,则选择待划分数组右端数据项作为keyNum将导致快速排序退化为冒泡排序,此时快速排序的效率将变为O(N*N)

所以keyNum的选择对于快速排序来说是非常重要的,keyNum应尽量接近待划分数组数据项的平均值,避免出现keyNum是待划分数组最大数据项或最小数据项的情况

三数据项取中:

public class QuickSort {
	private int[] data;
	
	public QuickSort(int[] data){
		this.data = data;
	}
	
	public void sort(){
		this.reSort(0, data.length - 1);
	}
	
	private void reSort(int left, int right){
		//对于长度少于3的待划分数组,使用冒牌排序
		if(right - left + 1 <= 3){
			shortPart(left, right);
//			System.out.print("normal:");
//			display();
		}else{
			//通过三数据项取中的方式得到keyNum
			int p = getMiddleNum(left, right);
			int keyNum = data[p];
			//普通划分过程
			int n = part(left, right, keyNum);
			display();
			reSort(left, n - 1);
			reSort(n + 1, right);
		}
	}
	
	private void shortPart(int left, int right){
		//待划分数组长度为1
		if(right - left == 0)
			return;
		//待划分数组长度为2
		else if(right - left == 1){
			if(data[right] < data[left])
				swap(right, left);
		}else{
			int middle = left + 1;
			if(data[left] > data[middle])
				swap(left, middle);
			if(data[middle] > data[right])
				swap(middle, right);
			if(data[left] > data[middle])
				swap(left, middle);
		}
	}
	
	//将3个数据项中的中间值作为keyNum
	//并将keyNum移动至3个数据项的中间位置
	//避免划分出的2个子数组一个过大一个过小
	//如果不将keyNum移动至中间位置,在有些情况下
	//会导致划分次数变多,排序效率降低
	private int getMiddleNum(int left, int right){
		int middle = (left + right)/2;
		if(data[left] > data[middle])
			swap(left, middle);
		if(data[middle] > data[right])
			swap(middle, right);
		if(data[left] > data[middle])
			swap(left, middle);
		return middle;
	}
	
	private int part(int left, int right, int keyNum){
		while(true){
			while(left < right && 
					data[left] < keyNum)
				left++;
			while(right > left &&
					data[right] >= keyNum)
				right--;
			if(right == left)
				break;
			swap(left,right);
		}
		return right;
	}
	
	private void swap(int left, int right){
		int temp = data[right];
		data[right] = data[left];
		data[left] = temp;
	}
	
	public void display(){
		for(int i = 0 ; i < data.length ; i++){
			System.out.print(data[i] + " ");
		}
		System.out.print("\n");
	}

	public static void main(String[] args) {
		int[] data = {42, 89, 63, 12, 94, 27, 78, 3, 50, 36};
		QuickSort qs = new QuickSort(data);
		qs.sort();
		System.out.println("result:");
		qs.display();
	}
}

排序过程如下图所示:

36 3 27 12 42 63 78 89 50 94 
3 12 27 36 42 63 78 89 50 94 
3 12 27 36 42 63 78 50 89 94 
result:
3 12 27 36 42 50 63 78 89 94 

可以看到确实比使用待划分数组右端数据项的值作为keyNum所需的划分次数要少

 

PS:

写这个算法费了不少力气,主要是考虑的太过复杂,对于一个长度很长的数据,多1-2次的比较对效率是没什么影响的,反而会增加编码的复杂度,就像递归,递归的效率肯定没有循环的效率高,但是递归可以降低问题的复杂性

posted @ 2014-04-22 23:52  心意合一  阅读(223)  评论(0编辑  收藏  举报