算法(4)归并排序 java

在介绍归并排序之前,先简单的说一下O(NlogN)和O(N2)之间的比较,通过下面的图片可以明显的看出来,前者的优势是很明显的,并且随着N的增大,优势会越来越明显,优化之后的代码可能意味着笨的算法一辈子都算不出来结果,而优化之后的算法,一瞬间就算出来了(细思极恐,这不就是现实生活吗...)


归并排序:归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并

为什么说归并排序就是一个O(NlogN)的算法呢?请看下图


一个有N个元素的数组,假设N=8,那么采用分治法,通过上图可以看出来,进行3次二分法就能将数组拆分为单个元素,然后逐层进行归并操作,3就是通过log28=3得出来的 通过这种拆分方法,把问题降低到通过O(N)的时间复杂度就能得到一个排好序的子序列,因此归并排序是一个O(NlogN)的算法,其在性能上要比O(N2)要好,并且是一个很稳定的算法.

当然任何事情都是有两面性,归并算法的缺陷是需要借助于比较大的内存,但通过空间换取时间是比较值得的

可以使用递归思想或者循环来实现归并排序,本次代码中使用递归思想来实现归并排序

	// [l,r]
	public static void sort(int[] arr, int l, int r) {
		if (l >= r)
			return;
		int mid = (r + l) / 2;
		sort(arr, l, mid);
		sort(arr, mid + 1, r);
		merge(arr, l, r, mid);

	}

	// [l,mid] [mid+1,r]是已经排好序的 直接进行merge操作
	private static void merge(int[] arr, int l, int r, int mid) {
		int[] aux = new int[r - l + 1];// 大小从l 到r
		
		for (int i = l; i <= r; i++) {// 将arr中l到r的元素复制到aux中
			aux[i - l] = arr[i];
		}
		
		int i = l;// 在arr中的 左边有序的下标
		int j = mid + 1;//在arr中的 右边有序的下标 
		
		// 进行归并操作
		for (int k = l; k <= r;) {
			
			if (i > mid) {// 说明左边已经比完了 直接将右边j下标对应的数字给arr
				arr[k++] = aux[j++ - l];
			} else if (j > r) {// 说明右边已经比完了,直接将左边i下标对应的数字给arr
				arr[k++] = aux[i++ - l];
			} else {// 否则比较值 直接用三目运算
				arr[k++] = aux[i - l] < aux[j - l] ? aux[i++ - l] : aux[j++ - l];
			}
			
		}

	}

然而,上面的代码在某些情况下,其性能是不如插入排序的,因此归并排序是有一些优化的点的.来分析代码

public static void sort(int[] arr, int l, int r) {
		if (l >= r)
			return;
		int mid = (r + l) / 2;
		sort(arr, l, mid);
		sort(arr, mid + 1, r);
		merge(arr, l, r, mid);

	}
①在上面的代码中,将[l,mid]和[mid+1,r]两个区间进行归并排序,紧接着无论上面的两个区间是否已经达成了有序不做判断,直接进行merge操作,这样在近乎有序的情况下,其性能会有影响.
②如果数组近乎有序,那么在元素个数小于一个常数的时候,利用插入排序性能反而更好,至于用哪个常数,在不同的应用场景,可以进行试验.

经过两次优化之后的代码:

	// [l,r]
	public static void sort(int[] arr, int l, int r) {
		if (r - l + 1 <= 7) {// 优化②当需要进行归并排序的个数小于7的时候直接使用插入排序算法
			// 插入排序算法
			for (int i = l; i <= r; i++) {
				int t = arr[i];
				int j = i;
				for (; j > 0 && arr[j - 1] > t; j--) {
					arr[j] = arr[j - 1];
				}
				if (j != i) {
					arr[j] = t;
				}
			}
			return;
		}
		
		int mid = (r + l) / 2;
		sort(arr, l, mid);
		sort(arr, mid + 1, r);
		if (arr[mid] > arr[mid + 1]) {// 优化① 通过上面的步骤可以保证左区间和右区间都是有序的序列了,因此, 						//只有当左区间的最后一个元素大于右区间第一个元素时,才进行merge操作
			merge(arr, l, r, mid);
		}

	}

	// [l,mid] [mid+1,r]是已经排好序的 直接进行merge操作
	private static void merge(int[] arr, int l, int r, int mid) {
		int[] aux = new int[r - l + 1];// 大小从l 到r

		for (int i = l; i <= r; i++) {// 将arr中l到r的元素复制到aux中
			aux[i - l] = arr[i];
		}

		int i = l;// 在arr中的 左边有序的下标
		int j = mid + 1;// 在arr中的 右边有序的下标

		// 进行归并操作
		for (int k = l; k <= r;) {

			if (i > mid) {// 说明左边已经比完了 直接将右边j下标对应的数字给arr
				arr[k++] = aux[j++ - l];
			} else if (j > r) {// 说明右边已经比完了,直接将左边i下标对应的数字给arr
				arr[k++] = aux[i++ - l];
			} else {// 否则比较值 直接用三目运算
				arr[k++] = aux[i - l] < aux[j - l] ? aux[i++ - l] : aux[j++ - l];
			}

		}

	}
通过上面的优化,这个归并排序就已经算是达到了比较好的性能了.


-----------------------------------------------------------------分水岭-----------------------------------

再附上一个不实用递归,使用迭代来完成的归并排序,这个比较好理解

/**
	 * 通过自底向上的迭代来完成归并排序
	 * @param arr
	 */
	public static void sortBU(int[] arr) {
		for(int size = 1 ; size <= arr.length - 1 ; size += size) {//控制每次进行merge操作的元素的个数 2倍					                                  关系 1->2->4 
			for(int i = 0 ; i + size <= arr.length - 1 ; i +=(size * 2)) {//控制每次merge操作的两个区间[i,i+size - 1],[i + size , i+size * 2 - 1]
				merge(arr, i, Math.min(i + size * 2 - 1,arr.length - 1), i + size - 1);//为了避免i+size * 2 - 1 出现越界 使用Math.min 来取出 当前的right 和数组边界小的那一个
			}
		}
	}








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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值