排序算法之快速排序

快速排序是C.R.A.Hoare于1962年提出的一种划分交换排序。它采用了一种分治的策略,通常称其为分治法(Divide-and-ConquerMethod)。

分治法的基本思想 
    分治法的基本思想是:将原问题分解为若干个规模更小但结构与原问题相似的子问题。递归地解这些子问题,然后将这些子问题的解组合为原问题的解。

快速排序的基本思想


从待排序的数据序列中任取一个数据(如第一个数据)作为分界值,所有比它小的数据元素一律放到左边,所有比它大的数据元素放到右边。这样一趟后,就形成了两个子序列:左序列中的数据都比分界值小,右序列的数据元素都比分界值大。

然后,对左、右两个子序列递归进行上述操作,直到每个子序列只剩一个元素。

重点:

一、从上面的算法分析可以看出,实现快速排序的关键在于第一趟要做的事情(下方代码中 subSort()方法就是用来完成这三步):

(1)选出指定的基准(PivotKey)

(2)将所有小于分界值的数据元素放在左边

(3)将所有大于分界值的数据元素放到右边

主要是如何实现2、3两步!

二、实现了上方三步后就是对各个分组进行递归,再次求出其基于基值的两个分组,如此往复(下方的 quickSort()方法就是实现这三三步的)

java代码实现:

package fanzhenhua.sort;

import java.util.Arrays;


public class QuickSort {

	public static int subSort(DataWrap[] data,int start,int end){
		//每次进行部分排序的时候(第一次是全部)必须先确定一个基值,一般都取start对应的索引元素
		DataWrap temp=data[start];
		
		//当start<end时循环移动元素
		while(start<end){
			//从最右边开始取第一个小于基值的元素索引
			while(start<end&&data[end].compareTo(temp)>=0){
				end--;
			}
			//将该索引对应的元素移动到基值的左边(第一次移动的时候覆盖基值所在的位置)
			data[start]=data[end];
			//从基值所在的索引开始取第一个大于基值的元素索引
			while(start<end&&data[start].compareTo(temp)<=0){
				start++;
			}
			//将该索引对应的元素移动到基值的右边,覆盖end指向的元素位置
			data[end]=data[start];
			//第一趟结束后start所指向的元素有两个(start本身和end所指向的元素),继续循环移动元素,直到start>=end为止
		}
		//循环结束表示本次移动元素全部结束,此时start和end处于同一个位置,该位置所指向的元素存在两个
		//这两个元素的其中一个已经移动到它所应该存在的位置(基值的左边或者右边),所以start和end
		//所在的位置元素是重复的,也就是基值应该放置的位置
		data[end]=temp;//或者data[start]=temp;
		System.out.println(Arrays.toString(data));
		//此时数组被分成两部分,start的左边都小于基值(temp),start的右边均大于基值
		return start;
	}
	
	public static void quickSort(DataWrap[] data,int start,int end){
		//如果start<end的话则需要递归进行分组排序
		if(start<end){
			//将数组分成两部分,key左边小于基值,key右边大于基值,key值等于数组分完后基值所在的位置
			int key=subSort(data,start,end);
			//对key值左边的数组进行再一次的分组,递归调用
			quickSort(data,start,key-1);
			//对key值右边的数组进行再一次的分组,递归调用
			quickSort(data,key+1,end);
			//递归结束的条件为start>=end
		}
	} 
	public static void main(String[] args) {
        DataWrap[] data = {
                new DataWrap(21, "")
                ,new DataWrap(30, "")
                ,new DataWrap(49, "")
                ,new DataWrap(30, "*")
                ,new DataWrap(16, "")
                ,new DataWrap(9, "")
        };
       
        System.out.println("排序之前:" + Arrays.toString(data));
        quickSort(data, 0, data.length-1);
        System.out.println("排序之后:" + Arrays.toString(data));
    }


}

算法分析:

快速排序的时间主要耗费在划分操作上,对长度为k的区间进行划分,共需k-1次关键字的比较。 
(1)最坏时间复杂度 
    最坏情况是每次划分选取的基准都是当前无序区中关键字最小(或最大)的记录,划分的结果是基准左边的子区间为空(或右边的子区间为空),而划分所得的另一个非空的子区间中记录数目,仅仅比划分前的无序区中记录个数减少一个。
    因此,快速排序必须做n-1次划分,第i次划分开始时区间长度为n-i+1,所需的比较次数为n-i(1≤i≤n-1),故总的比较次数达到最大值: 
               Cmax = n(n-1)/2=O(n2) 
    如果按上面给出的划分算法,每次取当前无序区的第1个记录为基准,那么当文件的记录已按递增序(或递减序)排列时,每次划分所取的基准就是当前无序区中关键字最小(或最大)的记录,则快速排序所需的比较次数反而最多。
(2) 最好时间复杂度 
    在最好情况下,每次划分所取的基准都是当前无序区的"中值"记录,划分的结果是基准的左、右两个无序子区间的长度大致相等。总的关键字比较次数: 
        0(nlgn) 
注意: 
    用递归树来分析最好情况下的比较次数更简单。因为每次划分后左、右子区间长度大致相等,故递归树的高度为O(lgn),而递归树每一层上各结点所对应的划分过程中所需要的关键字比较次数总和不超过n,故整个排序过程所需要的关键字比较总次数C(n)=O(nlgn)。
    因为快速排序的记录移动次数不大于比较的次数,所以快速排序的最坏时间复杂度应为0(n2),最好时间复杂度为O(nlgn)。 
(3)基准关键字的选取 
    在当前无序区中选取划分的基准关键字是决定算法性能的关键。 
①"三者取中"的规则 
    "三者取中"规则,即在当前区间里,将该区间首、尾和中间位置上的关键字比较,取三者之中值所对应的记录作为基准,在划分开始前将该基准记录和该区伺的第1个记录进行交换,此后的划分过程与上面所给的Partition算法完全相同。

②取位于low和high之间的随机数k(low≤k≤high),用R[k]作为基准 
    选取基准最好的方法是用一个随机函数产生一个取位于low和high之间的随机数k(low≤k≤high),用R[k]作为基准,这相当于强迫 R[low..high]中的记录是随机分布的。用此方法所得到的快速排序一般称为随机的快速排序。具体算法【参见教材】
注意: 
     随机化的快速排序与一般的快速排序算法差别很小。但随机化后,算法的性能大大地提高了,尤其是对初始有序的文件,一般不可能导致最坏情况的发生。算法的随机化不仅仅适用于快速排序,也适用于其它需要数据随机分布的算法。
(4)平均时间复杂度 
    尽管快速排序的最坏时间为O(n2),但就平均性能而言,它是基于关键字比较的内部排序算法中速度最快者,快速排序亦因此而得名。它的平均时间复杂度为O(nlgn)。
(5)空间复杂度 
    快速排序在系统内部需要一个栈来实现递归。若每次划分较为均匀,则其递归树的高度为O(lgn),故递归后需栈空间为O(lgn)。最坏情况下,递归树的高度为O(n),所需的栈空间为O(n)。
(6)稳定性 
    快速排序是非稳定的,例如[2,2,1]。
 

参考链接:

https://blog.youkuaiyun.com/bruce_6/article/details/38871187

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值