白话讲排序系列(五) 归并排序

本文以通俗易懂的方式讲解归并排序的基本原理与实现步骤,并通过示例代码展示算法细节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

如何用最通俗易懂的话把归并排序说明白,这是个问题,本文只是做尝试;如果讲得不好,还请大家批评;如果讲得好,欢迎评论,转载。

好的,废话少说,开讲!

归并,之所以叫归并,就是算法的核心在于归并上;这就好比冒泡排序算法的核心,在于冒泡;选择排序的算法,核心在于从待排序的队列中选择出合适的数字一样。

(1):归并,既然是并;那至少得是两个数列吧?是不是?要不然一个巴掌拍不响啊!

既然是两个数列,那么,最最简单的情况,每个数列都只有一个数字,直接比较一下大小就能够合并,这再简单不过了,是不是?

好的,我们已经踏出了归并算法的第一步;比如说两个数,1和2 ;一次比较得出结果,两个数字合在一起,组成了有序数列{1,2}。

然后呢,又来了一个数字,比如说这次来的是3,那么,其会跟原先的数列进行一下比较,发现可以放在数列的最后,三个数字商量一下,组成了{1,2,3}这样一个有序数列。

从最通俗的话来说,寥寥几句,其实就谈到了归并算法的精髓,那就是合并。

(2)如果合并的两个序列多了呢,怎么来从代码的角度阐述这个问题呢,接下来讨论。

比如说,现在来了两个序列,一个是a = {1,3,5};一个是b = {2,4,6}。

很明显,合并后会出现一个大小为6的空间,我们先弄出来这么一个数组,比方说array。

首先,a中取出最小的元素,a[1] = 1,把这个数字放入新建的队列里;然后我们去看b,取出了b[1] = 2;取出来的这个数,要和已经在数组中的元素比较下,然后确定自己的位置,这样,array = {1,2}。

这里需要注意:我们是两个数列同时遍历的,所以要准备两个索引,对于a来说,就是i;对于b来说,就是j;这两个索引定义了a和b分别遍历到了什么位置。

大家可能会想到了,那么array是不是也需要一个位置来确定,其到底填充到了什么层次呢?是的,那需要不需要重新定义第三个变量?

这是不需要的,稍微想一下就能看出来,其实i和j与array的索引位置有着很密切的关系,所以array中填充到的位置,就可以用这两个值的组合来表示了:应该是i+j。

这时候,有心的同学可能会注意到了,为什么a和b都是有序的,真正排序的时候,不会出现这种巧合吧?

请认真想一下这个问题,后文会再度详细说明整体的流程。

就这样,a和b几乎是齐头并进,但是,他们两个总会有一个先全部为空的;即便是两个数列个数相同,但也会因为插入的先后次序,导致一个序列先插入到array中。

比如说,这时候1,2,3,4,5都插入到array中了,那剩下b中的数据,肯定要比前面五个元素都要大,因为b是有序的!!!

所以,哪一个序列多出来数字了,直接一股脑扔进array中,就可以了。

核心的归并,就是这么做的:双队列齐齐遍历,比较插入新数列中。

(3)回答一下上面我提出的问题,为什么拿出来两个有序的队列呢?

假如说,我们现在拥有一个序列 newArray = {1,3,5,4,7,6,9,8};大家需要注意了,我们排序的是一个序列,而不是前面举例子用到了单个元素和队列中的部分元素。

那么,怎么才能把上面的思想用到这里呢?

答案是:先拆分。

当然,这里并不是真的拆分,而是从单个元素的角度去考虑,这里面共有八个元素,我们从单个元素的角度看,然后两两合并,经过一轮合并,就变成了四组,每组两个元素。

第二轮呢,再把四组元素两两合并,结果,就成为了两组元素。

第三轮,把上文的两组元祖再予以合并,最终,整个队列就变成了完全有序的了。

是不是很好理解?

想要真正理解这个算法,必须要记住,这是一种我们从单个元素开始考虑的算法,单个元素合并,逐渐合并成大的元素,最终,达到整个元素有序的目的。


网上找到了一张图,有助于理解,其本质,就是两步:第一,拆分;第二,归并。

多说无益,直接上代码:这是第一版本的代码

public class MergeSort {
	public static void main(String[] args) {
		int[] array = new int[] { 2, 1, 4, 3, 6, 10, 8, 7 };

		// 注意,这里调用的时候,我们传入的是整个数组,从第一个元素到最后一个元素排序
		sort(array, 0, array.length - 1);
		for (int ele : array) {
			System.out.print(ele + " ");
		}
	}

	/**
	 * 
	 * @description 归并排序的调用逻辑,思想是递归
	 * @author yuzhao.yang
	 * @param a
	 *            需要排序的数组
	 * @return
	 * @time 2018年3月8日 下午12:09:01
	 */
	public static void sort(int[] a, int low, int high) {
		// 按照拆分的逻辑,现在先把整个数组拆成两部分,mid作为数组的中间值,作为划分标准
		int mid = (low + high) / 2;
		// 这个限定条件是什么意思呢?因为low肯定不会大于high的,最多是等于,等于意味着什么呢,那就是单个元素
		// 到这个时候,就是拆分到了最底层,接下来,就开始归并了
		// 而如果没拆分到单个元素,那就继续拆分。
		if (low < high) {
			// 如果还可以继续拆分的话,递归调用本逻辑,先对左边的数据进行排序
			sort(a, low, mid);
			// 递归调用右侧的排序逻辑
			sort(a, mid + 1, high);
			// 左右归并
			mergeSort(low, high, mid, a);
		}
	}

	/**
	 * 
	 * @description 核心算法,是把两个有序数列进行归并
	 * @param begin
	 *            合并的第一个序列开始索引
	 * @param end
	 *            合并的第二个序列结束索引;此处,两个索引必定是连接在一起的
	 */

	public static void mergeSort(int begin, int end, int mid, int[] array) {
		// 这里,新建了一个数组,必须说,此处是可以优化的
		int[] temp = new int[end - begin + 1];
		// 然后把两个序列融合在一起;采用遍历的方式
		int index = 0;
		int i = begin;
		int j = mid + 1;
		// 如果两个数列都没有遍历到末尾,谁小谁就先进入temp中
		// 注意,同时temp的索引也要移动
		while (i <= mid && j <= end) {
			if (array[i] < array[j]) {
				temp[index++] = array[i++];
			} else {
				temp[index++] = array[j++];
			}
		}
		while (i <= mid) {
			temp[index++] = array[i++];
		}
		while (j <= end) {
			temp[index++] = array[j++];
		}

		// 这里面,temp已经成为有个有序的数组
		// 而真正数组中合并的两个部分,就可以用这个有序数组来替代
		for (int ele : temp) {
			array[begin++] = ele;
		}
	}
}

这里,第一版本的代码有一些需要优化的地方,至少,对于每次迭代的过程中都需要建立一个新的数组,是完全没有必要的,我们可以最开始就新建一个与待排序数组大小相同的数组,然后每次排完序的队列,都直接将这个新数组中相应的部分予以覆盖。

public class MergeSort2 {

	public static void main(String[] args) {
		int[] array = new int[] { 2, 1, 4, 3, 6, 10, 8, 7, 5, 11, 14, 13, 20,
				19 };
		// 这里,考虑把每一次都要用的数组,用一个早就定义好的数组来定义
		// 这样,可以很大程度减少递归调用过程中的内存占用
		int[] newArray = new int[array.length];

		sort(newArray, array, 0, array.length - 1);
		for (int ele : array) {
			System.out.print(ele + " ");
		}
	}

	/**
	 * 
	 * @description 归并排序的调用逻辑,思想是递归
	 * @author yuzhao.yang
	 * @param a
	 *            需要排序的数组
	 * @return
	 * @time 2018年3月8日 下午12:09:01
	 */
	public static void sort(int[] newArray, int[] array, int low, int high) {
		// 按照拆分的逻辑,现在先把整个数组拆成两部分,mid作为数组的中间值,作为划分标准
		int mid = (low + high) / 2;
		// 这个限定条件是什么意思呢?因为low肯定不会大于high的,最多是等于,等于意味着什么呢,那就是单个元素
		// 到这个时候,就是拆分到了最底层,接下来,就开始归并了
		// 而如果没拆分到单个元素,那就继续拆分。
		if (low < high) {
			// 如果还可以继续拆分的话,递归调用本逻辑,先对左边的数据进行排序
			sort(newArray, array, low, mid);
			// 递归调用右侧的排序逻辑
			sort(newArray, array, mid + 1, high);
			// 左右归并
			mergeSort(low, high, mid, newArray, array);
		}
	}

	/**
	 * 
	 * @description 核心算法,是把两个有序数列进行归并
	 * @param begin
	 *            合并的第一个序列开始索引
	 * @param end
	 *            合并的第二个序列结束索引;此处,两个索引必定是连接在一起的
	 * @param mid
	 *            把待排序数组划分为两半的中间值
	 * @param tempArray
	 *            中间数组,用于减少递归调用内存
	 * @param array
	 *            待排序数组
	 */

	public static void mergeSort(int begin, int end, int mid, int[] tempArray,
			int[] array) {
		// 这里,不需要新建数组了,直接用最先前创建的新数组
		// 然后把两个序列融合在一起;采用遍历的方式
		int index = begin;
		int i = begin;
		int j = mid + 1;
		// 如果两个数列都没有遍历到末尾,谁小谁就先进入temp中
		// 注意,同时temp的索引也要移动
		while (i <= mid && j <= end) {
			if (array[i] < array[j]) {
				tempArray[index++] = array[i++];
			} else {
				tempArray[index++] = array[j++];
			}
		}
		while (i <= mid) {
			tempArray[index++] = array[i++];
		}
		while (j <= end) {
			tempArray[index++] = array[j++];
		}
		for (int k = begin; k <= end; k++) {
			array[k] = tempArray[k];
		}
	}
}

跟第一代码相比,第二版代码使用一个贯穿全局的临时数组,其他基本相似。

复杂度分析:

  1. 时间复杂度为:O(nlogn);
  2. 空间复杂度为:O(n);这个容易看出来,我们第二版本的代码,就是利用了一个大小为n的数组作为数据暂存的空间。

稳定性:

与快速排序和堆排序相比,归并排序的最大特点是,它是一种稳定的排序算法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值