数据结构之应用实例-算法

目录

前言

最大子列和问题

算法1

算法2

算法3(分而治之)

算法4:在线处理


前言

本篇对于数据结构里的算法,结合实例进行了较为详细的解说。

最大子列和问题

上一篇提到了有关算法分析的小技巧,下面让我们看看算法分析的应用实例,叫做最大子列和问题。

所谓的最大子列和问题就是:

 函数f(i,j)即求从Ai到Aj连续子列的和的最大值。(i,j为任意的小于N的整数,不包括0)

对N个整数来说,有很多这样的连续的子列,我们要求的是所以连续子列和里面的最大的一个。如果这个和是一个负数的话,就返回0作为结束。要解决这个问题有很多不同的算法。

算法1

将所有的连续子列和全部都算出来,然后从中找最大的那一个。(最直接)

int MaxSubseqSuml( int A[],int N)  //输入整数序列以及整数序列的个数
{
    int ThisSum,MaxSum = 0;
    int i,j,k;
    for(i=0;i<N;i++){          //i是子列的左端
        for(j=i;j<N;j++){      //j是子列的右端,j总是大于等于i
            ThisSum = 0;      //ThisSum是从A[i]到A[j]的子列和
            for(k=i;k<=j;k++)  
                ThisSum+=A[k];
            if(ThisSum>MaxSum)  //如果刚得到的这个子列和更大
                MaxSum=ThisSum; //则更新结果
        }  //j循环结束
    } //i循环结束
    return MaxSum;   
}

这个算法的复杂度

很直观的理由:有三个嵌套的for循环。如果每一层for循环都是从0到N的,那么很显然三个乘在一起就是N的立方。

这个没有那么明显,因为只有第一重循环是从0到N的,但是无论如何用一下我们中学的证明技巧,仍可以证明整个的运算量是N的立方乘一个常数。

这个算法十分的不聪明。当i=0时,每一次计算子列都要从第一个数开始相加;当i=1时,每一次计算子列都要从第二个数开始相加。

当我们知道一个子列的和,计算下一个子列的时候,只需上一个子列的和加到下一个j,即为本次子列的和,不需要每次都从头开始相加。k循环完全是多余的。

算法2

int MaxSubseqSum2(int A[],int N)
{
    int ThisSum=0;
    int MaxSum=0;
    int i,j
    for(i=0;i<N;i++){   //i是子列的左端位置
        ThisSum=0:      //ThisSum是从A[i]到A[j]的子列和
        for(j=i;j<N;j++){  //j是子列的右端位置
            ThisSum+=A[j]:  //对于相同的i,不同的j,只要在j-1的基础上累加1项即可
            if(ThisSum>MaxSum)  //如果刚得到的这个子列和更大
                MaxSum=ThisSum;  //则更新结果
        }  //j循环结束
    }  //i循环结束
    return MaxSum;
}

很显然这个算法有两个循环的嵌套,所以这个算法的复杂度

 算法2要比算法1聪明,因为当N=10000时,这两个算法的效率就相差了10000倍。但是它不是最好的。

一个专业的程序员设计了一个算法,但他发现这个算法是O(N^2)时,下意识的本能就应该想:有没有可能把它改变为N或log N呢?这样就要快很多。

算法3(分而治之)

算法3的名字叫做分而治之,可以用来解决很多的应用题。它的大概思路就是:把一个问题切分成小的块,然后分头去解决它们,最后把结果合并起来。

首先把数组二等分,然后递归的去解决左右两边的问题。递归的去解决左边的问题,会得到左边的最大子列和;递归的去解决右边的问题,会得到右边的最大子列和。但是这样我们就可以说最大子列和是在它们俩之间吗?不一定。还有一种情况,就是跨越边界的最大子列和。找到这三种结果之后,最后的结果一定就是这三个中间最大的那一个。  计算这三种情况就叫做“分”,最后总结就叫做“治”。

例如:                       |             ||            |              |||             |            ||            |

4-35-2-126-2
4526
68
11

先从左边的分析。左边的4个数被两次二等分后,先看4与-3,比较后的最大子列和为4,5与-2比较后,最大的子列和为5。跨越边界的最大子列和为6,即为4,-3,5相加的结果。右边的分析亦是如此。最后计算整个的跨越边界的最大为11。

看上去不是很复杂,但是程序比较复杂,这里就不展示程序了。

难点在于怎么分析它的算法复杂度。分析这种递归的算法略微有点难度。

它的想法是:想象当我们解决整个问题有N个数字时,如果把它的复杂度记作T(N),那么当我们将数组二等分后,得到左边的复杂度为T(N/2),因为我们的规模减半了;同样道理,右边的递归结果的时间复杂度为T(N/2)。而怎么得到中间跨边界的最大子列和呢?做法就是从中间开始,分别向左右两边扫描元素,每一个元素都被扫描了一次,所以这个复杂度应该是N的常数倍。由此可以得到关于T(N)的递推公式:

T(N) = 2T(N/2) + c N             (左边、右边、中间复杂度之和)

T(1)为一个常数,T(1) = O(1)。怎么递推呢?可以把N换成 N/2 

T(N/2) = 2T(N/4) + c N/2        

然后将T(N/2)带入T(N)中,即可得到  T(N) =  2【2T(N/2^2) + c N/2】 + c N = 2^2T(N/2^2) + 2 cN

我们会一直展开直到T里面的数为1时为止。过了k步以后,T(N) = 2^k *O(1) + c kN ,其中 N/2^k = 1

 所以在分析复杂度时,就有了两项,一项是N乘一个常数,另一项是以2为底的logN乘N乘一个常数。

上一篇中提到过当两个复杂度相加时,取复杂度较大的一项。因此我们就得到整个算法的复杂度

T(N) = O(N*logN)

算法4:在线处理

N*logN已经是很快的算法了,但是在解决这个问题来说,还不是最快的,更快的算法叫做 在线处理 算法。

int MaxSubseqSum4(int A[].int N)
{
    int ThisSum = 0;
    int MaxSum = 0;
    int i;
    for(i=0;i<N;i++){
        ThisSum += A[i];     //向右累加
        if(ThisSum > MaxSum)
            MaxSum = ThisSum; //发现更大和则更新当前结果
        else if(ThisSum < 0)  //如果当前子列和为负
            ThisSum = 0;      //则不可能使后面的部分和增大,抛弃之
    }
    return MaxSum;
}

这个算法的时间复杂度:T(N) = O(N)

算法4中只有一个for循环,for循环里面所有的if,else这些东西都是常数数量级的复杂度,所以很显然这个算法的复杂度是线性的。

这个是我们可以想象的最快的算法,当然一个算法的效率这么高,总会有副作用的。副作用是它的正确性不是那么的明显,即别人理解这个算法是怎么工作的,略微有点困难。下面来看一个具体的例子:

-13-24-616-1

当我们首次for循环时,i=0,ThisSum = -1;if被跳过去,执行else if循环,因为子列和为0,所以ThisSum=0。因为我们要求连续的子列和,下一步我们要顺着往后去求和的,如果现在的和是负数的话,不管加什么数,都只能让后面的数字越来越小,不可能越来越大,所以可以抛弃它。

第二轮我们开始读下一个数字,传进来的是3,发现3比MaxSum大,于是MaxSum更新为3。

再进行一次for循环,加下一个数,于是当前和为正1,1比MaxSum要小,所以MaxSum的值不变,ThisSum的值为1,不会被抛弃。于是我们继续进行循环。

为什么这个算法叫做“在线处理”呢?因为整个过程是一个数字一个数字的读进来,而输入如果停住,后面的输入不读了,现在的程序应返回3,作为当前的最大子列和。对于前三个数的最大子列和,这个结果是正确的。

当进入第五轮循环时,ThisSum=-1,于是ThisSum的值归0,MaxSum的值为5。第六轮循环,ThisSum=1,MaxSum=5;第六轮,ThisSum=7,MaxSum的值更新为7。

为什么会快,因为它利用了这一点:一旦发现当前子列和为负,没有用了,就直接被抛弃,扫描后面的数字。

“在线的意思是指每输入一个数据就进行即时处理,在任何一个地方中止输入,算法都能正确给出当前的解。

有兴趣的话,可以跑一下,看看4个算法的运行时间怎么样。

下面来进行运行时间比较:

运行时间比较(秒)
算法1234
时间复杂度O(N^3)O(N^2)O(NlogN)O(N)
输入规模N=100.001030.000450.000660.00034
N=1000.470150.011120.004860.00063
N=1000448.771.012330.058430.00333
N=10000NA1110130.686310.03042
N=100000NANA8.01130.29832

注:NA即为Not Available

NA即算不下去了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值