关于大O的简短对话
老实说,当我第一次接触编程时,出于恐惧,我推迟了很长一段时间学习 Big O,而这篇文章真的是为那些感觉相似的人准备的。我不知道它是否有助于我说大部分是常识并且数学并不比乘法难得多。但以防万一,我将添加一些视觉效果,以使其尽可能不痛苦。图形非常愚蠢,但老实说,这有助于我记住事情。
如果我要为您提供 Big O 表示法背后的想法的最简单版本,或者您的代码运行的时间复杂度,它会是这样的:
- 基本操作是最有效的
- 重复操作(循环)可能更担心效率
- 嵌套循环是效率的最大担忧之一
老实说,这是最基本的。不会太可怕吧?在确定代码的性能时,还有一整类事情是你永远不必考虑的。您可以将它们视为“赠品”。他们包括:
- 基础数学、加法和减法
- 分配变量
- if/else 条件
- 声明一个函数
- 调用函数(按面值)
- 改变变量
如果我必须用可以想象的最简单的术语来总结大 O 的概念,那么循环几乎就是我们所担心的。我知道当我意识到这一点时,我感到不那么害怕了。
💡开始之前的注意事项:这意味着绝对基础,对于那些认为大 O 符号听起来像古老仪式的人。比我在这里包含的更多细微差别和更多描述。每当有机会获得清晰与更详细的信息时,我都会选择清晰。这篇文章是为那些需要基本的、可操作的信息的人准备了几个月的编程。这是我正在写的关于实时编码采访的更大系列的一部分,所以也请阅读一下🤓
为了解释其中的一些,让我们假设在我的空闲时间,我喜欢去寻宝游戏。我的朋友们为这些狩猎制定了规则,有些人比其他人更具挑战性。(侧边栏,有些人真的这样做!有一种爱好叫做寻宝,这几乎是现实生活中的寻宝。如果你好奇,可以在这里阅读。我的例子太傻了,不符合真正的寻宝,所以我会坚持使用“寻宝游戏”一词)。
第一部分:恒定时间或 O(1)
🏴☠️ Jamie 派我去寻宝,从 Chestnut 街的某个地址取回一支笔。我有一个地址,所以只需要去一所房子,很容易。
![](https://img-blog.csdnimg.cn/img_convert/e65a3438ac51d40b1ebcac594a6081bf.jpeg)
这是我们称之为“恒定时间”的一个例子。无论寻宝是为了笔还是其他东西,在这种情况下我只是敲了一扇门,它不会改变。这被认为是算法性能的一个非常积极的状态,实际上是最优化的。常量操作的一个示例是从 JavaScript 对象中检索值。
<span style="color:#171717"><span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code><span style="color:var(--syntax-declaration-color)">const</span> <span style="color:var(--syntax-name-color)">person</span> <span style="color:var(--syntax-error-color)">=</span> <span style="color:var(--syntax-text-color)">{</span>
<span style="color:var(--syntax-name-color)">name</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-string-color)">"</span><span style="color:var(--syntax-string-color)">Ann</span><span style="color:var(--syntax-string-color)">"</span><span style="color:var(--syntax-text-color)">,</span>
<span style="color:var(--syntax-name-color)">hobbies</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-text-color)">[</span><span style="color:var(--syntax-string-color)">"</span><span style="color:var(--syntax-string-color)">drawing</span><span style="color:var(--syntax-string-color)">"</span><span style="color:var(--syntax-text-color)">,</span> <span style="color:var(--syntax-string-color)">"</span><span style="color:var(--syntax-string-color)">programming</span><span style="color:var(--syntax-string-color)">"</span><span style="color:var(--syntax-text-color)">],</span>
<span style="color:var(--syntax-name-color)">pet</span><span style="color:var(--syntax-text-color)">:</span> <span style="color:var(--syntax-string-color)">"</span><span style="color:var(--syntax-string-color)">dog</span><span style="color:var(--syntax-string-color)">"</span>
<span style="color:var(--syntax-text-color)">}</span>
<span style="color:var(--syntax-name-color)">console</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">log</span><span style="color:var(--syntax-text-color)">(</span><span style="color:var(--syntax-name-color)">person</span><span style="color:var(--syntax-text-color)">.</span><span style="color:var(--syntax-name-color)">name</span><span style="color:var(--syntax-text-color)">)</span>
</code></span></span></span>
因为 JavaScript 对象是散列的一个例子,所以在对象中查找一些值是非常高效的。这是一个完美的模拟,拥有一个准确的地址,无需进一步搜索就可以直接找到它。无论里面有什么,检索项目的步骤都被认为是在恒定时间内运行的。
💡出于这个原因,对象/哈希数据结构通常是你在代码挑战中的朋友。
有时将数组转换为对象格式对于在这些类型的挑战中获得良好的性能非常有帮助。我们用大 O 表示法将此常数时间称为 O(1)。
第 2 部分:对数时间,或 O(log N)
🎁 我的朋友杰克让我完成了一项更棘手的任务。我附近正在建造一个新的开发项目,但他们没有添加可见的路牌。如果我走到房子那里,他们已经在前门的横梁上注明了地址。
我真的不想单独走到每栋房子,但在考虑之后我意识到杰克已经分享了随着你沿着街道前进,家庭地址数字会增加。
![](https://img-blog.csdnimg.cn/img_convert/dc8ab4a89da9deea64941dea10949964.png)
我不知道起始号码,因为这条路在相反的方向继续经过这条死胡同,所以我不确定最低的门牌号码是多少。但我知道我正在寻找#22。我意识到如果我从马路中间开始,我会看看地址是比我要找的地址低还是高,并且可以敲掉街区一半的房子,所以我不必搜索那些。
![](https://img-blog.csdnimg.cn/img_convert/ab79f2db74c99e5cca9f1667ffffcaf4.jpeg)
我看到中间的房子是#26,所以这个房子右边的所有房子的门牌号都太高了,不适合我要找的#22。所以我可以划掉我走过去的房子,以及右边的所有房子,因为它们的数字会更高。
现在在一个正常的世界里,这些房子都是规则的,我可以数数到正确的房子,但我看到一些空的泥地,不知道这些房子是否也会成为住宅,或者用于景观美化或公园。因为我不完全确定什么是地段,所以我不确定只是沿着街道数数到正确的房子是否可行。
所以我决定再次采用我的“从中间挑选”的方法,这种方法至少可以穿过街道剩余部分的一半,这取决于房子的编号是太高还是太低。出于这个原因,我在剩下的三间房子中再次选择了中间的房子。
![](https://img-blog.csdnimg.cn/img_convert/5e6c4426bfd5d13f8ebdd3ff8c41751a.jpeg)
怎么知道!那是正确的。如果你想想想我的“寻宝算法”,最终的结果是相当不错的。尽管街上有七间房子,但我最终只需要走到两间房子。就我必须做多少工作才能成功完成这项任务而言,我的方法意味着每次我走到一所房子的时候,剩下的要搜索的房子就会减少一半。
我们将每次切割结果集时的性能称为对数性能,并且在恒定时间(O(1))之后它是您可以获得的理想值。即使您的结果集为 7,您执行的操作也可能低至 2。如果您不太记得数学中的对数,那真的很好 - 您只需要知道它对于算法,因为您每次都在显着减少结果集。
在大 O 语言中,我们将对数性能写为 O(log n)。如果你担心记住大 o 描述符,在面试中简单地称其为“对数时间”是完全可以的。如果您只需要知道对数时间意味着每次通过都可能将结果减少一半,那么当您这样做时很容易识别出这一点。
作为边栏:我的寻宝游戏示例是二分搜索算法的示例。还不错吧?一遍又一遍地从中间挑选。如果您知道您的结果已经按某种顺序排列,那么这是一个很好的算法。
第 3 部分:线性时间,或 O(n)
🍬我的朋友 Jess 听到我在这些寻宝游戏中投入了多少工作,对我表示同情。对我来说幸运的是,它恰好是 10 月 31 日,她给我分配了不给糖就捣蛋的任务!我的寻宝活动是从梧桐街上的每一户人家中得到一块糖果。
在这个有点刻意凌乱的图形中,您可以看到与我上一个任务相比的工作量。
![](https://img-blog.csdnimg.cn/img_convert/eca42bdc845fcfcd93e5c66f5714d44d.jpeg)
这就是我们在大 O 中所说的“线性时间”,也就是循环,也就是简单的乘法。我的任务是乘以房屋数量。在更大的街道上,这可能是 50 次行程。根据我在哪条街上以及它有多少房子,我的工作以线性方式增加。
对于大 O,线性时间在很多情况下是一种很好的算法性能。不如我们介绍的前两个好,但通常你会在面试中收到的代码挑战足够复杂,以至于有合理的机会需要循环。我们将其写为 O(n),其中“n”基本上是您要乘以的内容,或者您必须执行的循环数。
第 4 部分:二次和三次时间
👿 我的最后一个朋友 Andrew 真的很狡猾,让我开始了迄今为止最艰难的寻宝活动。他把我引到主要街道上,并说对于街道上的每一所房子,我需要看看我是否能在每所房子里找到一个在其他房子里重复的物品。
很快我就完成了 25 项单独的任务,因为我要为五个房子中的每一个做五件事。你可以很容易地看到我们是否把它放在我有 5 x 5 任务或 5^2 的数字上。这就是我们所说的二次时间。
在编程中,这意味着一件事:嵌套循环!每个嵌套层都添加一个指数。如果我的朋友说我希望你在不止一条街道上这样做,那么数学就变成了邻里 x 房屋 x 物品,并且可以很容易地进入立方时间,或 O(n^3),通常以第三个嵌套形式呈现现实生活中的循环。
我的示例仍然非常易于管理,但在现实生活中的编程中,我们正在寻找比这更大的数字。查看任务如何气球:
基数 | 任务^2 | 任务^ 3 | 任务 ^ 4 |
---|---|---|---|
100 | 10000 | 1000000 | 100000000 |
1000 | 1000000 | 1000000000 | 1000000000000 |
100000 | 10000000000 | 1000000000000000 | 100000000000000000000 |
1000000 | 1000000000000 | 1000000000000000000 | 10000000000000000000000000 |
这可能是我可以和你谈论的最重要的领域。它在我的工作和编程中出现最多。您可以快速了解为什么它会快速变得重要。
您会注意到,如果他们不注意嵌套循环,那么您的基数为 100 的第一行可以快速进行比最后一行更多的操作,起始数为一百万。拥有 100 万条数据集的人可以让程序做的工作比拥有 100 条记录的数据集的人少,这一切都取决于他们对嵌套循环的谨慎程度。
在代码挑战中为我提出的大多数更好的点是我是否可以从循环中获得一些东西到恒定时间,然后我是否可以从嵌套循环中获得一些东西到单个循环中。(所以从三次时间 O(n^3) 到二次时间 O(n^2))。*
我们还有一个更通用的词来表示指数将是 n 次方的情况: *指数时间 (O(2^n))。
第 5 部分:这可能是什么样子
假设我们遇到了以下代码挑战。
<span style="color:#171717"><span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code>Given an array of unique integers and a target number, determine if any two
numbers in the array when added together would equal the target. If that
condition is met, return the array position of the two numbers. You cannot
reuse a number at a single index position (So array [4,2] could make
target number 6, but array [3] could not since it would require reusing the
array value at position array[0]).
Example: given array [1, 2, 3, 4, 5, 6] and target number 10, you would return
an answer [3, 5] since at array[3] you have number 4, at array[5] you have
number 6, and added together they make 10.
Example: given array [1,2,4] and target number 50, you would return false
because the condition can not be met.
</code></span></span></span>
这是对 Leetcode 问题的解释,它在下面提出后续问题,“你能得到低于 O(n^2) 的时间复杂度吗”。他们问的事实是我们可能的一个很好的暗示。但是对于一个非常新的程序员来说,解决这个问题的明显方法可能是这样的。(您也可以在此处自行运行代码)。
![](https://img-blog.csdnimg.cn/img_convert/8dd8ef6ef7f1bd8c3b7a7a4daff8dd42.jpeg)
你可以很快看到,指数逻辑赶上了我们,我们在这个方法中有很多单独的任务调用。
回到我之前所说的关于尝试使事情成型,您可以利用恒定时间和线性时间,我将使用所有相同的测试用例,但以更好的 Big O 方式重写此函数。(顺便说一句,这是说明性的,也许不是我将如何解决这个问题,所以不要在评论中找我哈哈)。
在这类问题中,哈希真的是你的朋友。因此,当我遍历我的列表时,我将创建一个 JavaScript 对象,其中每个键是列表项需要添加以达到目标编号的内容,值将是我找到该项目的索引。
<span style="color:#171717"><span style="color:var(--syntax-text-color)"><span style="color:var(--syntax-text-color)"><code>Example: given target number 8 and list [1,2,3,4,9,6] my JS object would
look like this:
{
// first array item is 1, we'd need to find a 7 to make 8, its at index 0
7: 0,
// second array item is 2, we'd need to find a 6 to make 8, its at index 1
// so on and so forth
6: 1,
5: 2,
4: 3,
6: 5
}
</code></span></span></span>
所以有了这个对象,这意味着我不需要做一个嵌套循环。我可以做另一个常规循环(线性时间)并检查当前列表项是否是此 Javascript 对象的键之一。如果是,我找到了一个匹配的对,其总和是目标数。我已经将第一个数字所在的索引保存为键值,所以我已经很方便了,然后可以返回我在第二个循环中所在的列表项的索引。您也可以在此处使用此代码。
![](https://img-blog.csdnimg.cn/img_convert/acbad86a44e8daaa7e75d4f5e9a3c00f.jpeg)
我使用了相同的测试用例,你可以看到得到了相同的结果。但是使用我的嵌套循环方法,我们看到的最差性能是 > 200 次操作,并且通过不嵌套我的循环,它只有 30 次!这真是太神奇了。一旦你习惯了这种方式,提取嵌套循环行为并找到另一种方式并不难。
可视化我们学到的东西
了解这些性能的一个好方法是查看性能图表,我从这个网站上抓取了这个。
另一个有趣的笔记
看到上面的问题,你可能想知道,因为我们循环了两次,大 O 符号是 O(2(n)) 吗?如果一个循环是 O(n),我们已经这样做了两次:一次是构建 JavaScript 对象,第二次是再次处理列表,看看我们是否有两个数字将与目标数字相加。
如果你还记得我说过我们关心的主要是循环,那么在这里添加一个星号表示我们关心问题中存在的最大规模的操作。这意味着什么?
- 如果您的程序中有一个性能为 O(n) 的循环,然后在代码中您有一个性能为 O(n^2) 的嵌套循环,我们将大 O 简称为 O(n^2) - 指数为程序中最高数量级的行为,也就是最大的担心。所以我们放弃较小的
- 如果您有一个运行在 O(n^2) 的嵌套循环,然后在运行 O(n^3) 的三重嵌套循环上运行,我们会将其简化为立方时间。三重嵌套循环是代码中发生的最高数量级的事情,因此我们简化为 O(n^3)。
如果您考虑一下,这样做更公平。几乎所有你写的东西都会有一些 O(1) 的操作,但是说一个程序的整体性能是 O(1) 几乎是不准确的。如果它有一个循环,则该语句将不准确。
结论
我们可以涵盖一些其他类型的 Big O 表示法,但这些是我最常看到的那些,它们应该会让新程序员在代码挑战中走得更远。即使你对数学概念不感兴趣,记住循环是你需要担心的最重要的事情,我认为任何人都可以记住的简单建议,它确实有助于你牢记性能你有什么水平的编码专业知识。
此外,这里还有一些其他很棒的资源: