设为首页|收藏本站|
开启左侧

[职场] 如何解答面试中的算法复杂度问题?

[复制链接]
35855 0
啊啦啦啦 发表于 2021-7-9 07:55:41 | 只看该作者 打印 上一主题 下一主题
 
如何解答面试中的算法复杂度问题? 第1张图片

面试的过程中,不论什么算法题目,在我们解答完毕以后,通常会被问到一个耳熟能详的问题:你这个代码的时间复杂度是多少?如果说算法是一个优化的世界,那么清楚地知道我们算法的效率就是其中至关重要的一部分。本篇文章将向大家介绍一下算法复杂度,以及如何计算算法复杂度。
在平时的工作和学习中,「算法复杂度」这个词我们听到过很多次了。「复杂」这个词听起来就很复杂,很多同学可能并不清楚如何计算一个算法的复杂度;或者能够计算一些基于循环的简单复杂度,但对于渐进复杂度或者均摊复杂度就算不清了。关于时间复杂度的计算,也有一套非常繁杂的公式和理论,例如什么 O、Ω、θ 之类的。这里我们尽量避免这些东西,帮助大家最直观地来理解什么是时间复杂度。
小知识:一般的,算法复杂度分为两个维度,一个是时间复杂度,另一个是空间复杂度。由于现在内存和硬盘也比早期的时候便宜很多了,所以限制空间的题目一般比较少,这里也主要来说时间复杂度。在后续的课程中,如果没特别说明的情况下,复杂度也一律指时间复杂度。

如何解答面试中的算法复杂度问题? 第2张图片

语句的执行次数

讨论算法复杂度,不妨从某个语句的执行次数开始。
例一
先来看一个例子,这里有一个函数,里面执行的操作是 a = 1,那么可以算一下这段代码的计算次数,我们知道这里一共只执行了一次计算。
Python3
def func():    a = 1例二
换一个复杂一点的情况,下面这个例子里我们加了一个循环,所以显然这里进行了 10 次计算。
Python3
def func():    for i in range(10):        a = 1再复杂一点,我们再加一个循环,这里一共执行了 10 × 10 = 100 次计算。
Python3
def func():    for i in range(10):        for j in range(10):            a = 1再把上述例子改难一点,把第二重循环的起点变成 i。
Python3
def func():    for i in range(10):        for j in range(i, 10):            a = 1

单选题:上述代码中 a = 1 执行了多少次
A. 10
B. 100
C. 55
D. 50

如何解答面试中的算法复杂度问题? 第3张图片

时间复杂度

我们在前一节最后留下了这样的代码:
Python3
def func():    for i in range(10):        for j in range(i, 10):            a = 1在这里 10 是一个已经给定的值,很多情况下这个数是被一个变量代替的,比如 n。那么如果把 10 改成 n 的话:
Python3
def func():    for i in range(n):        for j in range(i, n):            a = 1这时我们再计算执行次数的话,就变成了:

如何解答面试中的算法复杂度问题? 第4张图片

我们可以说他的时间复杂度为

如何解答面试中的算法复杂度问题? 第5张图片

。在这里,我们用他的计算次数来代表他的时间复杂度,计算次数越多,算法执行时间越长。
在时间复杂度的计算中,一般只保留最高次的项,并且不保留系数,那么这样简化以后,我们说这个代码的时间复杂度为 O(n^2)。
算法常数
细心的同学可能会思考一个问题,上面代码中,循环最里层只执行了一个 a = 1 的操作。如果再执行一个 a = 2 的操作呢?
Python3
def func():    for i in range(n):        for j in range(i, n):            a = 1            a = 2  重新计算代码的计算次数,可以看到我们的计算次数确实增加了一倍。

如何解答面试中的算法复杂度问题? 第6张图片

但是按照刚才的说法,现在算法时间复杂度仍然是 O(n^2)。
最高次项系数和低次项
为什么最高次项系数和低次项在计算算法复杂度的时候可以省略呢?
首先来说系数,不妨假设两个算法时间复杂度都是 O(n^2),但是就算都是 O(n^2),跑了两个 n^2 还是跑 10 个 n^2 还是有区别的,这两者绝对速度显然不一样。不过换一个角度,如果对比一个 O(n^2) 和一个 O(n^3) 的算法来说,就算 O(n^2) 的算法系数是 10,只要 n 足够大,也会远好于三次方的算法,这时系数带来的性能差异就可以忽略不计了。
而对于非最高次项来说,以 O(n^2 + n) 举例,在 n 很大的情况下,比如 n = 10000,那么左边的 n^2 计算结果是 10^8,而右边的 n 只有 10^4,对于整体计算次数来说是可以忽略不计的。所以在计算算法复杂度的时候,系数,和非最高次项是可以省略的
算法常数
系数也好,非最高次项也好,都可以称作当前算法的常数,即通俗来说影响算法性能,但是不会影响算法的复杂度量级。实际上还有一些其他的算法常数,例如我们这里假定了所有的语句计算时间都是单位时间,但是实际上是有差别的,比如取模操作就比加法操作慢,这多出来的时间也可以认为是常数时间。
时间复杂度 vs 具体执行时间
在力扣刷题的时候,在 AC 之后会提示当前代码时间打败了百分之多少的人。有些同学看到打败了 99% 的人,就认为自己的时间复杂度已经是最优了,实际上这是一个非常常见的误区:时间复杂度不等于具体执行时间
虽然一般的,我们说复杂度高的程序运行时间会更长,但是这也和具体的数据规模挂钩。力扣上很多题目,数据量给的非常小,这也导致了复杂度高但是常数小的代码,反而比复杂度低但是常数大的代码快。
举个例子,比如一道题目,一个人写了一个 O(10n^2) 的算法,另一个人写了一个 O(n^3) 的算法,按之前的说法显然是前者更优。但是我们假设 n = 2,前者的计算次数是 40,而后者的计算次数仅有 8,复杂度低的代码反而计算次数更多。另外由于数据小,程序本身也不会运行多长时间,其他的包括网络,CPU 调度时间等等的时间都会影响到系统的时间判定,所以力扣给出的运行时间 可以参考,但是 并不具有绝对的指导意义,大家还是要重点关注自己的算法复杂度。

如何解答面试中的算法复杂度问题? 第7张图片

常见时间复杂度

在本篇文章的最后,我们来回顾一些常见的时间复杂度。
常数时间复杂度
第一个例子,我们刚才说过,他的时间复杂度为 O(1),也叫常数复杂度。
Python3
#常数复杂度def func(n):    a = 1线性时间复杂度
第二个例子,我们有一重循环,所以复杂度为 O(n),也叫作线性复杂度。
Python3
#线性复杂度def func(n):    for i in range(n):        a = 1多项式时间复杂度
第三个例子,我们有两重循环,时间复杂度为 O(n^2),我们也可以继续累多层循环,这些都是多项式复杂度。
Python3
#多项式复杂度 1def func(n):    for i in range(n):        for j in range(n):            a = 1

Python3
#多项式复杂度 2def func(n):    for i in range(n):        for j in range(i, n):            a = 1指数时间复杂度
看一个复杂些的例子,这是一个递归函数,计算 n 需要先计算两遍 n-1,那等于说要计算 4 遍 n-2,8 遍 n-3...一直最后到 1。所以我们说这个函数的时间复杂度为 O(2^n),这是一个指数复杂度的算法。大多数搜索算法的复杂度都是指数级别。
Python3
#指数复杂度def func(n):    if n <= 1: return n    return func(n-1) + func(n-1)对数时间复杂度
和指数复杂度对应的是对数复杂度,下面对于 n 每次我们询问 n/2,那么最终的函数调用次数大约为以 2 为底数的 log2(n)。
Python3
def func(n):    if n <= 1: return n    return func(n//2)渐进时间复杂度
接下来我们分析两个稍微复杂一点的例子。首先是下面这个函数:
Python3
def func(n):    if n <= 1: return    mid = n // 2    for i in range(mid+1, n):        a = 1    func(mid)

该函数的时间复杂度是?
A. O(n)
B. O(n^2)
C. O(2^n)
D. O(log(n))
均摊时间复杂度
最后我们来看看均摊复杂度,这也是复杂度分析中较难的一种类型。
假设我们有一个全局数组 a,然后有一个函数 func,如果输入是 0,我们就往 a 里放一个数,如果输入不是 0,我们就把 a 中的数全部 pop 出来。这样经过 n 次操作以后,算法时间复杂度是多少?
Python3
a = []def func(x):    if x == 0:        a.push(x)    else:        while a.size() > 0:            a.pop()for i in range(n):    func(某个 x)我们首先来分析单次的时间复杂度:

  • 显然对于 x = 0 的时候,我们只是放一个数到数组,单次操作时间复杂度为常数。如果有 n 次的话,时间复杂度是 O(n)。
  • 麻烦的是 x 不等于 0 的情况,极端情况下,我们先放了 n - 1 个数,然后最后一次把这些数都 pop 出来,这样单次操作的时间复杂度就是 O(n) 了。假设有 n 次操作总体时间复杂度这么算是 O(n^2)。
但是第二种情况是不符合直觉的,因为显然我们不会每一次都 pop n 个数出来,第一次 pop 完了以后后面的 pop 都几乎没有代价了。
不妨换一个思路,第二个操作中,每次都会从数组中拿数出来,但是不管他每次操作具体拿出来多少,总共能拿出来的数的数量也就是我们插入到数组中的数的数量。而插入的数的总数最多为 n。因此所有的 pop 操作 加起来 的时间复杂度为 O(n)。因此,步骤一和步骤二的时间复杂度都是 O(n),总体时间复杂度为 O(n)。
小技巧:均摊复杂度分析的难点在于每一步操作的复杂度都不一致,但是这些操作的复杂度总和往往是固定的,这时直接考虑总体执行次数就能想明白问题了。


总结
我们通过一些简单的算法例子了解了算法复杂度的一些类别,但是到了真实的题目中分析起来仍然可能会有一些不一样的情况。
本文内容选自:LeetBook《高频算法实战》

如何解答面试中的算法复杂度问题? 第8张图片

作者简介:小函,北京大学硕士。ACM/ICPC 亚洲区预选赛金牌,拥有大型算法竞赛命题经验。校招曾获得小米,阿里,头条,微软,百度,搜狐,快手等多家公司 offer,具有丰富的面试与被面试经验。在机器学习领域,曾获得多个国内外机器学习竞赛金牌和冠军。
内容简介:这门课程将帮助大家梳理面试中必须掌握的基础知识点,学习常用解题技巧,真正动手去 AC 每一个知识点。课程中还会讨论很多经典的算法题目,包括力扣题和小函老师亲自设计的题目,并对这些问题进行扩展发散,追踪知识点的原理,帮助大家知其然且知其所以然,而不只是记住解法。


本文作者:小函
声明:本文归 “力扣” 版权所有,未经授权不得以任何形式储存、转载或商用。


上一篇:两同事在厕所闲聊后做了一个决定:&amp;#34;互相捐肾&amp;#34;
下一篇:57岁甄子丹晒照秀恩爱,和小17岁娇妻深情对视,一家人长腿太抢镜
@



1.西兔生活网 CTLIVES 内容全部来自网络;
2.版权归原网站或原作者所有;
3.内容与本站立场无关;
4.若涉及侵权或有疑义,请点击“举报”按钮,其他联系方式或无法及时处理。
 
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

排行榜
活跃网友
返回顶部快速回复上一主题下一主题返回列表APP下载手机访问
Copyright © 2016-2028 CTLIVES.COM All Rights Reserved.  西兔生活网  小黑屋| GMT+8, 2024-5-3 09:35