归并排序

概述

1945年,约翰·冯·诺依曼(John von Neumann)发明了归并排序,这是典型的分治算法的应用。在计算机科学中,归并排序是一种高效、通用、基于比较的排序算法。此外,归并排序还是稳定的,因为相同元素的相对次序在排序后不会发生变化。最开始,归并排序采用的是自顶向下的模式,后来,到了1948年,冯大神和赫尔曼·海因·戈德斯坦(Herman Heine Goldstine)两人共同撰写了一篇报告,描述了归并排序的另外一种实现方式,那就是自底向上。

接下来,本文将带你逐一讨论归并排序的这两种实现方式。

在这里插入图片描述
在这里插入图片描述

膜拜一下两位大佬(〃‘▽’〃)。

归并排序的思想很简单:

  1. 将待排序的线性表不断地切分成若干个子表,直到每个子表只包含一个元素,这时,可以认为只包含一个元素的子表是有序表。
  2. 将子表两两合并,每合并一次,就会产生一个新的且更长的有序表,重复这一步骤,直到最后只剩下一个子表,这个子表就是排好序的线性表。

在这里插入图片描述

这里面就用到了非常重要的分治思想,把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,最后将子问题的解合并,就得到了原问题的解。

自顶向下

以数组A[8] = {6, 3, 2, 7, 1, 5, 4, 8}为例,归并排序自顶向下的排序过程如下图所示:
在这里插入图片描述

在计算机程序中,这一过程可以用递归来实现。以下是C++代码实现:

/*自顶向下,递归
  Array:待排序的数组首地址
  low:待排序的范围的下界
  high:待排序的范围的上界的后一个位置
  比如你要对数组Array[0]~Array[5]进行排序,那么low=0,high=6*/
void mergeSort_Recursive(int* Array, int low, int high) {
	if (low + 1 < high) { //当子数组的长度大于1时,不断对数组进行分解
		int mid = low + (high - low + 1) / 2; //将数组分解成Array[low, mid)和A[mid, high),圆括号表示开区间,即数组中不包含此元素
		mergeSort_Recursive(Array, low, mid);
		mergeSort_Recursive(Array, mid, high);
		merge(Array, low, mid, high); /*合并Array[low, mid)和A[mid, high),该函数在文末的完整代码中给出*/
	}
}

自底向上

自底向上的思想就是,进行分解,直接从线性表中的单个元素开始,进行两两合并,然后再以每两个元素为单位,进行两两合并……直到最后只剩下一个线性表。还是以刚才那个数组为例,自底向上的归并排序图示如下:
在这里插入图片描述

很明显,这种方式跳过了分解的步骤,操作步骤少了很多,而且在用代码实现时,采用的是迭代而不是递归的方式,空间复杂度也少了很多。

下面是C++代码实现:

/*自底向上,迭代
  Array:待排序的数组首地址
  low:待排序的范围的下界
  high:待排序的范围的上界的后一个位置
  比如你要对数组Array[0]~Array[5]进行排序,那么low=0,high=6*/
void mergeSort(int* Array, int low, int high)
{
	int step = 1;
	while (step < high - low) {
		for (int i = low; i < high; i += step << 1) {
			int lo = i, hi = (i + (step << 1)) <= high ? (i + (step << 1)) : high; //定义二路归并的上界与下界 
			int mid = i + step <= high ? (i + step) : high;
			merge(Array, lo, mid, hi);
		}

		//将i和i+step这两个有序序列进行合并
		//序列长度为step
		//当i以后的长度小于或者等于step时,退出
		step <<= 1;//在按某一步长归并序列之后,步长加倍
	}
}

综合评价

在最坏情况下,以上两种方式的时间复杂度均为 O ( n l o g n ) O(nlogn) O(nlogn)。采用自顶向下的归并排序要用到递归,这种方法的好处就是代码容易理解,能比较直观地描述算法思想。但是,也正是由于递归,会使得在排序过程中占用大量计算机内部的栈空间,如果线性表长度过长,那么会可能会造成栈溢出,从而使程序崩溃,而自底向上的迭代就不会出现这种问题。所以,综合来看,在实际应用中,建议大家采用自底上的归并排序——也就是迭代。

递归对性能的消耗有时候甚至能达到指数级别,事实上,任何能用递归解决的问题,也都能用迭代解决。所以,不仅是归并排序,当你遇到其他问题时,如果能用迭代解决,那么不妨想想能不能用迭代来替换。

以下是完整代码:

#include <iostream>

using namespace std;

/*合并Array[low, mid)和Array[mid, high)
  合并前应保证Array[low, mid)和Array[mid, high)中的元素都是有序的*/
void merge(int* Array, int low, int mid, int high){
	int* A = Array + low; //合并后的向量A[0, high - low) = _elem[low, high)
	int lb = mid - low;
	int* B = new int[lb]; //前子向量B[0, lb) = _elem[low, mid)
	for (int i = 0; i < lb; B[i] = A[i++]); //复制前子向量B
	int lc = high - mid;
	int* C = Array + mid; //后子向量C[0, lc) = _elem[mid, high)

	/*i, j, k分别指向A, B, C中的元素*/
	for (int i = 0, j = 0, k = 0; (j < lb) || (k < lc);) { //B[j]和C[k]中小者转至A的末尾
		if (j < lb && k < lc) //如果j和k都没有越界,那么就选择B[j]和C[k]中的较小者放入A[i]
			A[i++] = B[j] < C[k] ? B[j++] : C[k++];
		if (j < lb && lc <= k) //如果j没有越界而k越界了,那么就将B[j]放入A[i]
			A[i++] = B[j++];
		if (lb <= j && k < lc) //如果k没有越界而j越界了,那么就将C[k]放入A[i]
			A[i++] = C[k++];
	}

	delete[] B; //释放临时空间B
}

/*自底向上,迭代
  Array:待排序的数组首地址
  low:待排序的范围的下界
  high:待排序的范围的上界的后一个位置
  比如你要对数组Array[0]~Array[5]进行排序,那么low=0,high=6*/
void mergeSort(int* Array, int low, int high)
{
	int step = 1;
	while (step < high) {
		for (int i = low; i < high; i += step << 1) {
			int lo = i, hi = (i + (step << 1)) <= high ? (i + (step << 1)) : high; //定义二路归并的上界与下界 
			int mid = i + step <= high ? (i + step) : high;
			merge(Array, lo, mid, hi);
		}

		//将i和i+step这两个有序序列进行合并
		//序列长度为step
		//当i以后的长度小于或者等于step时,退出
		step <<= 1;//在按某一步长归并序列之后,步长加倍
	}
}

/*自顶向下,递归
  Array:待排序的数组首地址
  low:待排序的范围的下界
  high:待排序的范围的上界的后一个位置
  比如你要对数组Array[0]~Array[5]进行排序,那么low=0,high=6*/
void mergeSort_Recursive(int* Array, int low, int high) {
	if (low + 1 < high) { //当子数组的长度大于1时,不断对数组进行分解
		int mid = low + (high - low + 1) / 2; //将数组分解成Array[low, mid)和A[mid, high),圆括号表示开区间,即数组中不包含此元素
		mergeSort_Recursive(Array, low, mid);
		mergeSort_Recursive(Array, mid, high);
		merge(Array, low, mid, high); //合并Array[low, mid)和A[mid, high)
	}
}

int main() {
	int A[8] = { 6, 3, 2, 7, 1, 5, 8, 4 };

	mergeSort(A, 0, 8);
	//mergeSort_Recursive(A, 0, 8);
	for (int i = 0; i < 8; i++) {
		cout << A[i] << " ";
	}
	system("pause");
	return 0;
}

注:文中部分图片来自网络

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值