1.时间&空间复杂度

衡量你编写的算法代码的执行效率:时间&空间复杂度

1.什么是时间复杂度?

时间复杂度的全称是渐进时间复杂度,表示算法的执行时间与数据规模之间的增长关系。

2.什么是空间复杂度?

空间复杂度全称就是渐进空间复杂度(asymptotic space complexity),表示算法的存储空间与数据规模之间的增长关系。

参考文章:https://www.zhihu.com/question/21387264

3.时间和空间复杂度分析的必要性

你可能会有些疑惑,我把代码跑一遍,通过统计、监控,就能得到算法执行的时间和占用的内存大小。为什么还要做时间、空间复杂度分析呢?这种分析方法能比我实实在在跑一遍得到的数据更准确吗?

事后统计法。但是,这种统计方法有非常大的局限性。

  • \01. 测试结果非常依赖测试环境

    测试环境中硬件的不同会对测试结果有很大的影响。

  • \02. 测试结果受数据规模的影响很大

    对同一个排序算法,待排序数据的有序度不一样,排序的执行时间就会有很大的差别。极端情况下,如果数据已经是有序的,那排序算法不需要做任何操作,执行时间就会非常短。除此之外,如果测试数据规模太小,测试结果可能无法真实地反应算法的性能。比如,对于小规模的数据排序,插入排序可能反倒会比快速排序要快!

所以,我们需要一个不用具体的测试数据来测试,就可以粗略地估计算法的执行效率的方法。这就是我们今天要讲的时间、空间复杂度分析方法。

4.大O时间复杂度

算法的执行效率,粗略地讲,就是算法代码执行的时间。但是,如何在不运行代码的情况下,用“肉眼”得到一段代码的执行时间呢?

int cal(int n) {
   int sum = 0;
   int i = 1;
   for (; i <= n; ++i) {
     sum = sum + i;
   }
   return sum;
 }

从 CPU 的角度来看,这段代码的每一行都执行着类似的操作:读数据-运算-写数据。

尽管每行代码对应的 CPU 执行的个数、执行的时间都不一样,但是,我们这里只是粗略估计,所以可以假设每行代码执行的时间都一样,为 unit_time。

在这个假设的基础之上,这段代码的总执行时间是多少呢?

第 2、3 行代码分别需要 1 个 unit_time 的执行时间,第 4、5 行都运行了 n 遍,所以需要 2n*unit_time 的执行时间,所以这段代码总的执行时间就是 (2n+2)*unit_time。可以看出来,所有代码的执行时间 T(n) 与每行代码的执行次数成正比。T(n)=(2n+2)*unit_time。

尽管我们不知道 unit_time 的具体值,但是通过这两段代码执行时间的推导过程,我们可以得到一个非常重要的规律,那就是,所有代码的执行时间 T(n) 与每行代码的执行次数 n 成正比。

T1(n)=O(f(n))

T(n) 表示代码执行的时间;

n 表示数据规模的大小;

f(n) 表示每行代码执行的次数总和。

因为这是一个公式,所以用 f(n) 来表示。公式中的 O,表示代码的执行时间 T(n) 与 f(n) 表达式成正比。

T(n) = O(2n+2),这就是大 O 时间复杂度表示法。大 O 时间复杂度实际上并不具体表示代码真正的执行时间,而是表示代码执行时间随数据规模增长的变化趋势,所以,也叫作渐进时间复杂度(asymptotic time complexity),简称时间复杂度。

当 n 很大时,你可以把它想象成 10000、100000。而公式中的低阶、常量、系数三部分并不左右增长趋势,所以都可以忽略。我们只需要记录一个最大量级就可以

T(n)=O(2n²+2n+10000)–>O(n²)

为什么大O时间复杂度只关注代码中最复杂的循环体,这样真的准确么?万一我还有一个循环体仅次与他呢,同时另外一个代码和我比较时间复杂度时,正好和仅次与最复杂的循环体一样,这样测出来不就不准确了么?

因为n量级足够大,所以可以忽略低阶。

5.时间复杂度计算技巧

01.只关注执行次数最多的一段代码

02.加法法则:总复杂度等于量级最大的那段代码复杂度

03.乘法法则:嵌套代码的复杂度等于内外复杂度的乘积

6.常见的复杂度量级

6.1 数据量和复杂度的增长关系

常见的复杂度量级 数据量和复杂度的增长关系

我们做题的时候要想办法降低时间复杂度,这样当n的数量级上升的时候,时间复杂度的降低效果就很明显了。(比如:O(n^2)–>O(n)指数级下降)

6.2 多项式量级

多项式量级是什么?

多项量级就是说这个时间复杂度是由n作为底数的O(n) O(logn)

非多项量级就是n不是作为底数

log的详细解释?

a^x =N(a>0,且a≠1),那么数x叫做以a为底N的对数,记作x=logaN,“log”是拉丁文logarithm(对数)的缩写

log的底数是10时可以写成lg,底数是e时可以简写成ln,没有写底数的都是以10为底

6.3 非多项式量级

复杂度量级

对于上面罗列的复杂度量级,我们可以粗略地分为两类,多项式量级和非多项式量级。其中,非多项式量级只有两个:O(2^n) 和 O(n!)。

我们把时间复杂度为非多项式量级的算法问题叫作 NP(Non-Deterministic Polynomial,非确定多项式)问题。

当数据规模 n 越来越大时,非多项式量级算法的执行时间会急剧增加,求解问题的执行时间会无限增长。所以,非多项式时间复杂度的算法其实是非常低效的算法。

7.O(1)

只要算法中不存在循环语句、递归语句,即使有成千上万行的代码,其时间复杂度也是Ο(1)

 int i = 8;
 int j = 6;
 int sum = i + j;

8.O(logn),O(nlogn)

8.1 O(logn)

对数阶logn时间复杂度非常常见,同时也是最难分析的一种时间复杂度。关键在关注变量的增长情况。

 i=1;
 while (i <= n)  {
   i = i * 2;
 }

从代码中可以看出,变量 i 的值从 1 开始取,每循环一次就乘以 2。当大于 n 时,循环结束。还记得我们高中学过的等比数列吗?

实际上,变量 i 的取值就是一个等比数列。如果我把它一个一个列出来,就应该是这个样子的:

i的等比数列

所以,我们只要知道 x 值是多少,就知道这行代码执行的次数了。通过 2^x=n也就是log2n。所以,这段代码的时间复杂度就是 O(log2n)。

实际上,不管是以 2 为底、以 3 为底,还是以 10 为底,我们可以把所有对数阶的时间复杂度都记为 O(logn)。为什么呢?

我们知道,对数之间是可以互相转换的

log3n = log32 * log2n

其中 C=log32 是一个常量。

所以 O(log3n) = O(C * log2n)

基于我们前面的一个理论:在采用大 O 标记复杂度的时候,可以忽略系数,即 O(Cf(n)) = O(f(n))。所以,O(log2n) 就等于 O(log3n)。因此,在对数阶时间复杂度的表示方法里,我们忽略对数的“底”,统一表示为 O(logn)。

8.2 O(nlogn)

如果你理解了我前面讲的 O(logn),那 O(nlogn) 就很容易理解了。还记得我们刚讲的乘法法则吗?**如果一段代码的时间复杂度是 O(logn),我们循环执行 n 遍,时间复杂度就是 O(nlogn) 了。**而且,O(nlogn) 也是一种非常常见的算法时间复杂度。

比如,归并排序、快速排序的时间复杂度都是 O(nlogn)。

快排最好的情况下,枢轴对两边的划分都很均匀,用递归树来表示如下图:

quickSort(arr,l,p-1);
quickSort(arr,p+1,r);

快排

9.O(m+n),O(m*n)

int cal(int m, int n) {
  int sum_1 = 0;
  int i = 1;
  for (; i < m; ++i) {
    sum_1 = sum_1 + i;
  }

  int sum_2 = 0;
  int j = 1;
  for (; j < n; ++j) {
    sum_2 = sum_2 + j;
  }

  return sum_1 + sum_2;
}

从代码中可以看出,m 和 n 是表示两个数据规模。**我们无法事先评估 m 和 n 谁的量级大,所以我们在表示复杂度的时候,就不能简单地利用加法法则,省略掉其中一个。**所以,上面代码的时间复杂度就是 O(m+n)。

10.复杂度分析

10.1 对数时间复杂度&指数时间复杂度Demo

时间复杂度logn&k^n

10.2 常见的搜索&排序算法时间复杂度

常见的搜索&排序算法时间复杂度

二分查找,二叉树遍历,二维矩阵二分查找,归并排序

01.二分查找O(logn)

一分为二,只查一边

02.二叉树遍历O(n)

思考方式01.主定理推导:每次一分为二,每边相等的时间复杂度

思考方式02.每个节点只走一次

03.排好序的二维矩阵二分查找 O(n)

一维度数组二分查找logn

有序二维矩阵 O(n),正常二维矩阵查找On^2,进行二分查找被降了一维On

04.归并排序O(n log n)

所有的排序最优都是nlogn

搜索算法复杂度

10.3 常用数据结构增删改查操作时空间复杂度表

常用数据结构操作时空间复杂度表

10.4 计算递归的时间复杂度方法

递归计算时间复杂度,计算其语句执行次数,画出递归执行顺序的树形结构–递归状态树

斐波那契

1.首先了解总执行次数(根据执行顺序画出树形结构图–递归树/状态树)

2.每多展开一层,运行的节点数是上一层的二倍,每层节点是2^n指数级别递增,总执行次数也是指数级的且有很多重复节点

爬楼梯空间复杂度

2^6

11.空间复杂度

在多数场景中,一个好的算法往往更注重的是时间复杂度的比较,而空间复杂度只要在一个合理的范围内就可以。

void print(int n) {
  int i = 0;
  int[] a = new int[n];
  for (i; i <n; ++i) {
    a[i] = i * i;
  }

  for (i = n-1; i >= 0; --i) {
    print out a[i]
  }
}

第 2 行代码中,我们申请了一个空间存储变量 i,但是它是常量阶的,跟数据规模 n 没有关系,所以我们可以忽略。

第 3 行申请了一个大小为 n 的 int 类型数组,除此之外,剩下的代码都没有占用更多的空间,所以整段代码的空间复杂度就是 O(n)。

我们常见的空间复杂度就是 O(1)、O(n)、O(n^2),像 O(logn)、O(nlogn) 这样的对数阶复杂度平时都用不到。而且,空间复杂度分析比时间复杂度分析要简单很多。

11.1 递归的空间复杂度

爬楼梯空间复杂度

为什么fib的时间复杂度是O2^n,而空间复杂度是On?

递归的空间复杂度看递归的层数

n层,空间复杂度:On

如果递归中有数组,两者之间的最大值就是你的空间复杂度

12.时间空间复杂度分析案列:60爬楼梯

递归

public static int climbStairs(int num){
    if (num<2){
        return 1;
    }
    return climbStairs(num-1)+climbStairs(num-2);
}
爬楼梯空间复杂度

时间复杂度:2^n(每层都是上一层的2倍)

空间复杂度:On(递归的空间复杂度看递归的层数)

缓存数组

public static int climbStairs(int n) {
    int[] memo = new int[n + 1];
    memo[0]=1;
    memo[1]=1;
    return fib(memo,n);
}

private static int fib(int[] memo, int n) {
    if (memo[n]!=0)return memo[n];
    if (n>=2)memo[n]=fib(memo,n-1)+fib(memo,n-2);
    return memo[n];
}

时间复杂度:On(递归不在是2^n,直接从缓存中取值)

空间复杂度:On(引入了数组)

动态规划(数组)

public static int climbStairs(int n) {
    int[] arr=new int[n+1];
    arr[0]=1;
    arr[1]=1;
    for (int i=2;i<=n;i++){
        arr[i]=arr[i-1]+arr[i-2];
    }
    return arr[n];
}

时间复杂度:On

空间复杂度:On(引入了数组)

动态规划(无数组)

public static int climbStairs(int n) {
    int first=1,second=1;
    for (int i=2;i<=n;i++){
        int tmp=first+second;
        first=second;
        second=tmp;
    }
    return second;
}

时间复杂度:On

空间复杂度:O1

for (int i=2;i<=n;i++){
arr[i]=arr[i-1]+arr[i-2];
}
return arr[n];
}


时间复杂度:On

空间复杂度:On(引入了数组)

### 动态规划(无数组)

```java
public static int climbStairs(int n) {
    int first=1,second=1;
    for (int i=2;i<=n;i++){
        int tmp=first+second;
        first=second;
        second=tmp;
    }
    return second;
}

时间复杂度:On

空间复杂度:O1

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值