软件工程 算法 编码 大 O 符号基础变得非常简单

关于大O的简短对话

老实说,当我第一次接触编程时,出于恐惧,我推迟了很长一段时间学习 Big O,而这篇文章真的是为那些感觉相似的人准备的。我不知道它是否有助于我说大部分是常识并且数学并不比乘法难得多。但以防万一,我将添加一些视觉效果,以使其尽可能不痛苦。图形非常愚蠢,但老实说,这有助于我记住事情。

如果我要为您提供 Big O 表示法背后的想法的最简单版本,或者您的代码运行的时间复杂度,它会是这样的:

  • 基本操作是最有效的
  • 重复操作(循环)可能更担心效率
  • 嵌套循环是效率的最大担忧之一

老实说,这是最基本的。不会太可怕吧?在确定代码的性能时,还有一整类事情是你永远不必考虑的。您可以将它们视为“赠品”。他们包括:

  • 基础数学、加法和减法
  • 分配变量
  • if/else 条件
  • 声明一个函数
  • 调用函数(按面值)
  • 改变变量

如果我必须用可以想象的最简单的术语来总结大 O 的概念,那么循环几乎就是我们所担心的。我知道当我意识到这一点时,我感到不那么害怕了。

💡开始之前的注意事项:这意味着绝对基础,对于那些认为大 O 符号听起来像古老仪式的人。比我在这里包含的更多细微差别和更多描述。每当有机会获得清晰与更详细的信息时,我都会选择清晰。这篇文章是为那些需要基本的、可操作的信息的人准备了几个月的编程。这是我正在写的关于实时编码采访的更大系列的一部分,所以也请阅读一下🤓


为了解释其中的一些,让我们假设在我的空闲时间,我喜欢去寻宝游戏。我的朋友们为这些狩猎制定了规则,有些人比其他人更具挑战性。(侧边栏,有些人真的这样做!有一种爱好叫做寻宝,这几乎是现实生活中的寻宝。如果你好奇,可以在这里阅读。我的例子太傻了,不符合真正的寻宝,所以我会坚持使用“寻宝游戏”一词)。

第一部分:恒定时间或 O(1)

🏴‍☠️ Jamie 派我去寻宝,从 Chestnut 街的某个地址取回一支笔。我有一个地址,所以只需要去一所房子,很容易。

这是我们称之为“恒定时间”的一个例子。无论寻宝是为了笔还是其他东西,在这种情况下我只是敲了一扇门,它不会改变。这被认为是算法性能的一个非常积极的状态,实际上是最优化的。常量操作的一个示例是从 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)

🎁 我的朋友杰克让我完成了一项更棘手的任务。我附近正在建造一个新的开发项目,但他们没有添加可见的路牌。如果我走到房子那里,他们已经在前门的横梁上注明了地址。

我真的不想单独走到每栋房子,但在考虑之后我意识到杰克已经分享了随着你沿着街道前进,家庭地址数字会增加。

我不知道起始号码,因为这条路在相反的方向继续经过这条死胡同,所以我不确定最低的门牌号码是多少。但我知道我正在寻找#22。我意识到如果我从马路中间开始,我会看看地址是比我要找的地址低还是高,并且可以敲掉街区一半的房子,所以我不必搜索那些。

我看到中间的房子是#26,所以这个房子右边的所有房子的门牌号都太高了,不适合我要找的#22。所以我可以划掉我走过去的房子,以及右边的所有房子,因为它们的数字会更高。

现在在一个正常的世界里,这些房子都是规则的,我可以数数到正确的房子,但我看到一些空的泥地,不知道这些房子是否也会成为住宅,或者用于景观美化或公园。因为我不完全确定什么是地段,所以我不确定只是沿着街道数数到正确的房子是否可行。

所以我决定再次采用我的“从中间挑选”的方法,这种方法至少可以穿过街道剩余部分的一半,这取决于房子的编号是太高还是太低。出于这个原因,我在剩下的三间房子中再次选择了中间的房子。

怎么知道!那是正确的。如果你想想想我的“寻宝算法”,最终的结果是相当不错的。尽管街上有七间房子,但我最终只需要走到两间房子。就我必须做多少工作才能成功完成这项任务而言,我的方法意味着每次我走到一所房子的时候,剩下的要搜索的房子就会减少一半。

我们将每次切割结果集时的性能称为对数性能,并且在恒定时间(O(1))之后它是您可以获得的理想值。即使您的结果集为 7,您执行的操作也可能低至 2。如果您不太记得数学中的对数,那真的很好 - 您只需要知道它对于算法,因为您每次都在显着减少结果集。

在大 O 语言中,我们将对数性能写为 O(log n)。如果你担心记住大 o 描述符,在面试中简单地称其为“对数时间”是完全可以的。如果您只需要知道对数时间意味着每次通过都可能将结果减少一半,那么当您这样做时很容易识别出这一点。

作为边栏:我的寻宝游戏示例是二分搜索算法的示例。还不错吧?一遍又一遍地从中间挑选。如果您知道您的结果已经按某种顺序排列,那么这是一个很好的算法。

第 3 部分:线性时间,或 O(n)

🍬我的朋友 Jess 听到我在这些寻宝游戏中投入了多少工作,对我表示同情。对我来说幸运的是,它恰好是 10 月 31 日,她给我分配了不给糖就捣蛋的任务!我的寻宝活动是从梧桐街上的每一户人家中得到一块糖果。

在这个有点刻意凌乱的图形中,您可以看到与我上一个任务相比的工作量。

这就是我们在大 O 中所说的“线性时间”,也就是循环,也就是简单的乘法。我的任务是乘以房屋数量。在更大的街道上,这可能是 50 次行程。根据我在哪条街上以及它有多少房子,我的工作以线性方式增加。

对于大 O,线性时间在很多情况下是一种很好的算法性能。不如我们介绍的前两个好,但通常你会在面试中收到的代码挑战足够复杂,以至于有合理的机会需要循环。我们将其写为 O(n),其中“n”基本上是您要乘以的内容,或者您​​必须执行的循环数。

第 4 部分:二次和三次时间

👿 我的最后一个朋友 Andrew 真的很狡猾,让我开始了迄今为止最艰难的寻宝活动。他把我引到主要街道上,并说对于街道上的每一所房子,我需要看看我是否能在每所房子里找到一个在其他房子里重复的物品。

很快我就完成了 25 项单独的任务,因为我要为五个房子中的每一个做五件事。你可以很容易地看到我们是否把它放在我有 5 x 5 任务或 5^2 的数字上。这就是我们所说的二次时间

在编程中,这意味着一件事:嵌套循环!每个嵌套层都添加一个指数。如果我的朋友说我希望你在不止一条街道上这样做,那么数学就变成了邻里 x 房屋 x 物品,并且可以很容易地进入立方时间,或 O(n^3),通常以第三个嵌套形式呈现现实生活中的循环

我的示例仍然非常易于管理,但在现实生活中的编程中,我们正在寻找比这更大的数字。查看任务如何气球:

基数任务^2任务^ 3任务 ^ 4
100100001000000100000000
1000100000010000000001000000000000
100000100000000001000000000000000100000000000000000000
10000001000000000000100000000000000000010000000000000000000000000

这可能是我可以和你谈论的最重要的领域。它在我的工作和编程中出现最多。您可以快速了解为什么它会快速变得重要。

您会注意到,如果他们不注意嵌套循环,那么您的基数为 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) 的时间复杂度吗”。他们问的事实是我们可能的一个很好的暗示。但是对于一个非常新的程序员来说,解决这个问题的明显方法可能是这样的。(您也可以在此处自行运行代码)。

你可以很快看到,指数逻辑赶上了我们,我们在这个方法中有很多单独的任务调用。

回到我之前所说的关于尝试使事情成型,您可以利用恒定时间和线性时间,我将使用所有相同的测试用例,但以更好的 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 对象的键之一。如果是,我找到了一个匹配的对,其总和是目标数。我已经将第一个数字所在的索引保存为键值,所以我已经很方便了,然后可以返回我在第二个循环中所在的列表项的索引。您也可以在此处使用此代码。

我使用了相同的测试用例,你可以看到得到了相同的结果。但是使用我的嵌套循环方法,我们看到的最差性能是 > 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 表示法,但这些是我最常看到的那些,它们应该会让新程序员在代码挑战中走得更远。即使你对数学概念不感兴趣,记住循环是你需要担心的最重要的事情,我认为任何人都可以记住的简单建议,它确实有助于你牢记性能你有什么水平的编码专业知识。

此外,这里还有一些其他很棒的资源:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
专题十:算法分析与设计 1.常用的算法设计方法:   1.1 迭代法   1.2 穷举搜索法   1.3 递推法   1.4 递归法   1.5 贪婪法   1.6 分治法   1.7 动态规划法   1.8 回溯法 算法基础部分: 算法是对特定问题求解步骤的一种描述,算法是指令的有限序列,其中每一条指令表示一个或多个操作。 算法具有以下5个属性:   有穷性:一个算法必须总是在执行有穷步之后结束,且每一步都在有穷时间内完成。   确定性:算法中每一条指令必须有确切的含义。不存在二义性。只有一个入口和一个出口   可行性:一个算法是可行的就是算法描述的操作是可以通过已经实现的基本运算执行有限次来实现的。   输入:一个算法有零个或多个输入,这些输入取自于某个特定对象的集合。   输出:一个算法有一个或多个输出,这些输出同输入有着某些特定关系的量。 所以对应的算法设计的要求:   正确性:算法应满足具体问题的需求;   可读性:算法应该好读,以有利于读者对程序的理解;   健壮性:算法应具有容错处理,当输入为非法数据时,算法应对其作出反应,而不是产生莫名其妙的输出结果。   效率与存储量需求:效率指的是算法执行的时间;存储量需求指算法执行过程中所需要的最大存储空间。一般这两者与问题的规模有关。 1.1 迭代法: 迭代法是用于求方程或方程组近似根的一种常用的算法设计方法。设方程为f(x)=0,用某种数学方法导出等价的形式x=g(x),然后按以下步骤执行: (1)选一个方程的近似根,赋给变量x0; (2)将x0的值保存于变量x1,然后计算g(x1),并将结果存于变量x0; (3)当x0与x1的差的绝对值还小于指定的精度要求时,重复步骤(2)的计算。 若方程有根,并且用上述方法计算出来的近似根序列收敛,则按上述方法求得的x0就认为是方程的根。上述算法用C程序的形式表示为: 【算法】迭代法求方程的根 { x0=初始近似根; do { x1=x0; x0=g(x1); /*按特定的方程计算新的近似根*/ } while ( fabs(x0-x1)>Epsilon); printf(“方程的近似根是%f\n”,x0); } 迭代算法也常用于求方程组的根,令 X=(x0,x1,…,xn-1) 设方程组为: xi=gi(X) (I=0,1,…,n-1) 则求方程组根的迭代算法可描述如下: 【算法】迭代法求方程组的根 { for (i=0;i<n;i++) x[i]=初始近似根; do { for (i=0;i<n;i++) y[i]=x[i]; for (i=0;i<n;i++) x[i]=gi(X); for (delta=0.0,i=0;i<n;i++) if (fabs(y[i]-x[i])>delta) delta=fabs(y[i]-x[i]); } while (delta>Epsilon); for (i=0;i<n;i++) printf(“变量x[%d]的近似根是 %f”,I,x[i]); printf(“\n”); } 具体使用迭代法求根时应注意以下两种可能发生的情况: (1)如果方程无解,算法求出的近似根序列就不会收敛,迭代过程会变成死循环,因此在使用迭代算法前应先考察方程是否有解,并在程序中对迭代的次数给予限制; (2)方程虽然有解,但迭代公式选择不当,或迭代的初始近似根选择不合理,也会导致迭代失败。 1.2 穷举搜索法: 穷举搜索法是对可能是解的众多候选解按某种顺序进行逐一枚举和检验,并从中找出那些符合要求的候选解作为问题的解。 要解决的问题只有有限种可能,在没有更好算法时总可以用穷举搜索的办法解决,即逐个的检查所有可能的情况。可以想象,情况较多时这种方法极为费时。实际上并不需要机械的检查每一种情况,常常是可以提前判断出某些情况不可能取到最优解,从而可以提前舍弃这些情况。这样也是隐含的检查了所有可能的情况,既减少了搜索量,又保证了不漏掉最优解。 【问题】 将A、B、C、D、E、F这六个变量排成如图所示的三角形,这六个变量分别取[1,6]上的整数,且均不相同。求使三角形三条边上的变量之和相等的全部解。如图就是一个解。 程序引入变量a、b、c、d、e、f,并让它们分别顺序取1至6的整数,在它们互不相同的条件下,测试由它们排成的如图所示的三角形三条边上的变量之和是否相等,如相等即为一种满足要求的排列,把它们输出。当这些变量取尽所有的组合后,程序就可得到全部可能的解。细节见下面的程序。 # include <stdio.h> void main() { int a,b,c,d,e,f; for (a=1;a<=6;a++) //a,b,c,d,e依次取不同的值 for (b=1;b<=6;b++) { if (b==a) continue; for (c=1;c<=6;c++) { if (c==a)||(c==b) continue; for (d=1;d<=6;d++) { if (d==a)||(d==b)||(d==c) continue; for (e=1;e<=6;e++) { if (e==a)||(e==b)||(e==c)||(e==d) continue; f=21-(a+b+c+d+e);//最后一个用减法算 if ((a+b+c==c+d+e))&&(a+b+c==e+f+a)) { printf(“%6d,a); printf(“%4d%4d”,b,f); printf(“%2d%4d%4d”,c,d,e); scanf(“%c”); } } } } } }

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值