数据结构与算法之复杂度三步走

1、大O记法

计算机怎么判断程序性能?

我们都知道,编程基本上是在和数据打交道,大多数程序基本都在处理获取数据、查询数据、操作数据、返回数据相关的逻辑

因此出现了数据结构和算法,这两者出现本质为了解决如何能够更快、更省进行数据处理

更快:程序运行时间更短,对应的指标我们称之为时间复杂度

更省:程序运行所消耗的内存更少,对应的指标我们称之为空间复杂度

为什么需要复杂度?

我相信,大家第一眼看到程序运行时间更短,消耗的内存更少这句话,第一反应通过开始运行和结束运行两个时间点差额和内存使用情况

通过这种计算方法可以判断性能,为什么还需要进行复杂度分析呢?主要有三个原因:

  • 测试结果大大依赖硬件条件

    以时间举例,你不可能绝对的说某个代码需要跑10s,在某台机器上需要跑10s。因为程序的执行和机器的性能正向相关,如果放在一台老的机器上可以会大大多于10s,而放在一款未来高性能机器上可能只需要5s。因此受硬件环境的影响,并不能简单的用时间去统计

  • 测试结果事后才能计算

    上面所有的操作,往往都是在程序运行之后才能形成统计结果,但我们大多希望在伪码阶段,便能预测程序的执行性能。因此这种事后统计法不能作为性能预测的方法

  • 测试结果受原始数据特性影响大

    我们以排序为例,需要多少时间呢?试想一下如果给出的数据就已经是排好序的,那需要的时间几乎等于0,而如果给出的数据是完全的打乱的,那需要时间肯定非常长。因此我们发现测试结果本身就存在差异,除非我们的样本足够的多,否则计算出来的结果往往具有片面性

  • ......

总结:综上所述,我们需要一个不用具体的测试数据和测试环境,就可以粗略地估计算法执行效率的方法。这个方法称作为复杂度,称作为大О记法

2、时间复杂度

如何计算时间复杂度呢?由于无法使用实际运行时间预估程序的运行时间,我们可以使用步数作为时间复杂度的计算

所谓步数,就是数组的每次索引值的读取,就算作一步,也可以称为 unit_time

案例1:数组值获取 (O(1))

如下代码:

int a[] = {0, 1, 2, 3, 4, 5, 6};
System.out.println(a[3]); // step1

计算机可以通过一步跳到任意一个索引的位置进行数据的读取

如果用大〇记法,称作为常数时间---O(1)。即:无论数组的长度多少,获取数组中某个值的都是一步到位,并且随着数组数量的增加,时间复杂度并不会提升

如果用数学公式 f(x) 表示程序运行所需要的时间(x 表示数组的长度,后面坐标图都一样),结果是:f(x) = 1

对数时间---O(log(N)) 与 O(1) 比较可以发现:o(log(N))o(1)增长的更快一些,性能更差一点

案例2:计算 N 的阶乘 (O(N))

题目:我们需要定义一个函数,这个函数能计算传入参数 N 的阶乘 N!

f(N) = N *(N-1)*(N -2) * ...* 2* 1

 public int factorial(int n) {
     int result = 1; //step1
     for (int i = 1; i <= n; i++) { //step2
         result *= i;  // step3
     }
     return result;
 }

step1∶初始化变量result 1步

step2:初始化变量i 1步,i++相当于i = i + 1,需要执行N次,因此 step2一共N+1步

step3:执行N步

(在此只考虑赋值的情况,i <= n 代码暂不考虑)

在这种情况下,一共的时间复杂度为 2N+2,使用大O记法,称作为 线性时间---O(2N+2)

从这两个案例可以看出,大O记法表示的是:代码执行时间随着数据规模增长变化的趋势,也就是需要把 N 从 0 开始逐次增加,观察其中的变化。而在这种情况下,常量、系数部分并不决定增长趋势,所以可以忽略。所以上面O(2N+2),通常会表示为O(N)

案例3:计算一个数组中所有的组合方式 (O(N^2))

我们需要定义一个函数,能打印传入参数的两两组合情况。例如:我们传入一个数组{ "hello", "spring" ,"springmvc", "mybatis"},里面有4个英文词语,我们希望两两组合,那么一共有哪些组合情况呢?

public void combine(String args[]) {
     for(int i=0; i<arg.length; i++) { //step1
         for(int j=i+1; j<args.length; j++) { //step2
             System.out.println(args[i] + args[j]); //step3
         }
     }
 }

最终的结果为 N^2 +N+1 步。注意,此次有指数出现了,我们称为 指数时间--O(N^2 +N+1),忽略常数为 O(N^2)

对于指数时间而言,线性时间都可以被忽略,例如:O(N^2+N+1),最终忽略以后的结果为 O(N^2)

总结:从上面3个案例中可以得出:大O记法,只保留最大趋势公式,并且 指数 > 线性 > 对数 > 常数,因此在计算时间复杂度的时候,其实不用一行行看代码,只需要关注 for循环嵌套情况,在写代码时,如果能用对数复杂度的代码替换线性复杂度的代码,那就是大大的性能优化

3、空间复杂度

如何计算空间复杂度?

空间复杂度和时间复杂度一样,同样遵循 大O记法。时间复杂度是以步作为基础单位,空间复杂度是以一个基础数据类型值当做基础单位

如:

O(1):

 int a = 0;
 int j = 0;

O(N):

 int a[] = new int[n];

优先考虑时间复杂度

在实际编程过程中,大多数情况只会考虑时间复杂度!!

因为我们默认计算机内存是足够大的,足够给我们使用的

如摩尔定律所说的那样:当价格不变时,集成电路上可容纳的元器件的数目,约每隔18-24个月便会增加一倍,性能也将提升一倍。换而言之,每个18-24个月,同样大小的芯片里可存储的数据量将翻一倍。因此完全可以忽略内存空间不够的情况。一般只有参与嵌入式开发、单片机相关的编程开发才需要注意空间的问题。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值