MergeSort 归并排序

问题

用Merge Sort对长度为nn的无序序列ss从小到大(升序)排序。

解法

将长度为nn的序列s=[x0,x1,,xn1]s = [x_0, x_1, \dots, x_{n-1}]分为左右两个部分,left=[x0,,xk]left = [x_0, \dots, x_k]right=[xk+1,,xn1]right = [x_{k+1}, \dots, x_{n-1}],其中0kn10 \le k \le n-1。想象leftleftrightright都是已排序的。如图:

leftleftrightright两个已排序的序列合并即可得到更大的有序序列:

function Merge(s, k, begin, end):
    let sc[begin...end] = s[begin...end]
    let i = begin, j = k+1, k = begin
    while i <= k and j <= end
        if s[i] < s[j]
            sc[k++] = s[i++]
        else
            sc[k++] = s[j++]
    while i <= k
        sc[k++] = s[i++]
    while j <= end
        sc[k++] = s[j++]
    let s[begin...end] = sc[begin...end]

(1) Merge函数第1行:left=[xbegin,,xk],right=[xk+1,,xend]left = [x_{begin}, \dots, x_{k}], right = [x_{k+1}, \dots, x_{end}]

(2) Merge函数第2行:构造长度与ss相同的数组scsc,存储leftleftrightright合并后的结果,该结果最终会复制给ss。该操作需要的空间规模为T(n)T(n)

(3) Merge函数第3-12行:将leftleftrightright按序合并,得到有序序列scsc

(4) Merge函数第13行:将scsc复制到ss上;

上述操作如图:

如何得到已排序的leftleftrightright?递归的对left,rightleft, right也应用上述操作即可。直到序列本身的长度小于等于1时,可以直接看作已排序序列,不需要继续递归:

function MergeSort(s, begin, end):
    if end <= begin+1
        return
    let mid = (begin + end) / 2
    MergeSort(s, begin, mid)
    MergeSort(s, mid+1, end)
    Merge(s, mid, begin, end)

(1) MergeSort函数第1行:在序列s=[x0,,xn1]s = [x_0, \dots, x_{n-1}]上调用MergeSort时begin=0,end=n1begin = 0, end = n-1

(2) MergeSort函数第2-3行:当endbegin+1end \le begin+1时,待排序的序列s=[xbegin,xend]s = [x_{begin}, x_{end}]长度小于等于1,可以看作是已排序的,直接返回;

(3) MergeSort函数第4-7行:将待排序的序列s=[xbegin,xend]s = [x_{begin}, x_{end}]midmid分开,分别递归的调用自己进行排序,得到两个已排序的left=[xbegin,,xmid],right=[xmid+1,,xend]left = [x_{begin}, \dots, x_{mid}], right = [x_{mid+1}, \dots, x_{end}],再将两部分合并即可;

复杂度

k=endbegink = end - begin,Merge函数的输入规模为T(k)T(k),合并kk个元素的时间复杂度为O(k)O(k)

MergeSort函数的初始输入规模为T(n)T(n),因此调用Merge函数的输入规模为T(n)T(n),每次递归后输入规模为上一层的T(n2)T(\frac{n}{2}),可得:

T(n)=2T(n2)+O(n)=2T(2T(n22)+n2)+O(n)=22T(n22)+2O(n)=2T(2T(n23)+n22)+2O(n)=23T(n23)+3O(n)=\begin{matrix} T(n) & = & 2 \cdot T(\frac{n}{2}) + O(n) & & \\ & = & 2 \cdot T(2 \cdot T(\frac{n}{2^2}) + \frac{n}{2}) + O(n) & = & 2^2 \cdot T(\frac{n}{2^2}) + 2 \cdot O(n) \\ & = & 2 \cdot T(2 \cdot T(\frac{n}{2^3}) + \frac{n}{2^2}) + 2 \cdot O(n) & = & 2^3 \cdot T(\frac{n}{2^3}) + 3 \cdot O(n) \\ & = & \cdots & & \end{matrix}

假设递归层数为LL,可得:

T(n2L)=1T(\frac{n}{2^L}) = 1

即:

L=T(log2n)=O(log2n)L = T(log_2 n) = O(log_2 n)

LL代入原始递推公式,可得:

T(n)=2LT(n2L)+LO(n)=O(2log2n)+O(log2n)O(n)=O(n)+O(log2n)O(n)=O(nlog2n)\begin{matrix} T(n) & = & 2^L \cdot T(\frac{n}{2^L}) + L \cdot O(n) \\ & = & O(2^{log_2 n}) + O(log_2 n) \cdot O(n) \\ & = & O(n) + O(log_2 n) \cdot O(n) \\ & = & O(n \cdot log_2 n) \end{matrix}

该算法的时间复杂度为O(nlog2n)O(n \cdot log_2 n)。因为每次Merge函数都会申请规模为T(n)T(n)的内存,其空间复杂度为O(n)O(n)

归并排序适用于数据量超过内存的应用场景。试想硬盘上存储着100GB的数字需要排序,而可使用的内存只有1GB,显然无法将所有数字都放在内存中排序(也可以是分布在100台机器的数据无法存储在1台服务器这样的分布式应用场景)。从硬盘中依次读取1GB数字,对其排序后写回硬盘。反复100次即可得到100个已序的数组;再将两个已序数组进行归并排序,排序后写回硬盘,得到更长的已序数组;之后同理。最终可将100GB的数字在硬盘上排序。

源码

MergeSort.h

MergeSort.cpp

测试

MergeSortTest.cpp

Last updated