高级排序算法-归并排序

辅助工具

数组工具类代码 : https://blog.****.net/love905661433/article/details/83106078
用于对比的基础排序代码 : https://blog.****.net/love905661433/article/details/83117977

归并排序

  • O(nlogn)级别算法
  • 需要额外的辅助空间
  • 稳定的排序算法

对于排序算法稳定性的定义, 这里直接引用百度词条:

假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。

排序步骤 :

  1. 首先将一个数组递归进行分解, 知道分解为每一部分都是一个元素, 这时每一部分都是有序的了

  2. 然后两两进行归并, 递归进行, 直到再归并成一个数组, 排序就完成了, 如下图所示:
    高级排序算法-归并排序

代码实现

package sort.merge;

import sort.Sort ;

/**
 * 归并排序
 * @author 七夜雪
 *
 */
public class MergeSort implements Sort {
	
	@Override
	public void sort(int [] arr) {
		mergeSort(arr, 0, arr.length - 1);
	}
	
	/**
	 * 对数组arr的[l, r]的区间进行排序
	 * @param arr
	 * @param l
	 * @param r
	 */
	private void mergeSort(int[] arr, int l, int r){
		// 递归终止条件
		if (l >= r) {
			return;
		}
		
		int mid = l + (r - l) / 2;
		// 对[l, mid]部分进行归并排序
		mergeSort(arr, l, mid);
		// 对[mid + 1, r]进行归并排序
		mergeSort(arr, mid + 1, r);
		// 对[l, mid], [mid + 1, r]这两部分进行归并
		merge(arr, l, mid, r);
	}
	
	
	/**
	 * 对数组[l, mid], [mid + 1, r]这两部分进行归并
	 * @param arr
	 * @param l
	 * @param mid
	 * @param r
	 */
	private void merge(int[] arr, int l, int mid, int r){
		// 用于归并排序的辅助空间, 因为[l, r]是前闭后闭的区间, 所以aux的大小是r - l + 1
		int[] aux = new int[r - l + 1];
		for (int i = 0; i < aux.length; i++) {
			aux[i] = arr[i + l];
		}
		
//		for (int i = l; i <= r; i++) {
//			aux[i - l] = arr[i];
//		}
		
		// 用于记录左半部分数组下标
		int leftIndex = l;
		// 用于记录右半部分数组下标
		int rightIndex = mid + 1;
		// 循环进行归并
		for (int k = l; k <= r; k++) {
			// 表示左侧部分已经完成归并, 直接使用右侧部分进行归并
			if (leftIndex > mid) {
				arr[k] = aux[rightIndex - l];
				rightIndex++;
			// 表示右侧部分已经完成归并, 直接使用左侧部分进行归并
			} else if (rightIndex > r) {
				arr[k] = aux[leftIndex - l];
				leftIndex++;
			// 左侧元素小于右侧元素, 使用左边元素进行归并
			} else if (aux[leftIndex - l] < aux[rightIndex - l]) {
				arr[k] = aux[leftIndex - l];
				leftIndex++;
			} else {
				arr[k] = aux[rightIndex - l];
				rightIndex++;
			}
		}
	}
	
}

归并排序优化

虽然归并排序是nlogn级别的算法, 但是在数组数据量比较小的时候, 插入排序的效率仍然是高于归并排序的, 所以可以在对数组分解到足够小之后, 使用插入排序, 然后再递归进行归并排序, 具体代码如下 :

package sort.merge;

import sort.Sort ;
import utils.ArrayUtil ;

/**
 * 归并排序
 * 优化 : 对于较小的数组使用插入排序性能要优于归并排序, 
 * 所以可以在分解到较小的部分时, 使用插入排序对数组进行排序
 * @author 七夜雪
 *
 */
public class MergeSort2 implements Sort {
	
	@Override
	public void sort(int [] arr) {
		mergeSort(arr, 0, arr.length - 1);
	}
	
	/**
	 * 对数组arr的[l, r]的区间进行排序
	 * @param arr
	 * @param l
	 * @param r
	 */
	private void mergeSort(int[] arr, int l, int r){
		// 递归终止条件, 小于等于16个元素时使用插入排序
		if (r - l < 16) {
            // 对arr数组的[l,r]区间进行排序, 插入排序算法
			ArrayUtil.insertSort(arr, l, r);
			return;
		}
		
		int mid = l + (r - l) / 2;
		// 对[l, mid]部分进行归并排序
		mergeSort(arr, l, mid);
		// 对[mid + 1, r]进行归并排序
		mergeSort(arr, mid + 1, r);
		// 对[l, mid], [mid + 1, r]这两部分进行归并
		merge(arr, l, mid, r);
	}
	
	
	/**
	 * 对数组[l, mid], [mid + 1, r]这两部分进行归并
	 * @param arr
	 * @param l
	 * @param mid
	 * @param r
	 */
	private void merge(int[] arr, int l, int mid, int r){
		// 用于归并排序的辅助空间, 因为[l, r]是前闭后闭的区间, 所以aux的大小是r - l + 1
		int[] aux = new int[r - l + 1];
		for (int i = 0; i < aux.length; i++) {
			aux[i] = arr[i + l];
		}
		
//		for (int i = l; i <= r; i++) {
//			aux[i - l] = arr[i];
//		}
		
		// 用于记录左半部分数组下标
		int leftIndex = l;
		// 用于记录右半部分数组下标
		int rightIndex = mid + 1;
		// 循环进行归并
		for (int k = l; k <= r; k++) {
			// 表示左侧部分已经完成归并, 直接使用右侧部分进行归并
			if (leftIndex > mid) {
				arr[k] = aux[rightIndex - l];
				rightIndex++;
			// 表示右侧部分已经完成归并, 直接使用左侧部分进行归并
			} else if (rightIndex > r) {
				arr[k] = aux[leftIndex - l];
				leftIndex++;
			// 左侧元素小于右侧元素, 使用左边元素进行归并
			} else if (aux[leftIndex - l] < aux[rightIndex - l]) {
				arr[k] = aux[leftIndex - l];
				leftIndex++;
			} else {
				arr[k] = aux[rightIndex - l];
				rightIndex++;
			}
		}
	}
	
	
}

如果一个数组是近乎有序的, 或者说是完全有序的, 上述步骤会有很多无用的merge操作, 所以可以在进行merge前增加一个判断, 效率也会有一定的提高, 代码如下 :

	/**
	 * 对数组arr的[l, r]的区间进行排序
	 * @param arr
	 * @param l
	 * @param r
	 */
	private void mergeSort(int[] arr, int l, int r){
		// 递归终止条件, 小于等于16个元素时使用插入排序
		if (r - l < 16) {
			ArrayUtil.insertSort(arr, l, r);
			return;
		}
		
		int mid = l + (r - l) / 2;
		// 对[l, mid]部分进行归并排序
		mergeSort(arr, l, mid);
		// 对[mid + 1, r]进行归并排序
		mergeSort(arr, mid + 1, r);
        // 判断是否需要进行merge, 因为[l, mid], [mid + 1, r]这两部分都是排好序的数组
		if (arr[mid] > arr[mid + 1]) {
			// 对[l, mid], [mid + 1, r]这两部分进行归并
			merge(arr, l, mid, r);
		}
	}

自底向上的归并排序

上面的归并排序是自顶向下的归并排序, 对于归并排序也可以自底向上进行归并, 排序方法如下 :

  1. 从下往上分解成小块, 然后一层层往上进行归并, 最终归并成一整个数组, 如下图所示 :
    高级排序算法-归并排序

    上图从下往上步骤如下:

    1. 每个元素作为一部分, 进行归并, 归并后结果如第三行
    2. 然后按照每两个元素作为一部分, 进行归并, 归并后结果如第二行
    3. 最后对每四个元素作为一部分, 进行归并, 归并后结果如第一行, 整个数组已经有序了

代码实现 :

	public void sort(int [] arr) {
		int length = arr.length;
		// 数组元素个数小于等于16时, 直接使用插入排序
		if (length <= 16) {
			ArrayUtil.insertSort(arr, 0, length);
			return;
		}
		
		for (int sz = 8; sz < length; sz += sz) {
			// 每次遍历2*sz个长度
			for (int i = 0; i + sz < length; i +=sz + sz) {
				if (sz == 8 ) {
					ArrayUtil.insertSort(arr, i, Math.min(i + sz + sz -1, length - 1));
				} else {
					if (arr[i + sz - 1] > arr[i + sz]) {
						// 对[i, i + sz -1]和[i + sz, 2*sz + i -1]区间进行merge操作
						merge(arr, i, i + sz - 1, Math.min(i + sz + sz -1, length - 1));
					}
				}
			}
		}
		
	}

测试

对插入排序, 进行了优化的各个版本的归并排序进行性能测试, 这里使用了Junit进行测试, 代码如下 :

	/**
	 * 测试插入排序和归并排序性能
	 */
	@Test
	public void testSort(){
		// 生成一个随机数组
		int[] arr = ArrayUtil.generateArray(100000, 0, 100000);
		int[] arr1 = ArrayUtil.copyArray(arr);
		int[] arr2 = ArrayUtil.copyArray(arr);
		int[] arr3 = ArrayUtil.copyArray(arr);
		int[] arr4 = ArrayUtil.copyArray(arr);
		System.out.println("随机数组->插入排序 : " + ArrayUtil.testSort(arr, new InsertSort2()) + "s") ;
		System.out.println("随机数组->归并排序1 : " + ArrayUtil.testSort(arr1, new MergeSort()) + "s") ;
		System.out.println("随机数组->归并排序2 : " + ArrayUtil.testSort(arr2, new MergeSort2()) + "s") ;
		System.out.println("随机数组->归并排序3 : " + ArrayUtil.testSort(arr3, new MergeSort3()) + "s") ;
		System.out.println("随机数组->自底向上的归并排序 : " + ArrayUtil.testSort(arr4, new MergeSortBU()) + "s") ;

		/*
		 * 生成一个近乎有序的数组
		 * 100000 : 数组元素个数
		 * 10 : 在一个完全有序的数组上进行多少次元素交换
		 */
		arr = ArrayUtil.generateNearlyOrderedArray(100000, 10);
		arr1 = ArrayUtil.copyArray(arr);
		arr2 = ArrayUtil.copyArray(arr);
		arr3 = ArrayUtil.copyArray(arr);
		arr4 = ArrayUtil.copyArray(arr);
		System.out.println("近乎有序的数组->插入排序:" + ArrayUtil.testSort(arr, new InsertSort2()) + "s") ;
		System.out.println("近乎有序的数组->归并排序1:" + ArrayUtil.testSort(arr1, new MergeSort()) + "s") ;
		System.out.println("近乎有序的数组->归并排序2:" + ArrayUtil.testSort(arr2, new MergeSort2()) + "s") ;
		System.out.println("近乎有序的数组->归并排序3:" + ArrayUtil.testSort(arr3, new MergeSort3()) + "s") ;
		System.out.println("近乎有序的数组->自底向上的归并排序:" + ArrayUtil.testSort(arr4, new MergeSortBU()) + "s") ;

	}

测试结果 :

随机数组->插入排序 : 2.353s
随机数组->归并排序1 : 0.016s
随机数组->归并排序2 : 0.017s
随机数组->归并排序3 : 0.017s
随机数组->自底向上的归并排序 : 0.015s
近乎有序的数组->插入排序:0.002s
近乎有序的数组->归并排序1:0.007s
近乎有序的数组->归并排序2:0.005s
近乎有序的数组->归并排序3:0.002s
近乎有序的数组->自底向上的归并排序:0.003s

从上面的测试结果来看, 对于随机数组来说, 归并排序的效率是远远高于插入排序的, 对于近乎有序的数组来说, 归并排序的性能也是可观的, 测试结果说明:

  • 归并排序1是最初始的归并排序版本
  • 归并排序2是底层使用了插入排序的版本
  • 归并排序3是在2的基础上加入了判断是否需要进行归并操作的版本