算法分析

简介: 算法分析

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

如何度量算法效率

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

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月前
|
机器学习/深度学习 算法 搜索推荐
从理论到实践,Python算法复杂度分析一站式教程,助你轻松驾驭大数据挑战!
【10月更文挑战第4天】在大数据时代,算法效率至关重要。本文从理论入手,介绍时间复杂度和空间复杂度两个核心概念,并通过冒泡排序和快速排序的Python实现详细分析其复杂度。冒泡排序的时间复杂度为O(n^2),空间复杂度为O(1);快速排序平均时间复杂度为O(n log n),空间复杂度为O(log n)。文章还介绍了算法选择、分而治之及空间换时间等优化策略,帮助你在大数据挑战中游刃有余。
87 4
|
4月前
|
人工智能 算法 BI
第一周算法设计与分析 D : 两面包夹芝士
这篇文章介绍了解决算法问题"两面包夹芝士"的方法,通过找出两个数组中的最大最小值,计算这两个值之间的整数个数,包括特判不存在整数的情况。
|
20天前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
49 1
|
2月前
|
并行计算 算法 IDE
【灵码助力Cuda算法分析】分析共享内存的矩阵乘法优化
本文介绍了如何利用通义灵码在Visual Studio 2022中对基于CUDA的共享内存矩阵乘法优化代码进行深入分析。文章从整体程序结构入手,逐步深入到线程调度、矩阵分块、循环展开等关键细节,最后通过带入具体值的方式进一步解析复杂循环逻辑,展示了通义灵码在辅助理解和优化CUDA编程中的强大功能。
|
2月前
|
算法
PID算法原理分析
【10月更文挑战第12天】PID控制方法从提出至今已有百余年历史,其由于结构简单、易于实现、鲁棒性好、可靠性高等特点,在机电、冶金、机械、化工等行业中应用广泛。
|
3月前
|
算法 搜索推荐 开发者
别再让复杂度拖你后腿!Python 算法设计与分析实战,教你如何精准评估与优化!
在 Python 编程中,算法的性能至关重要。本文将带您深入了解算法复杂度的概念,包括时间复杂度和空间复杂度。通过具体的例子,如冒泡排序算法 (`O(n^2)` 时间复杂度,`O(1)` 空间复杂度),我们将展示如何评估算法的性能。同时,我们还会介绍如何优化算法,例如使用 Python 的内置函数 `max` 来提高查找最大值的效率,或利用哈希表将查找时间从 `O(n)` 降至 `O(1)`。此外,还将介绍使用 `timeit` 模块等工具来评估算法性能的方法。通过不断实践,您将能更高效地优化 Python 程序。
68 4
|
3月前
|
算法 程序员 Python
程序员必看!Python复杂度分析全攻略,让你的算法设计既快又省内存!
在编程领域,Python以简洁的语法和强大的库支持成为众多程序员的首选语言。然而,性能优化仍是挑战。本文将带你深入了解Python算法的复杂度分析,从时间与空间复杂度入手,分享四大最佳实践:选择合适算法、优化实现、利用Python特性减少空间消耗及定期评估调整,助你写出高效且节省内存的代码,轻松应对各种编程挑战。
59 1
|
2月前
|
算法
PID算法原理分析及优化
【10月更文挑战第6天】PID控制方法从提出至今已有百余年历史,其由于结构简单、易于实现、鲁棒性好、可靠性高等特点,在机电、冶金、机械、化工等行业中应用广泛。
|
3月前
|
算法 数据可视化
基于SSA奇异谱分析算法的时间序列趋势线提取matlab仿真
奇异谱分析(SSA)是一种基于奇异值分解(SVD)和轨迹矩阵的非线性、非参数时间序列分析方法,适用于提取趋势、周期性和噪声成分。本项目使用MATLAB 2022a版本实现从强干扰序列中提取趋势线,并通过可视化展示了原时间序列与提取的趋势分量。代码实现了滑动窗口下的奇异值分解和分组重构,适用于非线性和非平稳时间序列分析。此方法在气候变化、金融市场和生物医学信号处理等领域有广泛应用。
186 19
|
4月前
|
算法
算法设计与分析作业
这篇文章是关于算法设计与分析的作业,其中包含了两个算法实现:一个是使用分治算法实现的十进制大整数相乘(包括加法、减法和乘法函数),并进行了正确性和健壮性测试;另一个是使用快速排序思想实现的分治查找第K小元素的程序,并分析了其平均和最坏时间复杂度。
算法设计与分析作业