排序黄金双雄——归并排序&快速排序

       上一篇讲到了冒泡&选择&插入排序,从有序的定理角度分析了三种排序算法的原理。基本思想就是遍历所有元素,让所有元素满足有序的定理。 所以冒泡&选择&插入排序逃不过双重循环,第一层循环遍历每一个元素,第二层循环和其他元素比较遍历,渐近时间复杂度都为O(n^2)。本篇介绍将介绍归并排序&快速排序,依旧从有序的定理出发解析这两个算法的原理及实现。本文的重点在小结部分,本文的重点在小结部分,本文的重点在小结部分,重要的事情说三遍!!! 文章较长建议收藏起来慢慢看,会让你所有收获。

1、 归并排序

1.1、原理

       归并排序(Merge Sort)所遵循的有序定理是命题1(任何一个元素不小于其存在的左边元素。。核心思想是将数组分为两大部分A&B,A中的所有元素总小于B中的元素,AB两部分满足命题1,再将AB两部分分别再以同样的方式分A’&B’,重复该操作直至A&B两部分中都仅存一个元素 。这样每一个元素都满足命题1,整个数组有序。
       Merge操作是归并的核心操作,目的在于将数组分为两大部分A&B,A中的所有元素总小于B中的元素。下面是Merge的代码实现:

	private static void merge(Comparable<?>[] a, int lo, int mid ,int hi) {
		int i = lo, j = mid+1;
		for(int k = lo; k <= hi; k++) aux[k] = a[k];  
		//注意:++,先赋值计算后自加!!!
		for(int k = lo; k <= hi; k++)
			if      (i > mid)              a[k] = aux[j++];   //左半边用尽,取右半边元素
			else if (j > hi)               a[k] = aux[i++];   //右半边用尽,取左半边元素
			else if (less(aux[j], aux[i])) a[k] = aux[j++];   //左半边元素小于右半边元素,取左半边元素
			else                           a[k] = aux[i++];   //右半边元素小于左半边元素,取右半边元素
	}
  • 自顶向下的归并排序:
    首先介绍自顶向下的归并,假设存在如下图的长度为10的乱序数组。首先分为两大部分AB,并交换其中的元素使A中的元素总小于B中的元素,称该操作为Merge。
    初始乱序数组
    执行第一次Merge有如下效果:
    第一次Merge
    蓝色区域A中的任意元素均小于红色区B的元素。接着对蓝色A区域的部分Merge,分半向下取整。对红色B区域的部分同理Merge。
    A区域的Merge效果
    两部分Merge后的整体效果如下:
    第二次整体Merge
    第三次Merge已经可以到单独元素了,原理同上不做赘述。
    第三次整体Merge
    最后一次Merge得到有序的数组。
    归并排序完成
    至此自顶向下的归并排序算法结束。为什么叫自顶向下呢?注意第一个分AB的Merge操作,对象是整个数组,然后重复操作直至单个元素。
  • 自底向上的归并排序:
    自底向上的归并排序和自顶向下想法相反,先从每一个元素开始Merge。假设一个长度为10的乱序数组如下:
    原始乱序数组
    两两配对并Merge,得到如下的效果:
    两两Merge
    接着以1×2个元素作为最小单元两两Merge,得到如下的效果:
    以2作为基数的Merge
    然后以2×2个元素作为最小单元两两Merge,得到如下的效果:
    以4作为基数的Merge
    最后以2×4作为最小单元Merge,结束归并排序:
    最后一次以8作为Merge基数

1.2 Java实现

package sort;

public class MergeSort extends AbstractSort{
	/*
	 * -归并算法-
	 * 一种分治的思想,由多化少,步骤繁琐但单一步骤极简。
	 * 需要开辟数组,并不是原地交换式。
	 * 		时间复杂度:O(NlgN)
	 */

	public MergeSort() {
	}
	
	private static Comparable<?>[] aux;
	//最小递归构成(核心)
	private static void merge(Comparable<?>[] a, int lo, int mid ,int hi) {
		int i = lo, j = mid+1;
		for(int k = lo; k <= hi; k++) aux[k] = a[k];  
		//注意:++,先赋值计算后自加!!!
		for(int k = lo; k <= hi; k++)
			if      (i > mid)              a[k] = aux[j++];   //左半边用尽,取右半边元素
			else if (j > hi)               a[k] = aux[i++];   //右半边用尽,取左半边元素
			else if (less(aux[j], aux[i])) a[k] = aux[j++];   //左半边元素小于右半边元素,取左半边元素
			else                           a[k] = aux[i++];   //右半边元素小于左半边元素,取右半边元素
	}
	//自顶向下递归,递归从顶部向下解开去分治
	public static void sort(Comparable<?>[] a, int lo, int hi) {
		if (hi <= lo) return;
		int mid = lo+(hi-lo)/2;
		sort(a, lo, mid);
		sort(a, mid+1, hi);
		merge(a, lo, mid, hi);
	}
	//自低向上非递归,从1开始,倍数合并,最后凑出完整数组
	public static void sort(Comparable<?>[] a) {
		int N = a.length;
		aux = new Comparable[N];
		for (int sz = 1; sz <N; sz = sz+sz) {
			for (int lo=0; lo<N-sz; lo += sz+sz) {
				merge(a, lo, lo+sz-1, Math.min(lo+sz+sz-1, N-1));
			}
		}
	}
	
	public static void main(String[] args) {
		Integer[] a = {2,4,5,8,53,3,14,24,35,23,45};
//		aux = new Comparable[a.length];
//		MergeSort.sort(a, 0, a.length-1);
		MergeSort.sort(a);
		show(a);
		System.out.println(isSort(a));
	}
}

1.3 性能分析

       对于长度为N的任意数组,归并排序需要1/2NlgN~NlgN次比较,渐近时间复杂度为O(NlgN),空间复杂度N,不属于原地排序。

2、 快速排序

2.1、原理

       快速排序(Quick Sort)所遵循的有序定理是命题3(有序数组其任何一个元素不小于其左边元素且不大于右边元素。。核心思想是Partition操作:将数组由一个元素a切分为两部分AB,a大于A中的任意元素且小于B中的任意元素,选取第二个元素a在A部分中,再将a’以同样方式插入A的A’&B’两部分,对B执行相同操作;重复该操作直至A&B两部分中都仅存一个元素 。这样每一个元素都满足命题3,整个数组有序。
       Partition操作是归并的核心操作,目的在于把数组的第一个元素a插入切分好的两大部分A&B,A中的所有元素总小于a并且B中的元素总大与a。下面是Partition的代码实现:

	//寻找和第一个元素互换的位置
	public static int partition(Comparable<?>[] a, int lo, int hi) {
		int i = lo, j = hi + 1;
		Comparable<?> v = a[lo];
		while(true) {
			//寻找左边比v大的那一位,记录到i,停止
			while(less(a[++i], v)) if(i == hi) break;
			//寻找右边比v小的那一位,记录到j,停止
			while(less(v, a[--j])) if(j == lo) break;
			if (i>=j) break;
			//a[i]>v>a[j],所以要交换i和j的元素
			exch(a, i, j);
		}
		exch(a, lo, j);
		return j;
	} 

下面以图示方式讲解Partition的流程。以如下数组为例,选取第一个元素a(红色),从左至右找违背小于a的元素然后停止并标记,从右至左寻找违背大于a的元素然后标记并停止,我把它称作“冲突”。
第一次冲突
交换两个违背的绿色元素,继续寻找直至下一次“冲突”,然后交换“冲突”的两个元素。
第二次冲突
继续寻找冲突,发现两个箭头相遇了,这个时候停止寻找,把蓝色箭头的和元素a交换。
终止寻找并交换
这个时候以红色元素a所在的位置将数组切分为两部分,前一部分A的任意元素小于a并且后一部分B的元素大于a,对a来说满足有序的命题3。
理解Partition操作后再来看快速排序,快速排序类似于归并都运用了分治的思想,不停的拆分整体让小部分满足有序,最后拆分到小部分是单个元素时就意味着每个元素都满足有序。那么接着上面的数组继续以图示说明,对A与B部分再次Partiton:
第二次Partition
这里额外说一点,可以看到A部分中的第一个元素是插入到了最后,这种情况是不是越来越像冒泡了?对!这个隐患会导致快排退化成冒泡。 后面在性能上继续介绍。接着继续递归深入Partition。中间过程不再展示了(懒),最后就排序完成了。
排序结束

2.2 Java实现

package sort;

public class QuickSort extends AbstractSort{
	
	/*
	 *  -快速排序-
	 *  是一种分治算法,类似于归并排序,但不需要开辟数组!!
	 *  
	 *  根据排序问题的命题3:
	 *  有序数组其任何一个元素不小于其左边元素且不大于右边元素。
	 *  由顶自下,让一个元素满足命题3,递归下去,则所有元素满足命题3
	 */
	
	public QuickSort() {
	}
	
	//寻找和第一个元素互换的位置
	public static int partition(Comparable<?>[] a, int lo, int hi) {
		int i = lo, j = hi + 1;
		Comparable<?> v = a[lo];
		while(true) {
			//寻找左边比v大的那一位,记录到i,停止
			while(less(a[++i], v)) if(i == hi) break;
			//寻找右边比v小的那一位,记录到j,停止
			while(less(v, a[--j])) if(j == lo) break;
			if (i>=j) break;
			//a[i]>v>a[j],所以要交换i和j的元素
			exch(a, i, j);
		}
		exch(a, lo, j);
		return j;
	} 
	
	public static void sort(Comparable<?>[] a) {
		sort(a, 0, a.length-1);
	}
	
	public static void sort(Comparable<?>[] a, int lo, int hi) {
		if (hi<=lo) return;
		int p = partition(a, lo, hi);
		sort(a, lo, p-1);
		sort(a, p+1, hi);
	}
	
	public static void main(String[] args) {
		Integer[] a = {12,23,24,34,21,23,57,34,67,84,52,78,52,58,83};
		QuickSort.sort(a);
		show(a);
		System.out.println(isSort(a));
	}
}

2.3 性能分析

       对于长度为N的任意数组,快速排序平均需要2NlgN次比较,渐近时间复杂度为O(NlgN),空间复杂度lgN,属于原地排序。 之前的隐患也看到了,快排在极端情况下有可能退化成冒泡排序,也就是说快排最多需要N*N/2次比较,所以快排前随机打乱数组是最廉价的预防手段

小结

       归并&快排都是效果不错的排序方法,其中快排是各大API中常用sort方法,确实快排有很多的优势,不仅实现简单思路也清晰。理解这两个算法思路是关键,更重要的理解由数学命题作为切入点而设计算法的思想。这两个算法的思想抽象出来就是:让子数组满足有序,然后分解到更小的子数组,递归下去直至子数组的长度为1,即每个元素都满足了有序。 归并排序选择的命题切入点是命题1——任何一个元素不小于其存在的左边元素。(效果上和命题2——有序数组其任何一个元素不大于其右边的元素,一致),快速排序选择的命题切入点是命题3——有序数组其任何一个元素不小于其左边元素且不大于右边元素。相比较之前的冒泡&选择&插入排序三兄弟,归并和冒泡&选择排序选择了一样的切入点(命题1和命题2),快速排序和插入排序一样选择了命题3作为切入点。归并和快排在分治思想的加持下性能有了显著提升。所以思想是一个算法的灵魂!也决定了算法的高度!

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值