算法分析

简介: 算法分析

这篇文章目的是分析算法的复杂度问题,关于算法的定义、特性等等问题在这里不作讲解。

如何度量算法效率

我们知道,算法是解决复杂问题的思路,条条大路通罗马,对于一个复杂的问题,能够解决的算法也有很多种,对于有多种解决方案的情况,我们当然是想选择一种快速、有效的算法了。那么我们该如何知晓一个算法的效率呢?

1、事后统计法

该方法通过设计好的测试程序和数据,然后在计算机中运行,接着对运行时间进行比较,耗时少的效率高。

但很显然,这种方式有很大缺陷,首先,算法的测试数据需要花时间设计,因为不同的测试数据往往会直接影响运行时间,然后是计算机的硬件也会影响运行时间。这就造成了度量结果的不稳定。

2、事前分析法

由此,事前分析法诞生了,该方法无需运行程序,就能够分析出一个算法的效率。

经过大量分析,前辈们总结出一个算法在计算机上运行时所消耗的时间取决于以下因素:

  1. 算法采用的策略、方法
  2. 编译产生的代码质量
  3. 问题的输入规模
  4. 机器执行指定的速度

我们抛开第四点,因为第四点是由计算机硬件决定的,我们无法左右。对于一个算法的效率,它的决定因素就是算法的好坏和问题的输入规模。

通过例子来感受一下:

void main(){
   
    int i,sum = 0,n = 100;
    for(i = 1;i <= n;i++){
   
        sum += i;
    }
    printf("%d",sum);
}

这段代码相信大家都不陌生,这是一个求1到100之间数字和的程序,我们可以来分析一下程序执行的步骤。

首先,程序执行第一行代码,int i,sum = 0,n = 100;只执行了一次,然后是for循环,循环条件i = 1;i <= n;i++和循环体sum += i;分别执行了n+1次和n次,最后是输出语句printf("%d",sum);,只执行一次。

执行次数分析出来之后,我们将每句代码的执行次数相加,即:1 + (n + 1) + n + 1 = 2n + 3,这段程序所有代码的执行次数为2n+3次,我们继续看一段程序:

void main(){
   
    int sum = 0,n = 100;
    sum = (1 + n) * n / 2;
    printf("%d",sum);
}

这是刚才求和程序的改进版本,首项加尾项的和乘以项数除以2,即可求出1到100的和,我们来算算这个程序的执行次数。会发现,这段程序中总共三行代码,都只执行了一次,那么总共的执行次数就为3。

对于事前分析法,我们无需关心实现的语言种类,运行的计算机硬件,忽略循环索引的递增、循环终止条件、变量声明和输出语句等等操作,即可得到一个比较客观的算法效率。

两个程序对于同一个问题的输入规模是n(输入规模指的是输入量的多少),对于第一个程序,忽略这些因素之后,它的效率为n,第二个程序,它的效率为1。通过比较执行效率不难发现,两个算法的效率相差的不是一点。

函数的渐进增长

当然了,算法的效率度量远没有这么简单,我们通过分析来总结一下该如何去计算一个算法的效率。

首先,我们假设有两个算法,这两个算法的输入规模都为n,而算法1要做2n+3次操作,算法2要做3n+1次操作,你能从操作次数上就判断出哪个算法更高效吗?这其实是办不到的,我们分析一下:

次数 2n+3 2n 3n+1 3n
n =1 5 2 4 3
n = 2 7 4 7 6
n = 10 23 20 31 30
n = 100 203 200 301 300

通过分析发现,当n = 1时,算法1不如算法2,而当n = 2时,算法1和算法2效率相同,按照这个规律,随着n逐渐增大,算法1的效率会逐渐高于算法2。

此时给出这样的定义,当输入规模n在无限制的情况下,只要超过了一个数值N,这个函数就总是大于另一个函数,我们称函数是渐进增长的。从表格数据中,我们还可以发现,对于常数项,比如2n+3和2n在n的变化下,其函数值并不受影响,所以在计算算法效率的时候可以忽略常数项。

我们再假设有两个输入规模都为n的算法,算法1要做4n + 8次操作,算法2要做2n2+1次操作,继续分析:

次数 4n+8 n 2n2+1 n2
n = 1 12 1 3 1
n = 2 16 2 9 4
n = 10 48 10 201 100
n = 1000 4008 1000 2000001 1000000

当n = 1时,算法1不如算法2,而当n = 10时,算法1的效率高于算法2,此后,随着n的逐渐增大,算法1的优势越来越明显,我们还能发现,这里除了去掉常数项外,还去掉了与n相乘的常熟,比较n与n2,结论相同。由此可以得出结论,算法效率与最高次项的常数也没有关系。

我们继续假设有三个输入规模都为n的算法,算法1要做2n2次操作,而算法2要做3n+1次操作,算法3要做2n2+3n+1次操作,继续分析:

次数 2n2 3n+1 2n2+3n+1
n = 1 2 4 6
n = 2 8 7 15
n = 10 200 31 231
n = 100 20000 301 20301
n = 1000 2000000 3001 2003001

分析发现,随着n的逐渐增大,算法1和算法2的差距逐渐拉大,当n无穷大时,算法1和算法2的效率基本相同,所以得出结论,算法效率与函数中的常数项和其它次要项没有关系,我们只需关注最高次项。

时间复杂度

扯了这么多,就是为了引出算法的时间复杂度,在前面的分析中我们得出结论,一个算法,随着输入规模n的增大,它会越来越优于(或者差于)另一算法,这其实就是事前分析法的依据,通过算法时间复杂度来估算算法时间效。那么什么是算法的时间复杂度呢?

定义:

在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随n的变化情况并确定T(n)的数量级,算法的时间复杂度,也就是算法的时间度量,记作:T(n) = O(f(n))。它表示随着问题规模n的增大, 算法执行时间的增长率与f(n)的增长率相同,称作算法的渐进时间复杂度,简称时间复杂度。其中f(n)是问题规模的某个函数。

这样用大写字母O来体现时间复杂度的方法,我们称之为大O记法。

如何分析算法的时间复杂度

如何分析出一个算法的时间复杂度,也叫推导大O阶,其实可以根据上面分析得出的结论:

  1. 用常数1取代程序中的所有加法常数
  2. 只保留最高阶项
  3. 如果最高阶存在且不为1,则去掉与最高阶相乘的常数

由此得到的结果即为算法的时间复杂度,下面讲解一下比较常见的大O阶:

1、常数阶

看下面的程序:

void main(){
   
    int sum = 0,n = 100;
    sum = (1 + n) * n / 2;
    printf("%d",sum);
}

执行次数为3,此时根据结论,用常数1代替所有加法常数,没有最高阶项,所以该算法的时间复杂度为O(1)。

2、线性阶

看下面的程序:

void main(){
   
    int i,sum = 0,n = 100;
    for(i = 1;i <= n;i++){
   
        sum += i;
    }
    printf("%d",sum);
}

执行次数为1 + (n + 1) + n + 1 = 2n + 3,根据结论,用常数1代替加法常数,3替换为1;保留最高阶项2n,去除与最高阶项相乘的常数,所以该算法的时间复杂度为O(n)。

其实在计算的时候,我们无需这样算出每一句代码的执行次数,对于赋值、循环条件、输出语句,我们可以直接不考虑,所以可以直接得出该算法的时间复杂度为O(n)。

3、对数阶

看下面的程序:

void main(){
   
    int count = 1;
    while(count < n){
   
        count *= 2;
    }
}

该程序中我们只需得出循环次数即可求出时间复杂度,由于每次count乘以2之后,就距离n更近了一分,也就是说,有多少个2相乘后大于n,才会退出循环。由2x = n得出x = log2n,所以该算法的时间复杂度为O( logn)。

4、平方阶

看下面的程序:

void main(){
   
    int i,j,n = 100;
    for(i = 0;i < n;i++){
   
        for(j = 0;j < n;j++){
   
            printf("%d\t",n);
        }
    }
}

我们知道,对于内层循环,其时间复杂度为O(n),而外层循环不过是执行n次内层循环,所以该算法的时间复杂度为O(n2)。

那么下面这个程序的时间复杂度为多少呢?

void main(){
   
    int i,j,n = 100;
    for(i = 0;i < n;i++){
   
        for(j = i;j < n;j++){
   
            printf("%d\t",n);
        }
    }
}

这里修改了一下内层循环,让j = i。分析一下,当i = 0时,内层循环执行n次;当i = 1时,内层循环执行n - 1次;当i = n - 1时,内层循环执行1次。所以总的执行次数为n + (n - 1) + (n - 2) + …… + 1 = (1/2)n^2 + (1/2)n

根据推导大O阶的结论,保留最高阶项n2/2,去掉相乘的常数1/2,最终算法的时间复杂度为O(n2)。

5、其它大O阶

还有一些常见的大O阶在这里就不做详细讲解了,就简单地提一下:

nlogn阶 O(nlogn)
立方阶 O(n3)
指数阶 O(2n)

常用的时间复杂度所耗费的时间从小到大依次是:

O(1) < O(logn) < O(n) < O(nlogn) < O(n2) < O(n3) < O(2n) < O(n!) < O(nn)

算法分析的最坏情况

在一个算法中,很有可能会出现一些不确定的情况,例如通过循环查找一个数组中的元素,当这个值就是数组的第一个元素时,算法的时间复杂度为O(1),而如果它是最后一个元素,那么时间复杂度为O(n),这也是这个程序中的最坏情况。

对于所有情况,平均运行时间是最有意义的,然后,一段程序的平均运行时间我们很难通过分析得到,所以一般在没有特殊说明的情况下,求一个算法的时间复杂度都是指求它在最坏情况下的时间复杂度。

空间复杂度

随着互联网科技的发展,早先比较贵的存储在如今都较为便宜,所以在某些特定的场景下,可以考虑使用空间来换取时间。

例如在一个求指定年份是否为闰年的算法中,我们可以事先定义一个有限大的数组,数组下标表示年份,哪一年是闰年,对应的下标元素就为1,此时,如果想判断输入的年份是否为闰年,只需得到该下标的元素值,若为1,则是闰年;若为0,则不是闰年。这样虽然高效,但会加大存储的开销。

比如排序算法中的基数排序是用空间换取时间的经典算法。

算法的空间复杂度通过计算算法所需的存储空间实现,空间复杂度的计算公式为:S(n) = O(f(n)),其中,n为问题的规模,f(n)为语句关于n所占存储空间的函数。

通常情况下,我们更注重算法的时间复杂度,所以,空间复杂度只作为一个了解,不深入讨论。

相关文章
|
2月前
|
数据采集 机器学习/深度学习 算法
|
2月前
|
人工智能 算法 BI
第一周算法设计与分析 D : 两面包夹芝士
这篇文章介绍了解决算法问题"两面包夹芝士"的方法,通过找出两个数组中的最大最小值,计算这两个值之间的整数个数,包括特判不存在整数的情况。
|
12天前
|
算法 搜索推荐 开发者
别再让复杂度拖你后腿!Python 算法设计与分析实战,教你如何精准评估与优化!
在 Python 编程中,算法的性能至关重要。本文将带您深入了解算法复杂度的概念,包括时间复杂度和空间复杂度。通过具体的例子,如冒泡排序算法 (`O(n^2)` 时间复杂度,`O(1)` 空间复杂度),我们将展示如何评估算法的性能。同时,我们还会介绍如何优化算法,例如使用 Python 的内置函数 `max` 来提高查找最大值的效率,或利用哈希表将查找时间从 `O(n)` 降至 `O(1)`。此外,还将介绍使用 `timeit` 模块等工具来评估算法性能的方法。通过不断实践,您将能更高效地优化 Python 程序。
30 4
|
10天前
|
算法 程序员 Python
程序员必看!Python复杂度分析全攻略,让你的算法设计既快又省内存!
在编程领域,Python以简洁的语法和强大的库支持成为众多程序员的首选语言。然而,性能优化仍是挑战。本文将带你深入了解Python算法的复杂度分析,从时间与空间复杂度入手,分享四大最佳实践:选择合适算法、优化实现、利用Python特性减少空间消耗及定期评估调整,助你写出高效且节省内存的代码,轻松应对各种编程挑战。
21 1
|
1月前
|
算法 数据可视化
基于SSA奇异谱分析算法的时间序列趋势线提取matlab仿真
奇异谱分析(SSA)是一种基于奇异值分解(SVD)和轨迹矩阵的非线性、非参数时间序列分析方法,适用于提取趋势、周期性和噪声成分。本项目使用MATLAB 2022a版本实现从强干扰序列中提取趋势线,并通过可视化展示了原时间序列与提取的趋势分量。代码实现了滑动窗口下的奇异值分解和分组重构,适用于非线性和非平稳时间序列分析。此方法在气候变化、金融市场和生物医学信号处理等领域有广泛应用。
|
16天前
|
机器学习/深度学习 存储 人工智能
文本情感识别分析系统Python+SVM分类算法+机器学习人工智能+计算机毕业设计
使用Python作为开发语言,基于文本数据集(一个积极的xls文本格式和一个消极的xls文本格式文件),使用Word2vec对文本进行处理。通过支持向量机SVM算法训练情绪分类模型。实现对文本消极情感和文本积极情感的识别。并基于Django框架开发网页平台实现对用户的可视化操作和数据存储。
20 0
文本情感识别分析系统Python+SVM分类算法+机器学习人工智能+计算机毕业设计
|
2月前
|
算法
算法设计与分析作业
这篇文章是关于算法设计与分析的作业,其中包含了两个算法实现:一个是使用分治算法实现的十进制大整数相乘(包括加法、减法和乘法函数),并进行了正确性和健壮性测试;另一个是使用快速排序思想实现的分治查找第K小元素的程序,并分析了其平均和最坏时间复杂度。
算法设计与分析作业
|
2月前
|
算法 Java 测试技术
算法分析(蛮力法与减治算法应用实验报告)
这篇文章是关于算法分析的实验报告,介绍了如何使用蛮力法解决背包问题,并通过伪代码和Java代码实现,同时分析了其时间效率;还介绍了基于减治法思想实现的二叉查找树的插入与查找,同样提供了伪代码、Java源代码实现和时间效率分析,最后展示了测试结果截图。
算法分析(蛮力法与减治算法应用实验报告)
|
1月前
|
编解码 算法 图形学
同一路RTSP|RTMP流如何同时回调YUV和RGB数据实现渲染和算法分析
我们播放RTSP|RTMP流,如果需要同时做渲染和算法分析的话,特别是渲染在上层实现(比如Unity),算法是python这种情况,拉两路流,更耗费带宽和性能,拉一路流,同时回调YUV和RGB数据也可以,但是更灵活的是本文提到的按需转算法期望的RGB数据,然后做算法处理
|
2月前
|
人工智能 算法
第一周算法设计与分析:C : 200和整数对之间的情缘
这篇文章介绍了解决算法问题"200和整数对之间的情缘"的方法,通过统计数组中每个数模200的余数,并计算每个同余类中数的组合数来找出所有满足条件的整数对(i, j),使得\( A_i - A_j \)是200的整数倍。
下一篇
无影云桌面