归并排序及其应用

1 归并排序的特点

归并排序是一种利用分治技术来实现一种稳定排序算法,该算法的时间复杂度为

O(nlogn),该算法的常数因子比较大,通常应用于数据量比较大的场合。通常,我们所学习的归并排序算法都是二路归并,本文也主要来讨论二路归并排序算法。

分治法的一般的求解步骤为:

a 分解:将原问题分解为一系列子问题

b 解决:递归地求解各子问题

c 合并:将子问题的结果合并为原问题的解

2 归并排序算法

归并排序是按照分治的思想来设计的,我们所熟悉的二路归并就是每次将问题分解为2个子问题来求解,其步骤为(假设我们需要对n个数的序列进行排序):

a 分解:将n个数的序列分解为2个包含n/2个数的子序列;

b 解决:使用合并排序算法递归地对这2个子序列进行排序;

c 合并:合并2个已经排序的子序列以得到最终的排序结果。

在对子序列排序时,当子序列中只含有1个数时递归结束,即单个数被认为是已经有序。具体的算法可以用以下代码来实现:

int y[10000];

//对2个有序的子序列(y[l...mid]和y[mid+1...r])进行排序

void Merge (int l, int mid, int r)
{
    int i,j,*R,*L,k;
    int nl,nr;
    nl = mid - l + 1;
    nr = r - mid;
    R = new int [nr];
    L = new int [nl];
    for (i = 0; i < nl; i++)
        L[i] = y[l + i];
    for (j = 0; j < nr; j++)
        R[j] = y[mid + j + 1];
    i = 0;
    j = 0;
    k = l;
    while (i < nl && j < nr)
    {
        if (L[i] <= R[j])
        {
            y[k++] = L[i++];
        }
        else
        {
            y[k++] = R[j++];
        }
    }
    while (i < nl)
    {
        y[k++] = L[i++];
    }
    while (j < nr)
    {
        y[k++] = R[j++];
    }
    free (R);
    free (L);
}

//2路归并排序

void Mergesort (int l, int r)
{
    int mid;
    if (l < r)
    {
        mid = (l + r) / 2;
        Mergesort (l,mid);
        Mergesort (mid + 1, r);
        Merge (l, mid, r);
    }
}

3 归并排序的应用

归并排序最为经典的一个应用就是求一个整数序列中逆序对数了。

exp01 设a[1...n]是一个包含n个不同数的数组,若当i<j时,有a[i] >a[j],则称(i,j)就是一个逆序对,现在要求设计一个O(nlog(n))的算法来求解数组a中逆序对数。

我们可以通过分析2路归并排序来解决该问题,在二路归并排序的Merge函数中,该函数每次都是合并2个有序的子序列,在统计逆序时,我们只需修改该函数即可。

分析:对于序列a[l...r],我们把它分解为a[l...mid]和a[mid+1...r],设序列a[l...r]的逆序数为s[l...r],其它的也类似。那么,

s[l...r] = s[l...mid] + s[mid+1...r] + sum(a[l...mid], a[mid+1...r])

其中,sum(a[l...mid], a[mid+1...r])表示子序列a[l...mid]中的元素比子序列a[mid+1...r]中的元素大的对数,即若x为a[l...mid]中的一个元素,y为a[mid+1...r]中的一个元素,则称(x,y)为一个“数对”,sum(a[l...mid], a[mid+1...r])表示“数对”的个数。很显然,sum(a[l...mid], a[mid+1...r])与子序列a[l...mid]和a[mid+1...r]是否有序无关。因此,我们就可以在a[l...mid]和a[mid+1...r]有序的情况下来计算sum(a[l...mid], a[mid+1...r])。很显然,我们只需对归并排序中的Merge函数稍做修改即可达到目的。具体的实现代码如下:

int y[10000];

int sum = 0; //全局变量,用于统计逆序对的个数

//对2个有序的子序列(y[l...mid]和y[mid+1...r])进行排序

void Merge (int l, int mid, int r)
{
    int i,j,*R,*L,k;
    int nl,nr;
    nl = mid - l + 1;
    nr = r - mid;
    R = new int [nr];
    L = new int [nl];
    for (i = 0; i < nl; i++)
        L[i] = y[l + i];
    for (j = 0; j < nr; j++)
        R[j] = y[mid + j + 1];
    i = 0;
    j = 0;
    k = l;
    while (i < nl && j < nr)
    {
        if (L[i] < R[j]) //注意等号没了
        {
            y[k++] = L[i++];
        }
        else
        {

    sum += nl - i;

            y[k++] = R[j++];
        }
    }
    while (i < nl)
    {
        y[k++] = L[i++];
    }
    while (j < nr)
    {
        y[k++] = R[j++];
    }
    free (R);
    free (L);
}

//2路归并排序

void Mergesort (int l, int r)
{
    int mid;
    if (l < r)
    {
        mid = (l + r) / 2;
        Mergesort (l,mid);
        Mergesort (mid + 1, r);
        Merge (l, mid, r);
    }
}

PS: 注意代码中的红色部分。

下面我们再来看一个例子,该例子好像是08年ACM ICPC亚洲区预选赛网选赛的一道题目。题目的大意如下:

exp02: 假设有n个士兵站成一排,每个士兵都有一个分数(该分数为正整数),现在要从中选出若干个士兵(>=1),要求所选出的士兵必须是相邻的,而且所选出的士兵的分数的平均值必须大于等于预先给定的一个常数b,求有多少中选法?

分析:假设a[1...n]代表n个士兵的分数,即a[i]代表第i个士兵的分数,即问题是求

(a[i]+a[i+1]+...+a[j]) / (j - i +1) >= b的(i,j)的个数。我们假设s[i]=a[1]+...a[i],且设

s[0] = 0,则问题转换为求(s[j] - s[i]) / (j - i) >= b的(i,j)的个数。我们很容易发现,(s[j] - s[i]) / (j - i) 类似与数学中直线的斜率,我们把(i,s[i])视为直角坐标系中的一个点,那么(s[j] - s[i]) / (j - i) 表示通过(i,s[i])和(j,s[j])两点的直线的斜率。又因为s[i]数组具有严格的单调性,即s[i+1]>s[i](因为a[i+1]为正整数),所以我们就可以把问题转换为一个求逆序对的问题。我们将点(i,s[i])映射到y轴上,其映射方法是过点(i,s[i])作一条斜率为b的直线,该直线与y轴的交点即为我们所映射的点(假设该点的坐标为(0,y[i])),即显然有,y[i] = s[i] - b*i 。于是,我们只需要求解数组y[i]中的逆序对数sums即可,然后再(n+1)*n / 2 - sums即可为我们所要求的解。至此,我们就可以直接套用求逆序对的O(nlog(n))的算法。

附:这里需要注意的是,当序列中存在2个数相等时也要认为是逆序,序列1,2,2中的逆序对为1个,而不是0个。至于问题的解为什么是(n+1)*n / 2 - sums以及为什么对点(i,s[i])以斜率b映射到y轴上,这个问题读者自己琢磨一下就会明白,由于我这里没有画图工具,读者只需画一个直角坐标系就会明白的,问题的难点就在于将原问题转换为一个求逆序对的问题。

 

Rong-Hua Li

2009.2.12日记

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值