概述
1945年,约翰·冯·诺依曼(John von Neumann)发明了归并排序,这是典型的分治算法的应用。在计算机科学中,归并排序是一种高效、通用、基于比较的排序算法。此外,归并排序还是稳定的,因为相同元素的相对次序在排序后不会发生变化。最开始,归并排序采用的是自顶向下的模式,后来,到了1948年,冯大神和赫尔曼·海因·戈德斯坦(Herman Heine Goldstine)两人共同撰写了一篇报告,描述了归并排序的另外一种实现方式,那就是自底向上。
接下来,本文将带你逐一讨论归并排序的这两种实现方式。
膜拜一下两位大佬(〃‘▽’〃)。
归并排序的思想很简单:
- 将待排序的线性表不断地切分成若干个子表,直到每个子表只包含一个元素,这时,可以认为只包含一个元素的子表是有序表。
- 将子表两两合并,每合并一次,就会产生一个新的且更长的有序表,重复这一步骤,直到最后只剩下一个子表,这个子表就是排好序的线性表。
这里面就用到了非常重要的分治思想,把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,最后将子问题的解合并,就得到了原问题的解。
自顶向下
以数组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;
}
注:文中部分图片来自网络