归并排序

归并排序是一种基于分治策略的排序算法,通过递归地将序列分解为两半,分别排序后合并,实现整体有序。其最坏情况运行时间为Θ(nlog2n)。本文详细介绍了归并排序的概念、算法分析、C语言实现以及算法改进,探讨了在不同情况下如何选择合适的子序列长度k以优化性能。

概念

归并排序是属于分治算法。
许多有用的算法在结构上是递归的:为了解决一个给定的问题,算法一次或多次递归地调用其自身以解决机密相关的若干子
问题。这些算法典型地遵循分治法的思想:将原问题分解为几个规模较小但类似于原问题的子问题,递归地求解这些子问题,
然后再合并这些子问题的解来建立原问题的解。
分治模式在递归时都要三个步骤:
分解原问题为若子问题,这些子问题是原问题的规模较小的实例。
解决这些子问题,递归地求解各个子问题,然而,若子问题的规模足够小,则直接求解。
合并这些子问题的解成原问题的解。

归并排序算法完全遵循分治模式。直观上其操作如下:
分解:分解待排序的 n 个元素的序列成各具 n/2 元素的两个子序列。
解决:使用归并排序递归地排序两个子序列。
合并:合并两个已排序的子序列以产生已排序的答案。

如我们通过调用一个辅助过程MERGE(A, p, q, r) 来完成合并,其中A是一个数组,p、q 和 r 是数组下标,满足p<= q < r.
该过程假设子数组A[p…q] 和 A[q+1…r]都已经排好序。它合并这两个子数组形成单一的已经排好序的子数组并代替当前的子数组A[p…r]。

MERGE例子:

有两副已经排好序(从上到下,从小到大)的扑克牌,牌面朝上,现在要将这两副牌合并为一副排好序的牌。
两副牌(即两个输入堆)最上面的两张牌比较,拿较小的出来一张另外放牌背朝上,当做第3副牌(即输出堆),那么拿出较小一张的那一副牌就会露出下一张,
重复此步骤,直到输入堆为空。
伪代码:
使用哨兵

MERGE(A, p, q, r)
n1 = q - p + 1
n2 = r - q
let L[1..n1+1] and R[1..n2+1] be new arrays
for i=1 to n1
    L[i] = A[p+i-1]
for j=1 to n2
    R[j] = A[q+j]
L[n1+1] = ∞
R[n2+1] = ∞
i = 1
j = 1
for k=p to r
    if L[i] <= R[j]
        A[k] = L[i]
        i = i + 1
    else 
        A[k] = R[j]
        j = j + 1

不要哨兵

MERGE(A, p, q, r)
n1 = q - p + 1
n2 = r - q
let L[1..n1+1] and R[1..n2+1] be new arrays
for i=1 to n1
    L[i] = A[p+i-1]
for j=1 to n2
    R[j] = A[q+j]
i = 1
j = 1
for k=p to r
    if i<=n1 and j <=n2
        if L[i] <= R[j]
            A[k] = L[i]
            i = i + 1
        else 
            A[k] = R[j]
            j = j + 1
    else if i>n1 and j<=n2
        A[k] = R[j]
        j = j + 1
    else if i<=n1 and j>n2
        A[k] = L[i]
        i = i + 1

MERGE过程图:

阴影(即颜色比较重的)的表示已经读取的数据:
这里写图片描述
这里写图片描述

MERGE_SORT总过程

MERGE-SORT(A, p, r)
if p < r
    q = [(p + r)/2]
    MERGE-SORT(A, p, q)
    MERGE-SORT(A, q + 1, r)
    MERGE(A, p, q, r)

总过程图

这里写图片描述

算法分析

分析分治算法

当一个算法包含其自身的递归调用时,我们往往可以用递归方程或递归式来描述其
运行时间,该方程根据在较小输入上的运行时间来描述在规模为 n 的问题上的
总运行时间。
假设 T(n) 是规模为 n 的一个问题的运行时间。若问题规模足够小,如对某个
常量 c, nc, 则直接求解,需要常量时间,我们将其写作 Θ(1).
假设把问题分解成 a 个子问题,每个子问题的规模是原问题的 1/b .(对于归并排序,
a 和 b 都为 2,然而,我们将看到在许多分治算法中,ab)为了求解一个规模为
n/b 的子问题,需要T(n/b)的时间, 所以需要 aT(n/b) 的时间来求解 a 个子问题。
如果分解问题需要时间D(n), 合并子问题的解成原问题的解需要时间C(n),那么得
递归式:

T(n)={Θ(1)ncaT(n/b)+D(n)+C(n)(1)

归并排序算法分析

虽然MERGE-SORT的伪代码在元素的数量不是偶数时也能正确地工作,但是,如果假定
问题规模是2的幂,那么基于递归的分析将被简化。这时每个分解步骤将产生规模刚好
为n/2的两个子序列。
下面我们分析建立归并排序n个数的最坏情况运行时间T(n)的递归式。归并排序
一个元素需要常量时间。当有 n > 1个元素时,我们分解运行时间如下:
分解:分解步骤仅仅计算子数组的中间位置,需要常量时间,因此,D(n)=\varTheta(1)
解决:我们递归的求解两个规模均为 n/2的子问题,将贡献2T(n/2)的运行时间.
合并:我们已经注意到在一个具有n个元素的子数组上过程MERGE需要\varTheta(n)
时间,所以C(n)=\varTheta(n).
为了分析归并排序而把函数D(n)与C(n)相加时,我们是在把一个\varTheta(n)函数与
另一个\varTheta(1)函数相加.相加的和是n的一个线性函数, 即\varTheta(n).
则归并排序的最坏运行时间T(n)的递归式:

T(n)={Θ(1)n=12T(n/2)+Θ(n)n>1(2)

为了直观的理解递归式(2)的解为什么是T(n)=Θ(nlgn), 我们并不需要主定理,所以,我们把式子(2)重写为:
T(n)={cn=12T(n/2)+c(n)n>1(3)

其中常量c代表求解规模为1的问题所需要的时间, 以及在分解步骤与合并步骤处理每个数组元素所需要的时间。(其实,一个常量c并不能代表2种情况的,我们只是去两种情况中常量里面的较大的一个为上界,较小的一个为下界,两个界的阶都是 nlog2n ,合在一起将给出运行时间Θ(nlog2n)).

递归式(2)的解T(n)=Θ(nlog2n),为了直观的理解解为什么是这个,我们直接上递归树的图:
这里写图片描述

(d)部分,完全扩展了递归树具有 log2n+1 层,高度为 log2n,每层将贡献总代价cn,所以总代价为 cnlog2n+cn,它就是Θ(nlog2n)

C语言实现完整归并排序过程:

#include <stdio.h>
void merge(int A[], int p, int q, int r);
void merge_sort(int A[], int p, int r);
int main(void)
{
    int i, n;
    int A[] = {3, 41, 52, 26, 38, 57, 9, 49};
    n = sizeof(A)/sizeof(int);
    printf("before merge sort array is:\n");
    for (i=0; i<n; ++i){
        printf("%d ", A[i]);
    }
    printf("\n");
    merge_sort(A, 0, n-1);  
    printf("after merge sort array is:\n");
    for (i=0; i<n; ++i){
        printf("%d ", A[i]);
    }
    printf("\n");   
    return 0;
}
void merge(int A[], int p, int q, int r)
{
    int i, j, k;
    int L[r];
    int R[r];
    int n1 = q - p + 1;
    int n2 = r - q;
    // let L[1...n1+1] and R[1...n2+1] be new arrays
    for (i=1; i<=n1; ++i){
        L[i] = A[p+i-1];
    }
    for (j=1; j<=n2; ++j){
        R[j] = A[q+j];
    }
    i = 1;
    j = 1;
    for (k=p; k<=r; ++k){
        if (i<=n1 && j<=n2){
            if(L[i] <= R[j]){
                A[k] = L[i];
                ++i;
            }
            else{
                A[k] = R[j];
                ++j;
            }
        }
        else if (i>n1 && j<=n2){
            A[k] = R[j];
            ++j;
        }
        else if (i<=n1 && j>n2){
            A[k] = L[i];
            ++i;
        }
    }
}
void merge_sort(int A[], int p, int r)
{
    int q = 0;
    if (p < r){
        q = (p + r)/2;
        merge_sort(A, p, q);
        merge_sort(A, q + 1, r);
        merge(A, p, q, r);  
    }
}

算法改进

虽然,归并排序的最坏情况运行时间为 Θ(nlog2n), 而插入排序的最坏情况运行时间为
Θ(n2), 但是插入排序中的常量因子可能使它在 n 较小时,在许多机器上实际运行得更快。
因此,在归并排序中当问题变得足够小时,采用插入排序来使递归的叶变粗是有意义的。考虑对归并排序的一种修改,其中使用插入排序来排序长度为k 的n/k个子表,然后使用标准的合并机制来合并这些子表,这里k是一个特定的值。

a. 证明:插入排序最坏情况可以在Θ(nk)时间内排序每个长度为k的n/k个子表.
证明:
因为插入排序的最坏情况为T(k)=Θ(k2)
故有,T(n)=Θ(k2n/k)=Θ(nk).

b.证明这些子列表可以在Θ(nlog2(n/k))最坏情况时间内完成合并.
证明:
每一层代价都是Θ(n), 共有log2(n/k)+1层,因此有,Θ(n)[log2(n/k)+1]=Θ(nlog2(n/k)).

c. 假定修改后的算法的最坏情况运行时间为 Θ(nk+nlog2(n/k)),要是修改后的算法与标准的归并排序具有相同的运行时间,作为n的一个函数,借记Θ,k的最大值是 什么?
k=log2n.

d. 那么在实践中,我们应该如何选择k?
在满足插入排序比合并排序更快的情况下,k取最大值.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值