「算法」时间复杂度没搞明白?怎么刷题~

点赞再看,养成习惯,微信搜一搜【一角钱小助手】关注更多原创技术文章。
本文 GitHub org_hejianhui/JavaStudy 已收录,有我的系列文章。

20200918210442.jpg

前言

数据结构和算法本身解决的是 “快” 和 “省” 的问题,即如何让代码运行的更快,如何让代码更节省存储空间。所以我们在写程序的时候,一定要对自己编写的程序的时间和空间复杂度有所了解,而且是养成习惯的,写完了之后能够下意识地分析出你这段程序的时间和空间复杂度,以及能够用最简洁的时间和空间复杂度完成这段程序。基本上是一个顶尖职业选手的必备的素养。如果你的时间复杂度写砸了的话其实你带给公司的程序或者机器或者说资源的损耗,而如果你能够简化,对公司来说是节约成本,而且这些节约成本其实就是改动一些代码所带来的,可谓是对你来说影响力非常大。基于此我们来重新回顾时间、空间复杂度分析。

时间复杂度

Big O notation

七种最常见的时间复杂度

  • O ( 1 ) O(1) O(1) : Constant Complexity 常数复杂度
  • O ( l o g n ) O(log n) O(logn) : Logarithmic Complexity 对数复杂度
  • O ( n ) O(n) O(n) :Linear Complexity 线性时间复杂度
  • O ( n 2 ) O(n^2) O(n2):N square Complexity 平方
  • O ( n 3 ) O(n^3) O(n3) :N cubic Complexity 立方
  • O ( 2 n ) O(2^n) O(2n) :Exponential Growth 指数
  • O ( n ! ) O(n!) O(n!) :Factorial 阶乘

注意:只看最高复杂度的运算,不考虑前面的系数的,比如说O(1)的话并不代表它的复杂度是 1,也可以是 2 或者 3,4,那么所有这些在 Big O 的表示的话就是 O(1) 相当于这样它只要是常数次的,不管是一次、两次、三次或者是X次,它都是O(1)的。如果它是线性时间复杂度,比如说O(n),它可能是运算来n次,也可能是运算2n次,所以的话它前面的常数系数在这里是不用进行考虑的。

我们怎么来看这样一个时间复杂度?

最常用的方式,就是直接看这个函数或者是说这段代码,它根据n的不同情况它会运行多少次。

案例1:

第一段代码, n = 1000,println("Hey - your input is") 这段不管 n 等于多少,它的程序只执行一次。所以它的时间复杂度就是 O(1) 。

第二段代码,同理,我们不关心前面的常数系数,也就是说第二段代码的话虽然它会执行三次,但不管 n 是多少,它只执行三次,所以它的时间复杂度还是常数的。

image.png

案例2:

第一段代码:虽然它只有三行代码,主要的 print 语句虽然只有一句,但是这段代码就发送了变化,当 n = 1 的时候,它执行一次,当 n = 100 的时候,它不再是只执行一次了。所以它的时间复杂度其实和 n 是线性关系的,所以我们就记为 O(n) 。

第二段代码:同理,如果是一个嵌套的循环,你会发现如果 n 等于 100的话,print 会执行 100 * 100 = 10000 次,所以它的时间复杂度就是 O(n^2) 。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5K05n0Sj-1600434791195)(//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/539ca092ddb747fb84ee192da104f6c0~tplv-k3u1fbpfcp-zoom-1.image)]

这里你可以思考一下,假设这里两层循环不是嵌套的而是并列,它的时间复杂度是多少呢?

案例3:

第一段代码:就假设如果 n 等于4的话,i 的话只会执行两次,那么这个函数体执行的次数永远是它的 log2(n) 。所以它的时间复杂度就是 O(logn) 的。

第二段代码:Fibonacci 数列求第 n 项的话,它这里用了一种递归的形式,那么这就是牵扯到递归程序在执行的时候,怎么计算它的时间复杂度,它的答案是 k 的 n 次方,k 是一个常数,它是指数级的,所以简单的递归求 Fibonacci 数列的话它是非常慢的。指数级别的时间复杂度。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LM0rPpgA-1600434791197)(//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2affb61d699e4dce826ce691511f65b8~tplv-k3u1fbpfcp-zoom-1.image)]

时间复杂度曲线

从下图可以得出 n 如果比较小的情况下,也就是在5以内的话,不同的时间复杂度其实都差不多,但是如果当 n 开始扩大,你会发现指数级的它涨的是非常快的,也就是说当你在写程序的时候,如果你能够优化时间复杂度,比如说从 2 的 n 次方降到 n 平方的话你得到的收益是非常大的,从这个图你可以看到 n 越大的话,它的差别就是天壤之别的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qG2VvJ67-1600434791198)(//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/18f953711a6346609f4b647d5fb4e0df~tplv-k3u1fbpfcp-zoom-1.image)]

所以我们在写程序的时候,一定要对自己编写的程序的时间和空间复杂度有所了解,而且是养成习惯的,写完了之后能够下意识地分析出你这段程序的时间和空间复杂度,以及能够用最简洁的时间和空间复杂度完成这段程序。

案例4:

不同的程序,完成同样的目标可能会导致时间复杂度的不一样。

题目:计算:1 + 2 + 3 + ... + n 的求和
image.png
方法一:用程序暴力求解

就是从1循环到n累加,它的程序的化就可以看下面直接用一个 y 每次累加 i,这个程序因为是一层循环,同时 n 等于多少的话,它就循环多少次,所以它的时间复杂度是 O(n)。

方法二:数学的求和公式

用这个公式程序就只有 1 行,它的时间复杂度是 O(1)。

所以可以看程序的不同方法,最后得到结果虽然是一样,但是它的时间复杂度会很不一样。所以当你在写一个程序的时候,当你碰到一个面试题的时候。

  • 首先和面试官把这个题目的意思全部都确认无误;
  • 第二想所有可能的解决的办法,同时比较这些方法的时间和空间复杂度;
  • 接下来找出最优的解决方案,就是时间最快的,内存的话用得最少的更好。
  • 最后测试结果。

案例5:

更复杂的情况:递归
**
递归的情况下,怎么来分析时间复杂度?

递归的话关键就是要了解它的递归,总共执行来语句多少次,如果是循环的话就好理解,n 次循环就执行来 n 次,那么递归的话其实它层层嵌套,下去怎么办?其实很多时候我们要借助的就是把递归的执行顺序画出这么一个树型结构,我们称之为它的递归状态的递归树

题目:Fib: 0, 1, 1, 2, 3, 5, 8, 13, 21, ... 求 Fibonacci 数列的第 n 项。

  • F(n) = F(n - 1) + F(n - 2)
  • 面试 (直接用递归)
int fib(int n) {
    if (n < 2) return n;
    return fib(n - 1) + fib(n - 2); 
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LWts0iIv-1600434791199)(//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/298f256cea994b46b35c02000e458bec~tplv-k3u1fbpfcp-zoom-1.image)]
以 Fib(6) 为例,分别是 Fib(5) 和 Fib(4) 在这个地方,要计算 6 就会变成至少要计算 一个 f5 和 f4,至少是多出来两次运算,同理可得可以看到两个现象:

  • 第一个现象的话就是它每多展开一层的话,运行的节点树就是上面一层的两倍;
  • 第二个现象就是有重复的节点出现在执行的状态树里面。

正是因为有这么多大量冗余的计算的话,导致求第6个数的 Fibonacci 数的话变成了2 的 6次方这么一个繁复的时间复杂度,所以你可以看到它的时间复杂度展开来说,是相对来说比较恐怖的,或者事倍功半的。这段代码一定不要在面试中直接这么写,可以加一个缓存把这些中间结果能够缓存下来,或者是直接用一个循环来写完这整个 Fibonacci 数列求 n 项的。

Master Theorem

我们再来了解一个叫主定理的东西,这个定理为什么重要就是因为这个主定理其实它使用来解决所有递归的函数怎么来计算它的时间复杂度,这里的话主定理本身的话,数学上来证明相对比较复杂。那怎样化简为实际可用的办法,其实关键就是这四种,我们记住就可以了。

一般在各种递归的情形的话,是在面试和我们平常工程中会用上的:

  • 第一种叫做二分查找,一般发生在一个数列本身有序的时候,在有序的数列里找你要的目标数,所以它每次都一份为二,只查一遍这么下去的话,最后它的时间复杂度是 log(n) 的。
  • 第二种二叉树的遍历,它为O(n)的,因为通过主定理可以发现它每次要一分为二,但是每次一分为二之后,每一边它是相等的时间复杂度这么下去,最后它的一个递推公司变成了图中的第二行,最后通过主定理推出为 O(n),每个节点访问一次,且仅访问一次。
  • 第三种是在一个二维矩阵,排好序的二维矩阵中进行二分查找,同理通过主定理推出时间复杂度为 O(n)。
  • 第四种就是归并排序(merge sort),所有排序最优的办法就是 nlogn 的。所以归并排序也是 nlogn 的时间复杂度。

以上我们记住就好了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xWeXkLug-1600434791200)(//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/531fe73ad7274dd9a7379245e6145219~tplv-k3u1fbpfcp-zoom-1.image)]
详见:

思考

二叉树遍历 - 前序、中序、后序:时间复杂度是多少?
图的遍历:时间复杂度是多少?
搜索算法:DFS、BFS 时间复杂度是多少?
二分查找:时间复杂度是多少?

你可以把答案发在评论里。

参考:如何理解算法时间复杂度的表示法

空间复杂度

既然时间复杂度不是用来计算程序具体耗时的,那么我也应该明白,空间复杂度也不是用来计算程序实际占用的空间的。

空间复杂度是对一个算法在运行过程中临时占用存储空间大小的一个量度,同样反映的是一个趋势,我们用 S(n) 来定义。

空间复杂度比较常用的有:O(1)、O(n)、O(n²),我们下面来看看:

空间复杂度 O(1)

如果算法执行所需要的临时空间不随着某个变量n的大小而变化,即此算法空间复杂度为一个常量,可表示为 O(1)。

例如:

int i = 1;
int j = 2;
++i;
j++;
int m = i + j;

代码中的 i、j、m 所分配的空间都不随着处理数据量变化,因此它的空间复杂度 S(n) = O(1)

空间复杂度 O(n)

我们先看一个代码:

int[] m = new int[n]
for(i=1; i<=n; ++i)
{
   j = i;
   j++;
}

这段代码中,第一行new了一个数组出来,这个数据占用的大小为n,这段代码的2-6行,虽然有循环,但没有再分配新的空间,因此,这段代码的空间复杂度主要看第一行即可,即 S(n) = O(n)。

部分图片来源于网络,版权归原作者,侵删。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值