排序算法(归并)

归并排序

本文中的大多数代码以及描述,都来自于《算法(第4版)》这本书,加上自己的一些分析与理解。

  • 基本描述
    归并排序是将一个数组拆分成两半分别排序,然后把两个已经排好序的结果归并起来。它有两种基本的实现,一种是自顶向下,这种需要递归,从上到下,不断拆分,把拆分的结果进行归并。另外一种是自底向上,先从最底层按照每两个元素进行归并,再分别按照4,8,16…个一层层向上归并,直到把整个数组归并。
  • 基本的归并算法
    归并算法就是把两个有序的数组进行归并(merge)。就是从两个数组的左边开始取值,哪个小就取哪一个,每取出一个值,放到新数组的最左侧。这个代码如下:
	/**
	 * 原地归并算法,从小到大的顺序
	 * 这个是把第一个数组a[lo]~a[mid]
	 * 与第二个数组a[mid+1]~a[hi]
	 * 两个有序的数组,归并到一块儿。
	 * @param a	需要归并的数组
	 * @param lo 第一个数组的起始索引
	 * @param mid	第一个数组的结束索引
	 * @param hi	第二个数组的结束索引
	 * @param temp	缓存数组,大小需要和a数组一样
	 */
	public void merge(Comparable<?>[] a,int lo,int mid,int hi,Comparable<?>[] temp) {
		int i=lo;
		int j=mid+1;
		for (int k = lo; k <= hi; k++) {
			temp[k]=a[k];
		}
		for (int k = lo; k <= hi; k++) {
			if 		(i>mid)						a[k]=temp[j++];
			else if (j>hi)						a[k]=temp[i++];
			else if (less(temp[j], temp[i]))    a[k]=temp[j++];
			else 								a[k]=temp[i++];       
		}
	}

	public boolean less(Comparable a,Comparable b) {
		return a.compareTo(b)<0;
	}
  • 归并算法简析
    • 先把a[lo]~a[hi]这部分的数据复制到缓存数组中
    • i是用来标记第一个数组最左侧还没有被访问的元素,j同理是第二个数组的。
    • for循环里面,第3行判断是如果第二个数组值小于第一个值,就取第二个的。第四行是取第一个数组的值。第一行其实是如果第一个数组的值已经被取完了,那么就直接取第二个数组的,第二行类似。
  • 自顶向下归并排序
    这种就是把一个大的数组不断拆分,拆成最小粒度的部分,也就是两个数据,然后进行归并,然后递归执行。这种是算法设计中分治思想的典型应用,把一个大问题分割成小问题分别解决。代码如下:
	@Override
	public void sort(Comparable[] a) {
		Comparable<?>[] temp=new Comparable<?>[a.length];
		sort(a, 0, a.length-1,temp);
	}
	
	/**
	 * 归并排序,
	 * 把数据拆分成两部分,然后分别排序,然后把排好序的两部分进行归并。
	 * @param a	需要排序的数据
	 * @param lo	需要排序的部分的起始索引
	 * @param hi	需要排序的部分的结束索引
	 * @param temp	缓存数组
	 */
	public void sort(Comparable<?>[] a,int lo,int hi,Comparable<?>[] temp) {
		if (hi<=lo) return;
		int mid=lo+(hi-lo)/2;
		sort(a, lo, mid,temp);
		sort(a,mid+1,hi,temp);
		merge(a, lo, mid, hi,temp);
	}
  • 自顶向下归并排序算法简析
    • 第一句判断是如果元素如果剩一个了,就不能再拆分了。
    • 取出中间的索引,然后拆分,分别再排序,最后再执行归并。
  • 执行过程
    以8个数据为例,在代码里面稍微加一些辅助代码,可以打印出如下执行过程:
sort(a,0,7)
 sort(a,0,3)
  sort(a,0,1)
   sort(a,0,0)
   sort(a,1,1)
   merge(a,0,0,1)
  sort(a,2,3)
   sort(a,2,2)
   sort(a,3,3)
   merge(a,2,2,3)
  merge(a,0,1,3)
 sort(a,4,7)
  sort(a,4,5)
   sort(a,4,4)
   sort(a,5,5)
   merge(a,4,4,5)
  sort(a,6,7)
   sort(a,6,6)
   sort(a,7,7)
   merge(a,6,6,7)
  merge(a,4,5,7)
 merge(a,0,3,7)

可以看到总共归并了7次。

  • 自底向上的归并排序
    上面的由大数组进行拆分,然后递归的进行归并。但是我们看到执行过程,也是最后很小很小的数据一次次归并。所以这边也就有了另外一种思维,干脆直接先归并小数组,然后一步步向上不断归并,最终完成归并,而且也没有递归。核心代码如下:
	public void sort(Comparable[] a) {
		int N=a.length;
		Comparable<?>[] temp=new Comparable<?>[N];
		for (int sz = 1; sz < N; sz=sz+sz) {//sz是子数组的大小,1,2,4,8...
			for (int lo = 0; lo+sz < N; lo+=sz+sz) {//lo是子数组的索引
				merge(a, lo, lo+sz-1, Math.min(lo+sz+sz-1, N-1), temp);
			}
		}
	}
  • 自底向上的归并排序算法简析
    代码其实并没有多少,但是一些参数的赋值,需要好好琢磨琢磨才行,而且最好是画图结合分析才比较容易。
    • sz代表是子数数组的大小,因为我们是从最小的开始归并,所以起始值为1,然后是2,4,8,16等等。
    • sz需要小于N。当N=2^n的时候,max(sz)=N/2;否则的时候,max(sz)会大于N/2,同时会小于N。比如N=9时,max(sz)=8,最后一次需要将前8个元素和最后一个元素归并排序。
    • sz准确一些的描述应该是归并时候的左侧数组的大小,右侧的大小实际上会小于等于sz。
    • lo是子数组的索引,从0开始,每次增加的值是两个sz,而lo+sz代表的是左侧数据最大的索引值,一定要小于N。
    • 然后进行归并,lo是其实,lo+sz-1是左侧数组的最大索引值。所以再加上数组大小就应该是右侧的最大索引值,即lo+sz+sz-1。但是N!=2^n的时候,这个值会超过数组大小,因此需要和N-1中取最小值。
  • 执行过程
    也以8个数据为例,打印如下过程:
sz=1
merge(a,0,0,1)
merge(a,2,2,3)
merge(a,4,4,5)
merge(a,6,6,7)
sz=2
merge(a,0,1,3)
merge(a,4,5,7)
sz=4
merge(a,0,3,7)

也是归并了7次,和上面一样。看一下9个的情况:

sz=1
merge(a,0,0,1)
merge(a,2,2,3)
merge(a,4,4,5)
merge(a,6,6,7)
sz=2
merge(a,0,1,3)
merge(a,4,5,7)
sz=4
merge(a,0,3,7)
sz=8
merge(a,0,7,8)

第九个数据会单出来,最后一次的时候才会归并它。

  • 简单图形解析
    假如输入的值是:64134285
    自底向上的示意图如下:
    在这里插入图片描述
    每一个汇合的地方,就是一次归并,代码也是按照从下往上,从左往右的顺序执行。

自顶向下的示意图如下:

在这里插入图片描述
这张图并不能很好的展示出来归并的过程,仅仅只是展示了拆分的过程,从上到下,一直拆分,拆分到最后一个元素的时候,就进行归并。归并的执行过程和上一张图稍微有一些不一样,它这边会优先把左侧的全部归并好,然后再去拆分右侧的,毕竟是按照递归执行的。

  • 简单优化
    • 对于自顶向下的归并来说,如果在归并的时候判断一下如果a[mid]<=a[mid+1],说明本身已经是有序的了,就可以直接跳过了。
    • 自顶向下的归并排序所需要的时间与数据大小的关系,是与NlgN成正比的,因此它要比一些基本排序算法快很多(冒泡,选择和插入),所以它可以处理百万级甚至更大规模的数组。但是当数据量较小的时候,它的性能并不如基本排序算法,所以可以简单判断一下在递归归并的时候,如果大小小于某个值(比如16)的时候,使用插入排序。
    • 自底向上的归并排序比较合适用链表组织的数据。这种方法值需要重新组织链表链接就能将链表原地排序(不需要创建任何新的链表节点)
  • 简单结论
    用自顶向下或者自底向上的方式实现任何分治类的算法都很自然。归并排序告诉我们,当能够用其中一种方法解决一个问题是,你都应该试试另外一种。你是希望化整为零(然后递归地解决他们)的方式解决问题,还是希望循序渐进的解决问题呢?

猜你喜欢

转载自blog.csdn.net/ywg_1994/article/details/93380809