当前位置: 首页 > news >正文

时间复杂度分析经典问题——最大子序列和

时间复杂度经典问题——最大子列和问题

最大子序列和问题

最大子列和问题是非常经典的问题,基本上讲算法的书都会将这个例子,用此例题来讲解算法时间复杂度的重要性,对比不同算法的时间复杂度。最大子列和问题如下:给定整数序列A1,A2,A3,A4,...,An(可能存在负数),求A(i)+A(i+1)+........+A(j)的最大值(无法输入公式),请看下图:
图片描述

注:为了方便起见,如果所有的整数均为负数,则最大的子序列和为0

算法的运行时间

这个问题之所以有如此的吸引力,主要是因为存在求解它的很多算法,而且这些算法的性能又差异很大。我们将讨论求解该问题的四种算法。这四种算法的运行时间如下表所示:(算法1是O(N^3),图中写错了)图片描述

  • 表中的几个重要的情况值得注意。对于小量的输入,算法可以在眨眼之间的完成,因而如果只是小量输入的情况下,那么花费大量的时间去设计优秀的算法恐怕是不值得的。另一方面,随着业务,用户的增加,小量输入的情况可能会发生变化,哪些低效率的程序可能必须要进行重写。
  • 其次,表中所给的时间不包括读入数据的所需要的时间,对于算法4,仅仅从磁盘读入数据所用的时间很可能在数量级上比求解问题所需的时间还要大。数据的读入一般是一个瓶颈,一旦数据读入,问题就会迅速解决。但是对于低效的算法,它必然要耗费大量的计算资源。因此,只要可能,使得算法足够有效而不至于成为问题的瓶颈是非常重要的。

我们还可以通过函数曲线来对这四种算法的时间复杂度函数进行分析,通过曲线我们清楚的可以看出O(nlgn)算法时间复杂度是介于O(n^2)的O(n)之间的,当然这也不难证明。在实际的情况中,当我们采用O(n^2)算法的时候,应该在仔细想想,能否将算法的时间复杂度优化成O(nlgn),这对算法的性能提升也是非常巨大的,不妨要问,为什么不优化为O(n)呢?事实上,O(n)时间复杂度意味着只需要进行一次扫描,就能找到问题的解,在大部分的问题中,这是非常的困难的。
图片描述

O(n^3)算法
#include<iostream>
#include<stdio.h>
using namespace std;

int MaxSubsequenceSum(int a[],int n);

int main(){
    //int a[6] = {-2, 11, -4, 13, -5, -2};
    int a[8] = {4, -3, 5, -2, -1, 2, 6, -2};
    printf("%d\n",MaxSubsequenceSum(a,8));
}

int MaxSubsequenceSum(int a[],int n){
    int ThisSum, MaxSum;
    MaxSum = 0;
    for(int i = 0; i < n; i++){
        for(int j = i; j < n; j++){
            ThisSum = 0;
            for(int k = i; k <= j; k++){
                ThisSum += a[k];
            }
            if(ThisSum > MaxSum){
                MaxSum = ThisSum;
            }
        }
    }
    return MaxSum;
}

这是一种O(n^3)的解法,说实话,我是写不来这样高时间复杂度的算法,这个算法重复做了很多的无用的计算,强行将算法复杂化,经过简单的分析,直接可以求 ThisSum += a[k] 语句的次数,就能够得出它的时间复杂度:图片描述

O(n^2)算法

对上述的算法直接优化,我们发现最里面的循环是完全多余的,很过分的消耗了大量的时间,很容易就能得到下面的算法

int MaxSubsequenceSum(int a[],int n){
    int ThisSum, MaxSum;
    MaxSum = 0;
    for(int i = 0; i < n; i++){
        ThisSum = 0;
        for(int j = i; j < n; j++){
            ThisSum += a[j];
            if(ThisSum > MaxSum){
                MaxSum = ThisSum;
            }
        }
    }
    return MaxSum;
}

相信大部分人首想想到的应该是这个算法把,这个算法性能只能说还行。但是,我们想到了O(n^2)的时候,应该多思考一下,能否将其转化为O(nlogn)呢?如果能的话,这将会极大的提高算法的性能。

O(nlogn)算法

如果没有O(n)算法的话,那么递归的威力就能体现出来了。这个算法采用的是分治策略,分治思想是把所求问题划分成两个大致相等的问题,然后递归的对它进行求解,这是分的思想,治的阶段是将两个子问题的解合并到一起,最后得到整个问题的解。
在这个问题中,最大的子序列和可能出现在三处,要么是序列的左半部分,要么是序列的右半部分,要么是跨越输入数据的中间左右部分都有,前面的两种情况可以用递归进行求解,第三种情况的最大子序列和可以通过求出前半部分的最大和以及后半部分的最大和而得到,我们可以通过下面的例子进行分析:图片描述

  • 前半部分最大子序列和为6,
  • 后半部分的最大子序列和为8。
  • 前半部分包含最后一个元素的最大和是4,而后半部分包含第一个元素的的最大和是7,因此跨越两部分的最大和是11,这是最大的子列和。

这个算法的源码有点复杂,仔细读几遍。

int MaxSubSum(int A[], int Left, int Right){
    int MaxLeftSum, MaxRightSum;
    int MaxLeftBorderSum, MaxRightBorderSum;
    int LeftBorderSum, RightBorderSum;
    int Center;
    if(Left == Right){
        if(A[Left] > 0){
            return A[Left];
        }else{
            return 0;
        }
    }
    
    Center = (Left + Right) / 2;
    MaxLeftSum = MaxSubSum(A, Left, Center);   //递归求解左半部分的最大和
    MaxRightSum = MaxSubSum(A, Center + 1, Right);  //递归求解右半部分的最大和
    
    MaxLeftBorderSum = 0;
    LeftBorderSum = 0;
    for(int i = Center; i >= Left; i--){
        LeftBorderSum += A[i];
        if(LeftBorderSum > MaxLeftBorderSum){
            MaxLeftBorderSum = LeftBorderSum;
        }
    }
    
    MaxRightBorderSum = 0;
    RightBorderSum = 0;
    for(int i = Center+1; i <= Right; i++){
        RightBorderSum += A[i];
        if(RightBorderSum > MaxRightBorderSum){
            MaxRightBorderSum = RightBorderSum;
        }
    }
    return Max3(MaxLeftBorderSum+MaxRightBorderSum,MaxLeftSum,MaxRightSum);
}

int Max3(int a, int b, int c){
    if(a>b){
        return a > c ? a : c;    
    }else{
        return b > c ? b : c;
    }
}

int MaxSubsequenceSum(int a[],int n){
    return MaxSubSum(a, 0, n-1);
}

时间复杂度分析
有兴趣的同学可以参考网易公开课:麻省理工学院公开课:算法导论,第三集分治法,讲的非常详细,还有推导过程。

O(n)算法
int MaxSubsequenceSum(int a[],int n){
    int ThisSum = 0, MaxSum = 0;    
    for(int j = 0; j < n; j++){
        ThisSum += a[j];
        if(ThisSum > MaxSum){
            MaxSum = ThisSum;
        }else if (ThisSum < 0){
            ThisSum = 0;        //ThisSum < 0,说明跨越a[j]不能使序列和变大
        }
    }
    return MaxSum;
}

这个算法的效率非常的高,又被称为在线处理算法,算法只需要扫描一遍序列,就能找到最大的子序列和,它的技巧就是一旦A[i]被读入并被处理,它就不再需要被记忆。不仅如此,在任意时刻,算法都能够对它已经读入的数据给出正确的答案。具有这种特性的算法叫做联机算法。仅需要常量的空间并以线性时间运算的联机算法集合是完美的算法。

相关文章:

  • Android Studio踩过的坑
  • 细说Redis(一)之 Redis的数据结构与应用场景
  • Python变量的相互转换
  • 2018.10.23-dtoi-1770不设找零No Change (nochange)
  • 【NOIP2017D2T3】列队
  • Algs4-1.3.13判断正确的出队次序
  • Dubbo分析之Exchange层
  • QML-qmake大法
  • DreamWeaver使用小结
  • Js jquery常用的身份证号码 邮箱电话等验证
  • POI 2018.10.27
  • w3c xml
  • Jmeter----逻辑控制器(Logic Controller)
  • Selenium实战教程系列(二)---元素定位
  • Spark内置框架rpc通讯机制及RpcEnv基础设施-Spark商业环境实战
  • [case10]使用RSQL实现端到端的动态查询
  • 2018一半小结一波
  • CSS实用技巧干货
  • JS笔记四:作用域、变量(函数)提升
  • Laravel 实践之路: 数据库迁移与数据填充
  • laravel5.5 视图共享数据
  • linux安装openssl、swoole等扩展的具体步骤
  • Mysql5.6主从复制
  • npx命令介绍
  • Spark学习笔记之相关记录
  • Tornado学习笔记(1)
  • Vue UI框架库开发介绍
  • 对话 CTO〡听神策数据 CTO 曹犟描绘数据分析行业的无限可能
  • 如何实现 font-size 的响应式
  • 如何用vue打造一个移动端音乐播放器
  • 使用putty远程连接linux
  • 系统认识JavaScript正则表达式
  • 一道闭包题引发的思考
  • #1014 : Trie树
  • #LLM入门|Prompt#3.3_存储_Memory
  • #多叉树深度遍历_结合深度学习的视频编码方法--帧内预测
  • #基础#使用Jupyter进行Notebook的转换 .ipynb文件导出为.md文件
  • (C语言)深入理解指针2之野指针与传值与传址与assert断言
  • (day 12)JavaScript学习笔记(数组3)
  • (八)Flask之app.route装饰器函数的参数
  • (笔试题)合法字符串
  • (强烈推荐)移动端音视频从零到上手(下)
  • (算法)Travel Information Center
  • (转载)在C#用WM_COPYDATA消息来实现两个进程之间传递数据
  • .Family_物联网
  • .htaccess配置常用技巧
  • .net图片验证码生成、点击刷新及验证输入是否正确
  • .NET中的Event与Delegates,从Publisher到Subscriber的衔接!
  • .net中生成excel后调整宽度
  • .vimrc php,修改home目录下的.vimrc文件,vim配置php高亮显示
  • /3GB和/USERVA开关
  • []C/C++读取串口接收到的数据程序
  • [1181]linux两台服务器之间传输文件和文件夹
  • [20170705]diff比较执行结果的内容.txt
  • [2018-01-08] Python强化周的第一天