什么是“大O”符号的简单英语解释?

我更喜欢尽可能少的正式定义和简单的数学。

793672 次浏览

大O只是用一种常见的方式“表达”自己,“运行我的代码需要多少时间/空间?”。

你可能经常看到O(n)、O(n2)、O(nlogns)等等,这些都只是展示;算法如何改变?

O(n)意味着大O是n,现在你可能会想,“什么是n!?”好吧,“n”是元素的数量。想象你想在数组中搜索一个项目。你必须查看每个元素,并作为“你是正确的元素/项目吗?”在最坏的情况下,项目在最后一个索引处,这意味着它花费的时间和列表中的项目一样多,所以一般来说,我们说“哦,嘿,n是一个公平的给定数量的值!”。

那么你可能会理解“n2”的意思,但更具体地说,你有一个简单的,最简单的排序算法;冒泡排序。该算法需要查看每个项目的整个列表。

我的名单

  1. 1
  2. 6
  3. 3

这里的流程将是:

  • 比较1和6,哪个最大?好的,6在正确的位置,向前移动!
  • 比较6和3,哦,3少了!让我们移动它,好吧,列表改变了,我们现在需要从头开始!

这是O n2,因为您需要查看列表中的所有项目,其中有“n”个项目。对于每个项目,您再次查看所有项目,进行比较,这也是“n”,因此对于每个项目,您看“n”次意味着n*n=n2

我希望这和你想要的一样简单。

但请记住,Big O只是以时间和空间的方式展现自己的一种方式。

编辑:快速说明,这几乎肯定会混淆大O符号(这是一个上限)和Theta符号(这是一个上限和下限)。根据我的经验,这实际上是非学术环境中讨论的典型。对造成的任何混乱表示歉意。

一句话:随着你的工作规模的增加,完成它需要多长时间?

显然,这只是使用“大小”作为输入,“花费的时间”作为输出——如果你想谈论内存使用等,同样的想法也适用。

这里有一个例子,我们有N件T恤要晾干。我们将假设让它们处于晾干位置非常快(即人类的互动可以忽略不计)。当然,现实生活中不是这样…

  • 在户外使用洗涤线:假设你有一个无限大的后院,洗涤在O(1)的时间内干燥。不管你有多少,它都会得到同样的阳光和新鲜空气,所以大小不会影响干燥时间。

  • 使用滚筒式烘干机:你每次放10件衬衫,然后一小时后完成。(忽略这里的实际数字-它们无关紧要。)所以烘干50件衬衫需要的时间大约是烘干10件衬衫的5倍。

  • 把所有东西都放在通风柜里:如果我们把所有东西都放在一堆里,让一般的温暖来做,中间的衬衫需要很长时间才能变干。我不想猜测细节,但我怀疑这至少是O(N^2)——随着洗涤负荷的增加,干燥时间会增加得更快。

“大O”表示法的一个重要方面是它不要表示对于给定的大小,哪种算法会更快。以哈希表(字符串键,整数值)与成对数组(字符串,整数)为例。根据字符串,在哈希表中查找键或数组中的元素更快吗?(即对于数组,“找到字符串部分与给定键匹配的第一个元素。”)哈希表通常摊销(~=“平均”)O(1)-一旦设置好,在100个条目表中查找条目的时间应该与在1,000,000个条目表中查找条目的时间大致相同。在数组中查找元素(基于内容而不是索引)是线性的,即O(N)-平均而言,您将不得不查看一半的条目。

这会使哈希表比数组更快吗?不一定。如果你有一个非常小的条目集合,数组可能会更快-你可能能够在计算你正在查看的哈希码所需的时间内检查所有字符串。然而,随着数据集的增长,哈希表最终会击败数组。

快速注意,我的答案几乎肯定会将Big Oh符号(这是一个上限)与Big Theta表示法“θ”(这是一个双向边界)混淆。但根据我的经验,这实际上是非学术环境中讨论的典型。对造成的任何混乱表示歉意。


BigOh复杂性可以用这个图可视化:

Big Oh Analysis

对于Big Oh符号,我可以给出的最简单的定义是:

Big Oh符号是算法复杂性的相对表示。

在这句话中有一些重要的和故意选择的单词:

  • 相对:你只能将苹果与苹果进行比较。你不能将算术乘法的算法与对整数列表进行排序的算法进行比较。但是比较两个算法进行算术运算(一个乘法,一个加法)会告诉你一些有意义的事情;
  • 代表: BigOh(最简单的形式)将算法之间的比较简化为单个变量。该变量是根据观察或假设选择的。例如,排序算法通常根据比较操作进行比较(比较两个节点以确定它们的相对顺序)。这假设比较是昂贵的。但是如果比较很便宜但交换很昂贵怎么办?它改变了比较;和
  • 复杂度:如果排序10000个元素需要一秒钟,那么排序一百万个元素需要多长时间?在这种情况下,复杂性是对其他事物的相对度量。

当你读完剩下的内容后,再回来重读上面的内容。

我能想到的BigOh最好的例子是做算术。拿两个数字(123456和789012)。我们在学校学到的基本算术运算是:

  • 添加;
  • 减法;
  • 乘法;和
  • 司。

每一个都是一个操作或问题。解决这些问题的方法称为算法

加法是最简单的。您将数字排成一行(向右),并将数字添加到一列中,将该加法的最后一个数字写入结果中。该数字的“十”部分被转移到下一列。

让我们假设这些数字的加法是该算法中最昂贵的操作。按理说,要将这两个数字相加,我们必须将6位数字相加(可能带有7位)。如果我们将两个100位数字相加,我们必须进行100次加法。如果我们添加两个 10,000位数字,我们必须进行10,000次加法。

看到模式了吗?复杂性(操作次数)与较大数字中的n位数成正比。我们称之为O(n)线性复杂度

减法类似(除了你可能需要借用而不是携带)。

乘法是不同的。你把数字排成一行,取底部数字的第一位数字,然后依次乘以顶部数字的每一位数字,以此类推。所以要乘以我们的两个6位数字,我们必须做36次乘法。我们可能需要做多达10或11列加法才能得到最终结果。

如果我们有两个100位数字,我们需要做10,000次乘法和200次加法。对于两个100万位数,我们需要做一万亿(1012)乘法和200万次加法。

随着算法扩展到n-平方,这是O(n2二次复杂度。这是引入另一个重要概念的好时机:

我们只关心复杂性中最重要的部分。

精明的人可能已经意识到我们可以将操作数表示为: n2+2n。但是正如你从我们的例子中看到的,每个数字为一百万位,第二项(2n)变得无关紧要(到该阶段占总操作的0.0002%)。

人们可以注意到,我们在这里假设了最坏的情况。当乘以6位数字时,如果其中一个有4位数字,另一个有6位数字,那么我们只有24次乘法。尽管如此,我们仍然计算'n'的最坏情况,即当两个都是6位数字时。因此Big Oh表示法是关于算法的最坏情况。

电话簿

我能想到的下一个最好的例子是电话簿,通常被称为白页或类似的东西,但它因国家而异。但我说的是按姓氏列出人们的名字,然后是首字母或名字,可能是地址,然后是电话号码。

现在,如果你让计算机在一个包含100万个名字的电话簿中查找“约翰·史密斯”的电话号码,你会怎么做?忽略你能猜到S的开头有多远的事实(假设你不能),你会怎么做?

一个典型的实现可能是打开中间,将500,000th与“Smith”进行比较。如果碰巧是“Smith, John”,我们真的很幸运。更有可能的是“John Smith”将在该名称之前或之后。如果是在之后,那么我们将电话簿的后半部分分成两半并重复。如果是在之前,那么我们将电话簿的前半部分分成两半并重复。以此类推。

这被称为二分查找,无论您是否意识到,它每天都在编程中使用。

因此,如果你想在包含一百万个名字的电话簿中找到一个名字,你实际上可以通过最多20次这样做来找到任何名字。在比较搜索算法时,我们决定这种比较是我们的'n'。

  • 对于一个有3个名字的电话簿,需要进行2次比较(最多)。
  • 对于7,最多需要3。
  • 15,需要4。
  • 1,000,000需要20。

这是惊人的好,不是吗?

在BigOh术语中,这是O(log n)对数复杂度。现在有问题的对数可以是ln(以e为基)、log10、log2或其他一些基。没关系,它仍然是O(log n),就像O(2n2)和O(100n2)仍然都是O(n2)一样。

在这一点上值得解释的是,BigOh可以用算法来确定三种情况:

  • 最佳案例:在电话簿搜索中,最好的情况是我们在一次比较中找到名称。这是O(1)不变的复杂性
  • 预期案例:如上所述,这是O(log n);和
  • 最坏情况:这也是O(log n)。

通常我们不关心最好的情况。我们感兴趣的是预期的和最坏的情况。有时这两者中的一个会更重要。

回到电话簿。

如果你有一个电话号码,想找到一个名字怎么办?警方有一个反向电话簿,但公众不能进行这种查找。或者是这样吗?从技术上讲,你可以在普通电话簿中反向查找一个号码。怎么做?

你从名字开始,然后比较号码。如果匹配,太好了,如果不匹配,你就继续下一个。你必须这样做,因为电话簿是无序(无论如何按电话号码)。

因此,要查找给定电话号码的名称(反向查找):

  • 最佳案例: O(1);
  • O(n)(50万);以及
  • 最坏情况: O(n)(对于1,000,000)。

旅行的推销员

这是计算机科学中一个非常著名的问题,值得一提。在这个问题中,你有N个城镇。每个城镇通过一定距离的道路连接到一个或多个其他城镇。旅行推销员问题是找到访问每个城镇的最短旅程。

听起来很简单?再想想。

如果你有3个城镇A,B和C,所有对之间都有道路,那么你可以去:

  • A→B→C
  • A→C→B
  • B→C→A
  • B→A→C
  • C→A→B
  • C→B→A

实际上比这更少,因为其中一些是等价的(例如,A→B→C和C→B→A是等价的,因为它们使用相同的道路,只是相反)。

实际上,有三种可能性。

  • 把这个带到4个城镇,你有(iirc)12种可能性。
  • 5是60。
  • 6变成360。

这是一个称为阶乘的数学运算的函数。基本上:

  • 5!=5×4×3×2×1=120
  • 6!=6×5×4×3×2×1=720
  • 7!=7×6×5×4×3×2×1=5040
  • 25!=25×24 × … × 2×1=15,511,210,043,330,985,984,000,000
  • 50!=50×49 × … × 2×1=3.04140932×1064

所以旅行推销员的BigOh问题是O(n!)阶乘或组合复杂性

当你到达200个城镇时,宇宙中已经没有足够的时间来解决传统计算机的问题。

有些事情要考虑。

多项式时间

我想快速提及的另一点是,任何复杂度为O(na的算法都被称为具有多项式复杂度或可在多项式时间中求解。

O(n),O(n2)等都是多项式时间。有些问题不能在多项式时间内解决。世界上有一些东西就是因为这个原因而使用的。公钥密码学是一个最好的例子。在计算上很难找到一个非常大的数字的两个素数因子。如果不是,我们就不能使用我们使用的公钥系统。

无论如何,这就是我(希望是简单的英语)对BigOh(修订)的解释。

大O是算法相对于其输入大小使用多少时间/空间的度量。

如果算法为O(n),则时间/空间将以与其输入相同的速率增加。

如果算法是O(n2),则时间/空间以其输入平方的速率增加。

诸如此类。

大O描述了当输入变大时函数增长行为的上限,例如程序的运行时。

示例:

  • O(n):如果我将输入大小加倍,则运行时加倍

  • O(n2):如果输入大小加倍运行时四倍

  • O(log n):如果输入大小加倍,运行时增加1

  • O(2n):如果输入大小增加1,则运行时加倍

输入大小通常是表示输入所需的位空间。

它展示了算法如何根据输入大小进行缩放。

O(n2:已知二次复杂度

  • 1项:1项业务
  • 10项:100次行动
  • 100项:10,000次操作

请注意,项目的数量增加了10倍,但时间增加了10倍2。基本上,n=10,因此O(n2)给了我们缩放因子n2,即102

O(n):已知线性复杂度

  • 1项:1次手术
  • 10项:10项操作
  • 100项:100项操作

这次项目的数量增加了10倍,时间也是如此。n=10,因此O(n)的缩放因子为10。

O(1):已知不变的复杂性

  • 1项:1项业务
  • 10项:1项操作
  • 100个项目:1个操作

项目的数量仍以10的倍数增加,但O(1)的缩放因子始终为1。

O(log n):已知对数复杂度

  • 1项:1项业务
  • 10项:2项操作
  • 100个项目:3个操作
  • 1000项:4项操作
  • 10,000个项目:5次操作

计算次数只增加了输入值的对数。所以在这种情况下,假设每个计算需要1秒,输入n的日志是所需的时间,因此log n

这就是它的要点。他们减少了数学运算,所以它可能不完全是n2或他们所说的任何东西,但这将是缩放的主导因素。

Big O描述了算法的基本缩放性质。

有很多信息Big O没有告诉你关于给定算法的信息。它切入骨髓,只提供关于算法缩放性质的信息,特别是算法的资源使用(想想时间或内存)如何响应“输入大小”。

想一想蒸汽机和火箭的区别。它们不仅仅是同一事物的不同变体(比如普锐斯发动机和兰博基尼发动机),它们的核心是截然不同的推进系统。蒸汽机可能比玩具火箭快,但没有蒸汽活塞发动机能够达到轨道运载火箭的速度。这是因为这些系统在达到给定速度(“输入大小”)所需燃料关系(“资源使用”)方面具有不同的缩放特征。

为什么这如此重要?因为软件处理的问题在大小上可能存在高达一万亿倍的差异。考虑一下。前往月球所需的速度与人类步行速度之间的比率小于10,000:1,与软件可能面临的输入大小范围相比,这绝对是很小的。由于软件可能面临天文数字的输入大小范围,因此算法的大O复杂性是潜在的,这是基本的缩放性质,胜过任何实现细节。

考虑规范排序的例子。气泡排序是O(n2)而合并排序是O(n log n)。假设你有两个排序应用程序,应用程序A使用气泡排序,应用程序B使用合并排序,假设对于大约30个元素的输入大小,应用程序A在排序方面比应用程序B快1000倍。如果你永远不必对超过30个元素进行排序,那么很明显你应该更喜欢应用程序A,因为它在这些输入大小上要快得多。但是,如果您发现可能需要对一千万个项目进行排序,那么您所期望的是,在这种情况下,应用程序B实际上比应用程序A快数千倍,这完全是由于每个算法的扩展方式。

大O表示法是一种根据空间或运行时间描述算法上限的方法。n是问题中元素的数量(即数组的大小、树中节点的数量等)。我们有兴趣描述n变大时的运行时间。

当我们说某些算法是O(f(n))时,我们是说该算法的运行时间(或所需空间)总是低于某些常数时间f(n)。

说二进制搜索的运行时间为O(logns),就是说存在一些常量c,你可以用log(n)乘以它总是大于二进制搜索的运行时间。在这种情况下,你总是会有一些常数log(n)比较因子。

换句话说,其中g(n)是算法的运行时间,我们说g(n)=O(f(n))当g(n)<=c*f(n)当n>k时,其中c和k是一些常数。

Big-O符号(也称为“渐近增长”符号)是当你忽略原点附近的常数和东西时,函数“看起来像”什么。我们用它来谈论事物如何缩放


基础

对于“足够”大的投入…

  • f(x) ∈ O(upperbound)表示f“增长速度不超过”upperbound
  • f(x) ∈ Ɵ(justlikethis)表示f“生长完全像”justlikethis
  • f(x) ∈ Ω(lowerbound)表示f“增长不慢于”lowerbound

big-O表示法不关心常数因子:函数9x²被称为“完全像10x²一样增长”。big-O渐近表示法也不关心非渐近的东西(“原点附近的东西”或“当问题大小很小时会发生什么”):函数10x²被称为“完全像10x² - x + 2一样增长”。

为什么要忽略等式中较小的部分?因为当你考虑越来越大的尺度时,它们与等式中的大部分完全相形见绌;它们的贡献变得相形见绌,无关紧要。(见示例部分。)

换句话说,当你到达无穷大时,这一切都是关于比率的。如果您将实际花费的时间除以#0,您将在大输入的限制中获得一个常数因子。直观地说,这是有道理的:如果你能将一个相乘得到另一个,函数“像”彼此“缩放”。那就是我们说…

actualAlgorithmTime(N) ∈ O(bound(N))e.g. "time to mergesort N elementsis O(N log(N))"

…这意味着对于“足够大”的问题大小N(如果我们忽略原点附近的东西),存在一些常数(例如2.5,完全组成),使得:

actualAlgorithmTime(N)                 e.g. "mergesort_duration(N)       "────────────────────── < constant            ───────────────────── < 2.5bound(N)                                    N log(N)

常数有很多选择;通常“最佳”选择被称为算法的“常数因子”…但是我们经常忽略它,就像忽略非最大项一样(请参阅常数因子部分了解它们通常不重要的原因)。

一般来说,O(...)是最有用的,因为我们经常关心最坏的情况。如果f(x)代表“坏”的东西,比如处理器或内存使用,那么“f(x) ∈ O(upperbound)”意味着“upperbound是处理器/内存使用的最坏情况”。


应用场景

作为一个纯粹的数学结构,big-O符号不仅限于谈论流转时长和内存。你可以用它来讨论任何有意义的缩放的渐近性,例如:

  • N人在聚会上可能握手的次数(Ɵ(N²),特别是N(N-1)/2,但重要的是它“像一样缩放”)
  • 作为时间函数的一些病毒式营销的概率预期人数
  • 网站延迟如何随CPU或GPU或计算机集群中的处理单元数量而变化
  • CPU上的热输出如何作为晶体管计数、电压等的函数扩展。
  • 算法需要运行多少时间,作为输入大小的函数
  • 算法需要运行多少空间,作为输入大小的函数

示例

对于上面的握手示例,房间里的每个人都和其他人握手。在那个例子中,#handshakes ∈ Ɵ(N²)。为什么?

后退一点:握手的次数正好是n-选择-2或N*(N-1)/2(N个人中的每个人握手N-1其他人的手,但是这个双倍握手除以2):

每个人都和其他人握手。图片信用和许可证来自维基百科/维基共享资源相邻矩阵

然而,对于非常多的人来说,线性术语N相形见绌,有效地为比率贡献了0(在图表中:随着参与者数量的增加,对角线上的空框在总框中的比例越来越小)。因此,缩放行为是order N²,或者握手次数“像N²一样增长”。

#handshakes(N)────────────── ≈ 1/2N²

就好像图表对角线上的空框(N*(N-1)/2个复选标记)甚至不存在(渐近N2个复选标记)。

(“简单英语”的临时题外话:)如果你想证明这一点,你可以对比率执行一些简单的代数,将其拆分为多个项(lim表示“在极限范围内考虑”,如果你没有看到它,就忽略它,它只是表示“并且N真的非常大”):

    N²/2 - N/2         (N²)/2   N/2         1/2lim ────────── = lim ( ────── - ─── ) = lim ─── = 1/2N→∞     N²       N→∞     N²     N²      N→∞  1┕━━━┙this is 0 in the limit of N→∞:graph it, or plug in a really large number for N

tl; dr:对于大的值,握手次数看起来像x²,如果我们写下比率#handshakes/x²,我们不需要确切地x²握手的事实甚至不会在任意大的一段时间内显示在小数中。

例如,对于x=100万,比率#handshakes/x²:0.499999…


建立直觉

这让我们可以做出像…

“对于足够大的输入大小=N,无论常数因子是什么,如果I输入大小的两倍

  • 我将O(N)(“线性时间”)算法所需的时间增加一倍。

N→(2N)=2(N

  • …我将O(N²)(“二次时间”)算法所需的时间加倍(四倍)。

→(2N)²=4(

  • …我将O(N²)(“立方时间”)算法所需的时间加倍立方(八元)。

cN3→c(2N)¾=8(cN3

  • 我给O(log(N))(“对数时间”)算法所花费的时间添加一个固定的量。

c log(N)→c log(2N)=(c log(2))+(c log(N))=(固定量)+(c log(N)

  • 我不会改变O(1)(“恒定时间”)算法所花费的时间。

c*1c*1

  • 我“(基本上)加倍”O(N log(N))算法所花费的时间。

c 2N log(2N)/c N log(N)(这里我们除以f(2n)/f(n),但我们可以像上面那样按摩表达式并分解cNlogN)
→2 log(2N)/log(N)
→2(log(2)+log(N))/log(N)
→2*(1+(log2N)-1)(对于大N,基本上是2;最终小于2.000001)
(或者,假设log(N)总是低于17,所以它是O(17 N),这是线性的;这既不严格也不合理)

  • 我荒谬地增加了O(2N)(“指数时间”)算法所需的时间。

2N→22N=(4N)………………换句话说……2N→2n+1=2N21=22N

[对于数学倾向,您可以将鼠标放在小侧边的扰流板上]

(信用https://stackoverflow.com/a/487292/711085

(从技术上讲,常数因子在一些更深奥的例子中可能很重要,但我已经在上面措辞了(例如在log(N)中),这样它就不会了)

这些是程序员和应用计算机科学家用作参考点的基本增长顺序。他们经常看到这些。(因此,虽然从技术上讲,你可以认为“加倍输入会使O(√N)算法慢1.414倍”,但最好把它想象成“这比对数更糟糕,但比线性更好”。)


常数

通常,我们不关心具体的常数因子是什么,因为它们不影响函数的增长方式。例如,两个算法可能都需要O(N)时间来完成,但一个可能比另一个慢两倍。除非因子非常大,否则我们通常不会太关心,因为优化是一件棘手的事情(什么时候优化为时过早?);而且,仅仅选择一个具有更好big-O的算法通常会将性能提高几个数量级。

一些渐近优越的算法(例如,非比较O(N log(log(N)))排序)可能具有如此大的常数因子(例如100000*N log(log(N))),或者像O(N log(log(N)))这样具有隐藏+ 100*N的开销相对较大,即使在“大数据”上也很少值得使用。


为什么O(N)有时是你能做的最好的,即为什么我们需要数据结构

如果你需要读取所有数据,O(N)算法在某种意义上是“最佳”算法。非常的阅读行为一堆数据是O(N)操作。将其加载到内存中通常是O(N)(如果你有硬件支持,或者更快,如果你已经读取了数据,或者根本没有时间)。但是,如果你在每一块数据(甚至每一条数据)上触摸甚至,你的算法将花费O(N)时间来执行这种查找。无论你的实际算法需要多长时间,它至少会O(N),因为它花了那段时间查看所有数据。

对于非常的写作行为来说也是如此。所有打印N件东西的算法都需要N次时间,因为输出至少有那么长(例如,打印出所有排列(重新排列的方法)一组N张扑克牌是阶乘的:O(N!)(这就是为什么在这种情况下,好的程序会确保迭代使用O(1)内存,并且不会打印或存储每个中间步骤))。

这激发了数据结构的使用:一个数据结构只需要读取数据一次(通常是O(N)次),加上一些任意数量的预处理(例如O(N)O(N log(N))O(N²)),我们试图保持小规模。此后,修改数据结构(插入/删除/等)并对数据进行查询只需要很少的时间,例如O(1)O(log(N))。然后你继续进行大量的查询!

例如,假设您拥有数百万个路段的纬度和经度坐标,并希望找到所有街道交叉口。

  • 朴素的方法:如果你有一个街道交叉口的坐标,想要检查附近的街道,你每次都必须经过数百万个片段,并检查每个片段的邻接性。
  • 如果你只需要做一次,那么只做一次O(N)工作的简单方法是没有问题的,但是如果你想做很多次(在这种情况下,N次,每个段一次),我们必须做O(N²)工作,或者1000000²=1000000000000操作。不好(现代计算机每秒可以执行大约十亿次操作)。
  • 如果我们使用一个称为哈希表(一种即时速度查找表,也称为哈希图或字典)的简单结构,我们通过在O(N)时间内预处理所有内容来支付少量成本。此后,平均只需要恒定的时间来查找某个键(在这种情况下,我们的键是纬度和经度坐标,四舍五入为网格;我们搜索相邻的网格空间,其中只有9个,这是一个常量)。
  • 我们的任务从不可行的O(N²)变成了可管理的O(N),我们所要做的就是支付少量成本来制作哈希表。
  • 类比:这个例子的类比是拼图游戏:我们创建了一个利用数据某些属性的数据结构。如果我们的路段像拼图,我们通过匹配颜色和图案来对它们进行分组。然后我们利用这一点来避免以后做额外的工作(将颜色相似的拼图相互比较,而不是每个拼图)。

这个故事的寓意是:数据结构让我们可以加快操作。更重要的是,高级的数据结构可以让你以非常聪明的方式组合、延迟甚至忽略操作。不同的问题会有不同的类比,但它们都涉及到组织数据的方式,这种方式利用了我们关心的某种结构,或者我们人为地强加给它来记账的结构。我们确实会提前工作(基本上是计划和组织),现在重复的任务要容易得多!


实用示例:编码时可视化增长顺序

渐近表示法的核心与编程完全不同。渐近表示法是一个数学框架,用于思考事物如何扩展,并可以在许多不同的领域中使用。也就是说…这就是你适用渐近表示法编码的方式。

基础知识:每当我们与大小为A的集合中的每个元素(例如数组,集合,map的所有键等)进行交互时,或者执行循环的迭代时,那就是大小为A的乘法因子。(如果我们不做任何花哨的事情,比如跳过循环或提前退出循环,或者根据参数更改函数中的控制流,这是非常常见的。

(在这里,x代表恒定时间工作单元、处理器指令、解释器操作码等)

for(i=0; i<A; i++)        // A * ...some O(1) operation     // 1
--> A*1 --> O(A) time
visualization:
|<------ A ------->|1 2 3 4 5 x x ... x
other languages, multiplying orders of growth:javascript, O(A) time and spacesomeListOfSizeA.map((x,i) => [x,i])python, O(rows*cols) time and space[[r*c for c in range(cols)] for r in range(rows)]

示例2:

for every x in listOfSizeA:   // A * (...some O(1) operation         // 1some O(B) operation         // Bfor every y in listOfSizeC: // C * (...some O(1) operation       // 1))
--> O(A*(1 + B + C))O(A*(B+C))        (1 is dwarfed)
visualization:
|<------ A ------->|1 x x x x x x ... x
2 x x x x x x ... x ^3 x x x x x x ... x |4 x x x x x x ... x |5 x x x x x x ... x B  <-- A*Bx x x x x x x ... x |................... |x x x x x x x ... x v
x x x x x x x ... x ^x x x x x x x ... x |x x x x x x x ... x |x x x x x x x ... x C  <-- A*Cx x x x x x x ... x |................... |x x x x x x x ... x v

示例3:

function nSquaredFunction(n) {total = 0for i in 1..n:        // N *for j in 1..n:      // N *total += i*k      // 1return total}// O(n^2)
function nCubedFunction(a) {for i in 1..n:                // A *print(nSquaredFunction(a))  // A^2}// O(a^3)

如果我们做一些稍微复杂的事情,你仍然可以直观地想象发生了什么:

for x in range(A):for y in range(1..x):simpleOperation(x*y)
x x x x x x x x x x |x x x x x x x x x   |x x x x x x x x     |x x x x x x x       |x x x x x x         |x x x x x           |x x x x             |x x x               |x x                 |x___________________|

在这里,你能画出的最小的可识别的轮廓是重要的;三角形是二维形状(0.5 A^2),就像正方形是二维形状(A^2);这里的2的常数因子仍然是两者之间的渐近比,然而,我们像所有因素一样忽略它……(这个技术有一些不幸的细微差别,我不在这里讨论;它可能会误导你。

当然,这并不意味着循环和函数不好;相反,它们是现代编程语言的构建块,我们喜欢它们。然而,我们可以看到,我们将循环、函数和条件与我们的数据(控制流等)编织在一起的方式模仿了我们程序的时间和空间使用!如果时间和空间使用成为一个问题,那就是当我们诉诸聪明并找到一个我们没有考虑过的简单算法或数据结构时,以某种方式减少增长的顺序。尽管如此,这些可视化技术(尽管它们并不总是有效)可以给你一个最糟糕情况下运行时间的天真猜测。

这是我们可以直观识别的另一件事:

<----------------------------- N ----------------------------->x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x xx x x x x x x x x x x x x x x xx x x x x x x xx x x xx xx

我们可以重新排列它,看到它是O(N):

<----------------------------- N ----------------------------->x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x xx x x x x x x x x x x x x x x x|x x x x x x x x|x x x x|x x|x

或者您可以对数据进行log(N)次传递,总时间为O(N*log(N)):

   <----------------------------- N ----------------------------->^  x x x x x x x x x x x x x x x x|x x x x x x x x x x x x x x x x|  x x x x x x x x|x x x x x x x x|x x x x x x x x|x x x x x x x xlgN x x x x|x x x x|x x x x|x x x x|x x x x|x x x x|x x x x|x x x x|  x x|x x|x x|x x|x x|x x|x x|x x|x x|x x|x x|x x|x x|x x|x x|x xv  x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x|x

无关但值得一提的是:如果我们执行哈希(例如字典/哈希表查找),那是O(1)的因子。这很快。

[myDictionary.has(x) for x in listOfSizeA]\----- O(1) ------/
--> A*1 --> O(A)

如果我们做一些非常复杂的事情,例如使用递归函数或分而治之算法,你可以使用主定理(通常有效),或者在荒谬的情况下使用Akra-Bazzi定理(几乎总是有效)您可以在维基百科上查找算法的运行时间。

但是,程序员不会这样想,因为最终,算法直觉只会成为第二天性。你会开始写一些低效的代码,并立即思考“我在做什么吗?0”。如果答案是“是”,并且你预见到它实际上很重要,那么你可以退后一步,想各种技巧让事情运行得更快(答案几乎总是“使用哈希表”,很少“使用树”,很少有更复杂的东西)。


摊销和平均案例复杂性

还有“摊销”和/或“平均案例”的概念(注意它们是不同的)。

平均病例数:这只不过是对函数的期望值使用大O表示法,而不是函数本身。在通常的情况下,你认为所有输入的可能性相等,平均情况只是运行时间的平均值。例如使用快速排序,即使对于一些非常糟糕的输入,最坏的情况是O(N^2),平均情况是通常的O(N log(N))(非常糟糕的输入数量非常少,以至于我们在平均情况下没有注意到它们)。

摊销最坏情况:一些数据结构可能具有很大的最坏情况复杂性,但要保证,如果你做了很多这些操作,你所做的平均工作量将优于最坏情况。例如,你可能有一个数据结构,通常需要O(1)时间不变。然而,偶尔它会“打嗝”,一次随机操作需要O(N)时间,因为也许它需要做一些簿记或垃圾回收机制什么的……但它向你承诺,如果它打嗝了,在N多个操作中不会再打嗝。最坏情况下的成本仍然是每次操作O(N),但摊销成本经过多次运行O(N)/N=每次操作O(1)。因为大操作足够罕见,所以大量的偶尔工作可以被认为作为一个常数因子与其余工作混合在一起。我们说工作在足够多的调用中“摊销”,以至于它渐近消失。

摊销分析的类比:

你开车,有时你需要花10分钟去加油站,然后花1分钟用气体重新填充油箱。如果你每次开车去任何地方都这样做(花10美元)开车去加油站,花几秒钟加满油一加仑的几分之一),这将是非常低效的。但如果你把每隔几天上一次油箱,开车到水池的11分钟加油站在足够多的旅行中被“摊销”,你可以忽略它,假装你所有的旅行都可能长5%。

平均情况和摊销最坏情况之间的比较:

  • 平均情况:我们对输入做一些假设;即如果我们的输入有不同的概率,那么我们的输出/运行时将有不同的概率(我们取其平均值)。通常,我们假设我们的输入都有相同的可能性(均匀概率),但如果现实世界的输入不符合我们对“平均输入”的假设,平均输出/运行时的计算可能毫无意义。如果你预计输入是一致随机的,这是值得思考的!
  • 摊销最坏情况:如果你使用摊销后的最坏情况数据结构,最终性能保证在摊销后的最坏情况……(即使输入是由一个知道一切并试图欺骗你的邪恶恶魔选择的)。通常,我们用它来分析一些算法,这些算法可能在性能上非常“不稳定”,有意想不到的大打嗝,但随着时间的推移,它们的性能与其他算法一样好。(但是,除非你的数据结构有许多优秀工作的上限,它愿意拖延,否则邪恶的攻击者可能会迫使你一次性赶上最大的拖延工作。

不过,如果你对攻击者的看法是0,除了摊销和平均情况之外,还有许多其他算法攻击向量需要担心。)

平均情况和摊销都是非常有用的工具,用于考虑和设计缩放。

(如果对此子主题感兴趣,请参阅平均案例与摊销分析之间的差异


多维大O

大多数时候,人们没有意识到工作中有不止一个变量。例如,在字符串搜索算法中,你的算法可能需要时间O([length of text] + [length of query]),即它在像O(N+M)这样的两个变量中是线性的。其他更天真的算法可能是O([length of text]*[length of query])O(N*M)。忽略多个变量是我在算法分析中看到的最常见的疏忽之一,在设计算法时可能会阻碍你。


整个故事

请记住,big-O并不是故事的全部。你可以通过使用缓存来大幅加快一些算法,使它们成为缓存遗忘,通过使用RAM而不是磁盘来避免瓶颈,使用并行化,或提前做工作——这些技术通常是增长顺序“big-O”表示法的独立,尽管你经常会在并行算法的big-O表示法中看到内核的数量。

还要记住,由于程序的隐藏约束,你可能并不真正关心渐近行为。你可能使用有限数量的值,例如:

  • 如果你要对5个元素进行排序,你不想使用快速的O(N log(N))快速排序;你想使用插入排序,它恰好在小输入上表现良好。这些情况经常出现在分而治之的算法中,你将问题分成越来越小的子问题,例如递归排序、快速傅里叶变换或矩阵乘法。
  • 如果由于某些隐藏的事实,某些值被有效地限制了(例如,人类的平均姓名被软限制在大约40个字母,而人类的年龄被软限制在150左右)。您还可以对输入施加边界以有效地使术语保持不变。

在实践中,即使在具有相同或相似渐近性能的算法中,它们的相对优点实际上也可能是由其他因素驱动的,例如:其他性能因素(快速排序和合并排序都是O(N log(N)),但快速排序利用了CPU缓存);非性能考虑,如易于实现;库是否可用,以及库的信誉和维护程度。

与2GHz计算机上的程序运行速度相比,500MHz计算机上的程序运行速度也会变慢。我们并不认为这是资源界限的一部分,因为我们考虑的是机器资源的缩放(例如每个时钟周期),而不是每秒。然而,也有类似的事情会“秘密”影响性能,例如你是否在仿真下运行,或者编译器是否优化了代码。这可能会使一些基本操作花费更长的时间(甚至相对于彼此),甚至渐近地加快或减慢一些操作(甚至相对于彼此)。不同的实现和/或环境之间的影响可能很小或很大。你是否会切换语言或机器来完成这些额外的工作?这取决于其他一百个原因(必要性、技能、同事、程序员的生产力、时间的货币价值、熟悉程度、变通方法、为什么不组装或GPU等……),这可能比性能更重要。

上述问题,就像选择使用哪种编程语言的影响,几乎从未被认为是常量因素的一部分(也不应该被认为是);然而,人们应该意识到它们,因为有时(尽管很少)它们可能会影响一些事情。例如在cpython中,本机优先级队列实现是渐近非最优的(你选择插入或find-min时,O(log(N))而不是O(1));你使用另一种实现吗?可能不会,因为C实现可能更快,而且其他地方可能还有其他类似的问题。这是权衡;有时它们很重要,有时不重要。


编辑:“简单的英语”解释到此结束。

数学附录

为了完整性,big-O符号的精确定义如下:f(x) ∈ O(g(x))表示“f由const*g渐近上界”:忽略x的某个有限值以下的所有内容,存在一个常数,使得|f(x)| ≤ const * |g(x)|。(其他符号如下:就像O表示≤,Ω表示≥。有小写变体:o表示<,ω表示>。)f(x) ∈ Ɵ(g(x))表示f(x) ∈ O(g(x))f(x) ∈ Ω(g(x))(由g上界和下界):存在一些常数,使得f始终位于const1*g(x)|f(x)| ≤ const * |g(x)|0之间的“带”中。这是你能做出的最有力的渐近陈述,大致相当于|f(x)| ≤ const * |g(x)|1。(对不起,为了清楚起见,我选择推迟到现在才提到绝对值符号;特别是因为我从未见过负值出现在计算机科学背景下。)

人们通常会使用= O(...),这也许是更正确的“comp-sci”符号,并且使用起来完全合法;“f=O(…)”被读取为“f是顺序…/f是以…为界的xxx-”,并被认为是“f是其渐近性是…的某种表达式”。我被教导使用更严格的∈ O(...)的意思是“是”的一个元素(仍然像以前一样阅读)。在这个特定的情况下,O(N²)包含{2 N²3 N²1/2 N²2 N² + log(N)- N² + N^1.9,…}这样的元素,并且是无限大的,但它仍然是一个集合。

O和Ω不是对称的(n=O(n²),但n²不是O(n)),而是是对称的,因此是对称的、传递的和自反的,因此将所有函数的集合划分为等价类。等价类是一组我们认为相同的东西。也就是说,给定你能想到的任何函数,你可以找到该类的一个规范/唯一的“渐近代表”(通过通常取极限…I认为);就像你可以将所有整数分组为偶数或偶数一样,你可以将所有带有的函数分组为x-ish,log(x)^2-ish等……基本上忽略较小的项(但有时你可能会遇到更复杂的函数,这些函数本身就是独立的类)。

=可能是更常见的表示法,甚至被世界著名的计算机科学家在论文中使用。此外,通常情况下,在随意的环境中,人们在表示Ɵ(...)时会说O(...);这在技术上是正确的,因为Ɵ(exactlyThis)的事物集是O(noGreaterThanThis)的子集……并且更容易输入. ;-)

好吧,我的2美分。

Big-O,是程序消耗的资源的增长率,w. r. t.问题实例大小

资源:可以是总CPU时间,也可以是最大RAM空间。默认情况下指CPU时间。

假设问题是“求和”,

int Sum(int*arr,int size){int sum=0;while(size-->0)sum+=arr[size];
return sum;}

问题实例={5,10,15}==>问题实例大小=3,循环迭代=3

问题实例={5,10,15,20,25}==>问题实例大小=5次循环迭代=5

对于大小为“n”的输入,程序以数组中“n”次迭代的速度增长。因此Big-O是N表示为O(n)

假设问题是“找到组合”,

    void Combination(int*arr,int size){ int outer=size,inner=size;while(outer -->0) {inner=size;while(inner -->0)cout<<arr[outer]<<"-"<<arr[inner]<<endl;}}

问题实例={5,10,15}==>问题实例大小=3,总迭代=3*3=9

问题实例={5,10,15,20,25}==>问题实例大小=5,总迭代=5*5=25

对于大小为“n”的输入,程序以数组中“n*n”迭代的速度增长。因此Big-O是N2表示为O(n2

大O符号最常用于程序员,作为计算(算法)完成所需时间的近似度量,表示为输入集大小的函数。

大O有助于比较两种算法随着输入数量的增加而扩展的程度。

更准确地说,大O符号用于表示函数的渐近行为。这意味着函数在接近无穷大时的行为。

在许多情况下,算法的“O”将落入以下情况之一:

  • O(1)-无论输入集的大小如何,完成时间都是相同的。一个例子是通过索引访问数组元素。
  • O(日志N)-完成时间的增加大致与log2(n)一致。例如,1024个项目的时间大约是32个项目的两倍,因为Log2(1024)=10和Log2(32)=5。一个例子是在二叉查找树(BST)中找到一个项目。
  • O(N)-完成的时间与输入集的大小成线性关系。换句话说,如果你将输入集中的项目数量增加一倍,算法需要大约两倍的时间。一个例子是计算链表中的项目数量。
  • O(N Log N)-完成时间增加了项目数乘以Log2(N)的结果。这方面的一个例子是堆排序快速排序
  • O(N^2)-完成时间大致等于项目数量的平方。这方面的一个例子是冒泡排序
  • O(N!)-完成时间是输入集的阶乘。这方面的一个例子是旅行商问题蛮力解

随着输入大小向无穷大增加,Big O忽略对函数增长曲线没有有意义贡献的因素。这意味着添加到函数或乘以函数的常量会被忽略。

不确定我是否会对这个主题做出进一步的贡献,但仍然认为我会分享:我曾经发现这篇博客文章在Big O上有一些非常有用(尽管非常基本)的解释和示例:

通过例子,这有助于将基本知识融入我的龟甲般的头骨中,所以我认为这是一个非常下降的10分钟阅读,让你朝着正确的方向前进。

什么是大O的简单英语解释?尽可能少的正式定义和简单的数学。

关于Big-O符号的需要的简单英语解释:

当我们编程时,我们要解决一个问题。我们编码的东西叫做算法。大O表示法允许我们以标准化的方式比较算法的最差情况性能。硬件规格随时间而变化,硬件的改进可以减少算法运行的时间。但更换硬件并不意味着我们的算法随着时间的推移更好或改进,因为我们的算法仍然是相同的。所以为了让我们能够比较不同的算法,以确定一个更好或不更好,我们使用大O表示法。

什么是大O符号的简单英语解释:

并非所有算法都在相同的时间内运行,并且可以根据输入中的项目数量而变化,我们将其称为n。基于此,我们考虑最坏的情况分析,或者运行时的上限随着n变得越来越大。我们必须知道n是什么,因为许多大O符号都引用了它。

大O

f(x)=O(g(x))当x去a(例如, = +∞) 意味着有一个函数k

  1. f(x)=k(x)g(x)

  2. k在a的某个邻域有界(如果 = +∞, 这意味着存在数N和M,使得对于每个x>N,|k(x)|

换句话说,简单地说:f(x)=O(g(x)),x→a意味着在a的邻域中,f分解为g和某个有界函数的乘积。

小o

顺便说一下,这里是比较小o的定义。

f(x)=o(g(x))当x去a意味着有一个函数k:

  1. f(x)=k(x)g(x)

  2. k(x)当x变为a时变为0。

示例

  • sin x=O(x)当x→0。

  • sin x=O(1)当x→+∞,

  • x2+x=O(x)当x→0时,

  • x2+x=O(x2)当x→+∞,

  • ln(x)=o(x)=O(x)当x→+∞。

注意!带有等号“=”的符号使用了“假等式”:o(g(x))=O(g(x))是真的,但O(g(x))=o(g(x))是假的。类似地,当x时写“ln(x)=o(x) → +∞", 但公式“o(x)=ln(x)”没有意义。

更多示例

  • O(1)=O(n)=O(n2)当n → +∞ (但不是相反,等式是“假”),

  • O(n)+O(n2)=O(n2)当n→+∞

  • O(O(n2))=O(n2)当n→+∞

  • O(n2)O(n3)=O(n5)当n→+∞


这是维基百科的文章:https://en.wikipedia.org/wiki/Big_O_notation

算法示例(Java):

public boolean search(/* for */Integer K,/* in */List</* of */Integer> L){for(/* each */Integer i:/* in */L){if(i == K){return true;}}    
return false;}

算法说明:

  • 该算法逐项搜索列表,查找密钥,

  • 迭代列表中的每个项目,如果它是键,则返回True,

  • 如果循环结束而没有找到密钥,则返回False。

Big-O表示复杂度的上限(时间,空间,…)

要找到时间复杂度上的Big-O:

  • 计算最坏情况需要多少时间(关于输入大小):

  • 最坏情况:键在列表中不存在。

  • 时间(最坏情况)=4n+1

  • 时间:O(4n+1)=O(n)|在Big-O中,常数被忽略

  • O(n)~线性

还有Big-Omega,它代表了最佳案例的复杂性:

  • 最佳情况:关键是第一项。

  • 时间(最佳情况)=4

  • 时间:Ω(4)=O(1)~瞬间\常数

衡量软件程序的速度是非常困难的,当我们尝试时,答案可能非常复杂,充满了异常和特殊情况。这是一个大问题,因为当我们想将两个不同的程序相互比较以找出哪个“最快”时,所有这些异常和特殊情况都会分散注意力并且无济于事。

由于所有这些无益的复杂性,人们试图使用尽可能最小和最不复杂的(数学)表达式来描述软件程序的速度。这些表达式是非常非常粗略的近似:尽管,如果运气好的话,它们将抓住一个软件是快还是慢的“本质”。

因为它们是近似值,我们在表达式中使用字母“O”(Big Oh),作为一种惯例,向读者表明我们正在进行严重的过度简化(并确保没有人错误地认为表达式在任何方面都是准确的)。

如果你把“Oh”读成“on the order of”或“大约”的意思,你就不会错得太远。(我认为选择大Oh可能是一种幽默的尝试)。

这些“Big-Oh”表达试图做的唯一事情是描述当我们增加软件必须处理的数据量时,软件会减慢多少。如果我们需要处理的数据量增加一倍,软件是否需要两倍的时间来完成它的工作?十倍的时间?在实践中,你会遇到并且需要担心的Big-Oh表达数量非常有限:

好的方面:

  • O(1)恒定:无论输入有多大,程序都需要相同的时间来运行。
  • O(log n)对数:程序运行时的增加速度很慢,即使输入的大小大大增加。

坏处:

  • O(n)线性:程序运行时与输入的大小成比例增加。
  • O(n^k)多项式:处理时间增长越来越快-作为多项式函数-随着输入大小的增加。

和丑陋的:

  • O(k^n)指数程序运行时间增长非常快,即使问题的大小适度增加-只有使用指数算法处理小数据集才实用。
  • O(n!)阶乘程序运行时间将超过您等待任何东西的时间,除了最小和最琐碎的数据集。

"什么是大O的简单英语解释?尽可能简单的数学定义。"

这样一个美丽简单而简短的问题似乎至少应该得到一个同样简短的答案,就像学生在辅导期间可能得到的那样。

大O符号简单地告诉算法可以在多长时间内运行,只有输入数据量**

(*在美妙的时间感中!)
(**这很重要,因为人们会总是想要更多,不管他们活在今天还是明天)

好吧,大O符号有什么好的,如果它是这样做的?

  • 实际上,大O分析是非常有用和重要的,因为大O直接将焦点放在算法自身的复杂性上,而完全忽略了任何只是比例常数的东西,比如JavaScript引擎、中央处理器的速度、你的互联网连接,以及所有那些很快就会像模型T一样可笑地过时的东西。大O只关注对生活在现在和未来的人同样重要的性能。

  • 大O符号也直接聚焦于计算机编程/工程最重要的原则,这一事实激励所有优秀的程序员不断思考和梦想:超越技术缓慢前进的唯一方法是发明更好的算法。

假设我们正在谈论一个算法一个,它应该对大小为n的数据集做一些事情。

那么O( <some expression X involving n> )用简单的英语表示:

如果你在执行A时运气不好,它可能需要尽可能多的X(n)操作才能执行完成。

碰巧的是,有一些函数(把它们想象成X(n)中的实现)往往经常出现。这些是众所周知的,很容易比较(例如:1Log NNN^2N!等…)

在讨论一个和其他算法时,通过比较这些算法,很容易根据它们可能(最坏情况)需要完成的操作数量对算法进行排名。

一般来说,我们的目标是找到或构造算法一个,使其具有返回尽可能低的数字的函数X(n)

一个简单直接的答案可以是:

大O代表该算法最糟糕的时间/空间。该算法永远不会占用超过该限制的更多空间/时间。大O在极端情况下代表时间/空间复杂度。

这是我在解释Big-O的常见变种时倾向于使用的简单英语动物寓言集

在所有情况下,更喜欢列表中较高的算法而不是列表中较低的算法。然而,迁移到更昂贵的复杂性类的成本差异很大。

O(1):

没有增长。不管问题有多大,你都可以在相同的时间内解决它。这有点类似于广播,在给定的距离上广播需要相同的能量,而不管广播范围内的人数。

O(logn):

这种复杂性与O(1)相同,只是稍微差了一点。出于所有实际目的,您可以将其视为一个非常大的常量缩放。处理1000和10亿项之间的工作差异仅为6倍。

O(n):

解决问题的成本与问题的大小成正比。如果您的问题的大小增加一倍,那么解决方案的成本就会增加一倍。由于大多数问题必须以某种方式扫描到计算机中,例如数据输入、磁盘读取或流量,这通常是一个负担得起的扩展因子。

O(nlogn):

这种复杂度与O(n非常相似。出于所有实际目的,两者是等价的。这种复杂度通常仍被认为是可扩展的。通过调整假设,一些O(nlogn算法可以转换为O(n算法。例如,限制键的大小将排序从O(nlogn减少到O(n

O(n2):

增长为正方形,其中n是正方形边长。这与“网络效应”的增长率相同,网络中的每个人可能都认识网络中的其他人。增长是昂贵的。大多数可扩展的解决方案无法使用这种复杂程度的算法,而不做大量的练习。这通常适用于所有其他多项式复杂性-O(nk-也是。

O(2n):

无法扩展。您没有希望解决任何不重要的问题。对于知道要避免什么以及专家找到O(nk中的近似算法很有用。

大O表示法是一种描述给定任意进审量参数的算法运行速度的方法,我们称之为“n”。它在计算机科学中很有用,因为不同的机器以不同的速度运行,简单地说算法需要5秒并不能告诉你很多,因为当你运行一个具有4.5 Ghz八核处理器的系统时,我可能运行一个15年前的800 Mhz系统,无论算法如何,这可能需要更长的时间。因此,我们没有指定算法在时间上的运行速度,而是用进审量参数或“n”来表示它的运行速度。通过以这种方式描述算法,我们能够比较算法的速度,而无需考虑计算机本身的速度。

我有更简单的方法来理解时间复杂度计算时间复杂度的最常见度量是Big O符号。这删除了所有恒定因子,以便在N接近无穷大时可以估计与N相关的运行时间。一般来说,你可以这样想:

statement;

是常量。语句的运行时间不会相对于N而改变

for ( i = 0; i < N; i++ )statement;

是线性的。循环的运行时间与N成正比。当N加倍时,运行时间也是如此。

for ( i = 0; i < N; i++ ){for ( j = 0; j < N; j++ )statement;}

是二次的。两个循环的运行时间与N的平方成正比。当N加倍时,运行时间增加N*N。

while ( low <= high ){mid = ( low + high ) / 2;if ( target < list[mid] )high = mid - 1;else if ( target > list[mid] )low = mid + 1;else break;}

是对数的。算法的运行时间与N可以除以2的次数成正比。这是因为算法在每次迭代中将工作区域分成两半。

void quicksort ( int list[], int left, int right ){int pivot = partition ( list, left, right );quicksort ( list, left, pivot - 1 );quicksort ( list, pivot + 1, right );}

是N*log(N)。运行时间由N个对数循环(迭代或递归)组成,因此该算法是线性和对数的组合。

一般来说,用一维中的每个项目做某事是线性的,用二维中的每个项目做某事是二次的,将工作区域分成两半是对数的。还有其他Big O度量,如立方、指数和平方根,但它们并不那么常见。Big O表示法被描述为O()在哪里是度量。快速排序算法将被描述为O(N*log(N))。

注意:所有这些都没有考虑最佳,平均和最坏情况的度量。每个都有自己的Big O符号。还要注意,这是一个非常简单的解释。Big O是最常见的,但它也更复杂,我已经展示过了。还有其他的符号,例如大ω,小o和大θ。你可能在算法分析课程之外不会遇到它们。

如果你脑子里有一个合适的无穷大概念,那么有一个非常简短的描述:

大O表示法告诉你解决一个无穷大问题的成本。

此外,还有

常数可以忽略不计

如果你升级到一台可以以两倍的速度运行算法的计算机,大O符号不会注意到这一点。常数因子改进太小,甚至在大O符号工作的规模中都不会注意到。请注意,这是大O符号设计的有意部分。

然而,尽管可以检测到任何比常数因子“更大”的东西。

当对计算的大小“大”到足以被认为是近似无穷大感兴趣时,大O符号大约是解决问题的成本。


如果以上没有意义,那么你的头脑中就没有一个兼容的直觉无穷大概念,你可能应该忽略上述所有内容;我知道的唯一使这些想法严谨的方法,或者解释它们,如果它们还没有直观地有用,是首先教你大O符号或类似的东西。

如果我想向6岁的孩子解释这一点,我会开始画一些函数f(x)=x和f(x)=x^2,例如,并问孩子哪个函数将是页面顶部的上层函数。然后我们将继续绘图,看到x^2获胜。“谁赢了”实际上是当x趋向无穷大时增长更快的函数。所以“函数x在x^2的大O中”意味着当x趋向无穷大时,x的增长速度比x^2慢。当x趋向于0时也可以这样做。如果我们为x从0到1绘制这两个函数,x将是一个上函数,所以“函数x^2在x的大O中,因为x趋向于0”。当孩子长大后,我补充说,真正的大O可以是一个函数,它的增长速度不是更快,而是与给定函数相同。此外,常量被丢弃。所以2x在x的大O中。

最简单的方式来看待它(用简单的英语)

我们试图看看进审量参数如何影响算法的运行时间。如果应用程序的运行时间与进审量参数成正比,那么它被称为n的大O。

上述说法是一个好的开始,但并不完全正确。

更准确的解释(数学)

假设

n=进审量参数

T(n)=表示算法运行时间为n的函数的实际函数

c=常数

f(n)=表示算法运行时间为n的函数的近似函数

那么就大O而言,只要下面的条件为真,近似f(n)就被认为足够好。

lim     T(n) ≤ c×f(n)n→∞

等式读作当n接近无穷大时,n的T小于或等于c乘以n的f。

在大O符号中,这写为

T(n)∈O(n)

这读为T of n是n的大O。

返回英文

根据上面的数学定义,如果你说你的算法是n的大O,这意味着它是n(进审量参数)或更快的函数。如果你的算法是n的大O,那么它也自动是n平方的大O。

n的大O意味着我的算法运行至少和这个一样快。你不能看着你算法的大O符号说它慢。你只能说它快。

看看加州大学伯克利分校关于Big O的视频教程。这实际上是一个简单的概念。如果你听到Shewchuck教授(又名上帝级教师)解释它,你会说“哦,这就是全部!”。

这是一个非常简单的解释,但我希望它涵盖了最重要的细节。

假设您处理问题的算法取决于一些“因素”,例如,让我们将其设为N和X。

根据N和X,你的算法将需要一些操作,例如在最坏的情况下,它是3(N^2) + log(X)操作。

由于Big-O不太关心常数因子(也就是3),所以你的算法的Big-O是O(N^2 + log(X))。它基本上转换为“你的算法在最坏情况下需要的运算量”。

假设您从亚马逊订购了《哈利·波特:完整的8部电影合集》[蓝光],并同时在线下载了相同的电影合集。您想测试哪种方法更快。交付几乎需要一天才能到达,下载大约提前30分钟完成。太好了!所以这是一场紧张的比赛。

如果我订购几部蓝光电影,如《指环王》、《暮光之城》、《黑暗骑士三部曲》等,并同时在线下载所有电影怎么办?这次,交付仍然需要一天才能完成,但在线下载需要3天才能完成。对于网上购物,购买的商品数量(输入)不会影响交货时间。输出是恒定的。我们称之为O(1)

对于在线下载,下载时间与电影文件大小(输入)成正比。我们称之为O(n)

从实验中,我们知道在线购物比在线下载更好。理解大O符号非常重要,因为它可以帮助您分析算法的扩展性效率

备注: Big O表示算法的最坏的情况。让我们假设O(1)O(n)是上面示例的最坏情况。

参考http://carlcheo.com/compsci

你想知道所有关于大O的事吗?我也是。

所以说到大O,我将使用只有一个节拍的单词。每个单词一个音。小词很快。你知道这些单词,我也知道。我们将使用一个音的单词。它们很小。我相信你会知道我们将使用的所有单词!

现在,我们来谈谈工作。大多数时候,我不喜欢工作。你喜欢工作吗?你可能喜欢,但我肯定我不喜欢。

我不喜欢去工作。我不喜欢花时间在工作上。如果我有我的方式,我只想玩,做有趣的事情。你和我的感觉一样吗?

有时候,我确实得去工作。这很悲伤,但却是事实。所以,当我在工作的时候,我有一个规则:我尽量少做工作。尽可能地接近没有工作。然后我去玩!

所以这里有个大新闻:大O可以帮助我不工作!如果我知道大O,我可以更多的时间玩。少工作,多玩!这就是大O帮助我做的。

现在我有一些工作要做。我有一张单子:一、二、三、四、五、六。我必须把这张单子上的所有东西都加起来。

哇,我讨厌工作。但是哦,好吧,我必须这样做。所以我走了。

一加二是三……加三是六……四是……我不知道。我迷路了。这对我来说太难了。我不太喜欢这种工作。

所以我们不要做这些工作。让我们想想这有多难。我需要做多少工作,把六个数字相加?

好吧,让我看看。我必须把1和2相加,然后把它加到3,然后把它加到4……总而言之,我数了6个加法。我必须做6个加法来解决这个问题。

大O来了,告诉我们这个数学有多难。

大O说:我们必须做六个加法来解决这个问题。一个加法,从一到六的每一件事。六个小块工作…每一点工作都是一个加法。

好吧,我现在不会做添加它们的工作。但我知道这有多难。这将是六个添加。

哦,不,现在我有更多的工作了。哎呀。谁做这种东西?!

现在他们让我从一加到十!我为什么要这么做?我不想加一到六。从一加到十……嗯……那就更难了!

这会有多难?我还需要做多少工作?我需要更多还是更少的步骤?

嗯,我想我得做十次加法…从一到十,每次加一次。十比六还多。我得做更多的工作才能从一加到十,而不是从一加到六!

我现在不想补充。我只是想知道增加这么多可能有多难。而且,我希望,尽快参加比赛。

从一加到六,这是一些工作。但是你看,从一加到十,这是更多的工作吗?

大O是你我的朋友。大O帮助我们思考我们有多少工作要做,这样我们就可以计划。而且,如果我们和大O是朋友,他可以帮助我们选择不那么难的工作!

现在我们必须做新的工作。哦,不。我一点也不喜欢这个工作。

新的工作是:将所有从1到n的东西相加。

等等!n是什么?我错过了吗?如果你不告诉我n是什么,我怎么能从一到n相加?

嗯,我不知道n是什么。我没有被告知。是吗?没有?哦,好吧。所以我们不能做这项工作。呼。

但是,虽然我们现在不做这项工作,但我们可以猜到,如果我们知道n,我们将不得不把n件事加起来,对吗?当然!

现在大O来了,他会告诉我们这项工作有多难。他说:把所有的东西从1到N,一个接一个,是O(n)。要把所有这些东西加起来,[我知道我必须加n次。][1]这是大O!他告诉我们做某种工作有多难。

在我看来,大O就像一个又大又慢的老板。他想着工作,但他不做。他可能会说,“那工作很快。”或者,他可能会说,“那工作又慢又难!”但他不做工作。他只是看着工作,然后告诉我们可能需要多少时间。

为什么?我不喜欢工作!没有人喜欢工作。这就是为什么我们都喜欢大O!他告诉我们工作有多快。他帮助我们思考工作有多难。

哦,更多的工作。现在,让我们不要做这项工作。但是,让我们制定一个计划,一步一步地去做。

他们给了我们一副十张牌。它们都混在一起:七、四、二、六……一点也不直。现在……我们的工作是对它们进行分类。

呃。听起来工作量挺大的!

我们怎么把这副牌分类?我有个计划。

我看每对牌,一对一对,从第一到最后。如果一对中的第一张牌很大,下一张牌很小,我就交换它们。否则,我就去下一对,以此类推……很快,这副牌就完成了。

当套牌完成后,我问:我在那张纸牌中交换了吗?如果是这样,我必须从头开始再做一次。

在某一时刻,在某一时刻,将不会有任何掉期,而我们的甲板将被完成。这么多的工作!

好吧,那需要多少工作量,按照这些规则对卡片进行排序?

我有十张牌。而且,大多数时候--也就是说,如果我运气不好--我必须把整副牌翻遍十次,每次翻牌多达十次。

大奥,帮帮我!

大O进来说:对于一副n张牌,这样排序将在O(N平方)时间内完成。

为什么他说n平方?

嗯,你知道n的平方是n乘以n。现在,我明白了:检查了n张牌,直到可能有n次通过甲板。这是两个循环,每个循环有n个步骤。这是n的平方要做的工作。很多工作,肯定的!

现在,当大O说它将需要O(n平方)的工作时,他并不是指n平方,在鼻子上补充道。在某些情况下,它可能会少一些。但在最坏的情况下,排序甲板将接近n平方的工作步骤。

现在这里大O是我们的朋友。

Big O指出:当n变大时,当我们对卡片进行分类时,这项工作会比旧的只是添加这些东西的工作要困难得多。我们怎么知道这一点?

那么,如果n真的变大了,我们不在乎我们可能会增加到n或n的平方。

对于大n,n的平方比n大。

大O告诉我们排序比添加东西更难。对于大n,O(n平方)大于O(n)。这意味着:如果n变得非常大,对n个混合的东西进行排序必须花费更多的时间,而不仅仅是添加n个混合的东西。

大O并不能为我们解决工作。大O告诉我们工作有多难。

我有一副牌。我把它们分类了。你帮了忙。谢谢。

有没有更快的方法来分类卡片?大O能帮我们吗?

是的,还有一种更快的方法!学习需要一些时间,但它有效……而且效果相当快。你也可以试试,但每一步都要慢慢来,不要失去你的位置。

在这种对一副牌进行排序的新方法中,我们不像以前那样检查牌对。这是您对这副牌进行排序的新规则:

一:我在我们现在使用的部分牌组中选择一张牌。如果你愿意,你可以为我选择一张。(我们第一次这样做时,“我们现在使用的部分牌组”当然是整个牌组。)

第二:我在你选择的那张牌上张开甲板。这张牌是什么?我怎么张开?好吧,我从开始牌开始,一张接一张,我寻找一张比张牌更高的牌。

第三:我从底牌往上走,寻找一张比斜板更低的牌。

一旦我找到了这两张牌,我就交换它们,然后继续寻找更多的牌来交换。也就是说,我回到第二步,在你选择的牌上展开。

在某个时候,这个循环(从二到三)将结束。当这个搜索的两半在张纸牌相遇时,它就结束了。然后,我们刚刚用你在第一步中选择的牌张开了甲板。现在,开始附近的所有牌都比张纸牌低;靠近结尾的牌比张纸牌高。酷技巧!

四(这是有趣的部分):我现在有两个小甲板,一个比斜板低,一个高。现在我去第一步,在每个小甲板上!也就是说,我从第一个小甲板上的第一步开始,当这项工作完成后,我从下一个小甲板上的第一步开始。

我把甲板分成几部分,然后对每一部分进行排序,越来越小,越来越小,有时我没有更多的工作要做。现在这可能看起来很慢,有所有的规则。但是相信我,它一点也不慢。它比第一种排序方法要少得多!

这种排序叫什么?它叫做快速排序!这种排序是由一个叫C. A. R. Hoare的人做的,他称之为快速排序。现在,快速排序一直在使用!

快速排序将大任务分解为小任务。也就是说,它将大任务分解为小任务。

嗯。我想这里面可能有一条规则。把大任务变小,把它们分开。

这种排序相当快。有多快?Big O告诉我们:在平均情况下,这种排序需要O(n log n)个工作来完成。

它比第一种排序快还是慢?大O,请帮忙!

第一个排序是O(n平方)。但是快速排序是O(n log n)。你知道n log n小于n平方,对于大n,对吧?好吧,这就是我们知道快速排序快的原因!

如果你必须对甲板进行排序,最好的方法是什么?嗯,你可以做你想做的,但我会选择快速排序。

我为什么选择快速排序?当然,我不喜欢工作!我想尽快完成工作。

我怎么知道快速排序的工作量更少?我知道O(n log n)小于O(n平方)。O更小,所以快速排序的工作量更少!

现在你知道我的朋友,大O。他帮助我们做更少的工作。如果你知道大O,你也可以做更少的工作!

你跟我一起学的!你太聪明了!太谢谢你了!

工作结束了,我们去玩吧!


[1]:有一种方法可以作弊,一次把从1到n的所有东西都加起来。一个叫高斯的孩子在他八岁的时候发现了这一点。虽然我没有那么聪明,所以别问我他是怎么做到的

简单地说,大O就像<=(小于或等于)。当我们说两个函数f和g时,f=O(g)意味着f<=g。

然而,这并不意味着对于任何n f(n)<=g(n)。实际上它的意思是f在增长方面小于或等于g。它意味着在某一点之后 f(n)<=c*g(n)ifc是常数在某一点之后意味着比所有n>=n0其中n0是另一个不变

大O是表示任何函数上界的一种手段。我们通常用它来表示告诉算法运行时间的函数的上界。

例如:f(n)=2(n^2)+3n是一个代表假设算法运行时间的函数,Big-O符号本质上给出了这个函数的上限,即O(n^2)

这个符号基本上告诉我们,对于任何输入'n',运行时间都不会大于Big-O符号表示的值。

另外,同意以上所有详细的答案。希望这有助于!!

Big O描述了一类函数。

它描述了函数对于大输入值的增长速度。

对于给定的函数f,O(f)对所有可以找到n0和常量c的函数g(n)进行描述,使得n>=n0的g(n)的所有值都小于或等于c*f(n)

在较少的数学单词中,O(f)是一组函数。即所有函数,从某个值n0开始,增长速度慢或与f一样快。

如果f(n)=n,则

g(n)=3n在O(f)中。因为常数因子无关紧要h(n)=n+1000在O(f)中,因为对于小于1000的所有值,它可能更大,但对于大O,只有巨大的输入才重要。

然而,i(n)=n^2不在O(f)中,因为二次函数比线性函数增长得更快。

我找到了一个关于大O符号的非常好的解释,特别是对于一个不太喜欢数学的人。

https://rob-bell.net/2009/06/a-beginners-guide-to-big-o-notation/

计算机科学中使用大O表示法来描述性能或算法的复杂性。Big O特别描述了最坏的情况,可用于描述执行时间所需或使用的空间(例如在内存或磁盘上)算法

任何读过《编程珍珠》或任何其他计算机科学的人没有数学基础的书就会碰壁当他们到达提到O(N log N)或其他看似疯狂的语法。希望这篇文章能帮助你获得一个了解大O和对数的基础知识。

首先是程序员,其次是数学家(或者第三或第三我发现彻底理解Big O的最好方法是在代码中生成一些示例。所以,下面是一些常见的顺序在可能的情况下加上描述和例子。

O(1)

O(1)描述了一个总是在同一时间执行的算法(或空间)而不管输入数据集的大小。

bool IsFirstElementNull(IList<string> elements) {return elements[0] == null;}

O(N)

O(N)描述了一种算法,其性能将线性增长与输入数据集的大小成正比。示例下面还展示了Big O如何偏爱最坏的情况场景;在任何迭代期间都可以找到匹配的字符串for循环,函数会提前返回,但Big O符号会始终假设算法将执行的上限最大迭代次数。

bool ContainsValue(IList<string> elements, string value) {foreach (var element in elements){if (element == value) return true;}
return false;}

O(N2

O(N2)表示一种算法,其性能直接与输入数据集大小的平方成正比。这是与涉及数据嵌套迭代的算法常见设置。更深的嵌套迭代将导致O(N3)、O(N4)等。

bool ContainsDuplicates(IList<string> elements) {for (var outer = 0; outer < elements.Count; outer++){for (var inner = 0; inner < elements.Count; inner++){// Don't compare with selfif (outer == inner) continue;
if (elements[outer] == elements[inner]) return true;}}
return false;}

O(2N

O(2N)表示一个算法,其增长与每个附加值翻倍输入数据集。O(2N)函数的增长曲线为指数-开始非常浅,然后迅速上升。一个O(2N)函数的例子是斐波那契的递归计算数字:

int Fibonacci(int number) {if (number <= 1) return number;
return Fibonacci(number - 2) + Fibonacci(number - 1);}

对数

对数解释起来有点棘手,所以我将使用一个通用的示例:

二进制搜索是一种用于搜索排序数据集的技术通过选择数据集的中间元素,本质上是中位数,并将其与目标值进行比较。如果值匹配将返回成功。如果目标值高于探测元素将占据数据集的上半部分对其执行相同的操作。同样,如果目标值低于它将执行的探测元素的值对下半部进行操作。它将继续将数据减半设置每次迭代,直到找到值或直到它可以不再分割数据集。

这种类型的算法被描述为O(log N)二进制搜索示例中描述的数据集会产生增长曲线在开始时达到峰值,然后随着大小逐渐变平的数据集增加,例如包含10个项目的输入数据集完成需要一秒钟,包含100个项目的数据集需要2秒,包含1000个项目的数据集需要3秒将输入数据集的大小加倍对它的增长是在算法的一次迭代之后将减半,因此与输入数据集相当大小。这使得像二分搜索这样的算法非常有效在处理大型数据集时。

前言

算法:解决问题的过程/公式


如何分析算法以及我们如何将算法相互比较?

例子:要求你和一个朋友创建一个函数来求和从0到N的数字。你想出f(x),你的朋友想出g(x)。两个函数的结果相同,但算法不同。为了客观地比较我们使用大O符号算法的效率。

Big-O符号:描述随着输入变得任意大,运行时相对于输入的增长速度。

3关键收获:

  1. 比较运行时的增长速度不是比较精确运行时间(取决于硬件)
  2. 只关心相对于输入(n)的运行时增长
  3. n变得任意大时,专注于随着n变大而增长最快的项(想想无穷大)AKA渐近分析

空间复杂度:除了时间复杂度,我们还关心空间复杂度(算法使用多少内存/空间)。我们不是检查操作的时间,而是检查内存分配的大小。

大O-经济观点。

我最喜欢的英语单词来描述这个概念是价格,你为一个任务支付,因为它变得更大。

把它看作是经常性成本,而不是你在开始时支付的固定成本。在大局中,固定成本变得可以忽略不计,因为成本只会增长,而且会增加。我们想衡量它们增长的速度和它们增加的速度,以及我们为问题的设置-规模提供的原材料。

但是,如果初始设置成本很高,并且您只生产少量产品,则需要查看这些初始成本-它们也称为常数。

因为从长远来看,这些常量并不重要,所以这种语言允许我们讨论任务,而不仅仅是我们在什么样的基础设施上运行它。所以,工厂可以在任何地方,工人可以是任何人——这都是肉汁。但从长远来看,随着你的投入和产出的增长,工厂的规模和工人的数量是我们可以改变的。

因此,这就变成了你需要花多少钱来运行某件事情的大图近似。由于时间空间是这里的经济数量(即它们是有限的),它们都可以用这种语言来表达。

技术说明:一些时间复杂度的例子——O(n)通常意味着如果问题的大小为“n”,我至少必须看到所有内容。O(log n)通常意味着我将问题的大小减半,并检查和重复,直到任务完成。O(n^2)意味着我需要查看成对的事物(比如n人之间的聚会握手)。

什么是“大O”符号的简单英语解释?

快速备注:

“大O”中的O指的是“Order”(或准确地说“order of”)
所以你可以从字面上得到它的想法,它是用来订购一些东西来比较它们的。

  • “大O”做两件事:

    1. 估计您的计算机应用该方法来完成任务的步骤数。
    2. 促进与他人进行比较的过程,以确定它是否好?
    3. “Big O”通过标准化Notations实现了上述两个。
  • 有七个最常用的符号

    1. O(1),表示您的计算机以1步完成任务,非常出色,订购号
    2. O(logN),表示您的计算机以logN步完成任务,很好,排序为2
    3. O(N),用N步完成任务,它是公平的,订单号3
    4. O(NlogN),以O(NlogN)步结束任务,不好,订单4
    5. O(N^2),用N^2步完成任务,很糟糕,订单号5
    6. O(2^N),用2^N步完成任务,太可怕了,订单号6
    7. O(N!),用N!步完成任务,太可怕了,订单号7

在此处输入图片描述

假设你得到符号O(N^2),不仅你清楚该方法需要N*N个步骤来完成任务,而且你会发现它的排名不如O(NlogN)

请注意行尾的顺序,以便您更好地理解。

在CS中,完成任务的一组步骤称为算法。
在术语中,Big O表示法用于描述算法的性能或复杂性。

此外,Big O建立最坏情况或测量上限步骤。
您可以参考Big-Ω(Big-Omega)以获得最佳情况。

大Ω(大欧米茄)符号(文章)|可汗学院

  • 总结
    "Big O"描述算法的性能并对其进行评估。

    或者正式地解决它,“Big O”对算法进行分类并标准化比较过程。

TLDR:Big O用数学术语解释算法的性能。

较慢的算法倾向于以n的x次方运行,或者很多,这取决于它的深度。而更快的算法,如二分搜索,运行在O(log n),这使得它随着数据集变得更大而运行得更快。大O可以用其他使用n的术语来解释,甚至不使用n(即: O(1))。

人们可以计算Big O查看算法中最复杂的线条。

对于较小或未排序的数据集,Big O可能会令人惊讶,因为像二分搜索这样的n log n复杂性算法对于较小或未排序的集合可能会很慢,对于线性搜索与二分搜索的简单运行示例,请看我的JavaScript示例:

https://codepen.io/serdarsenay/pen/XELWqN?editors=1011(下面写的算法)

function lineerSearch() {init();var t = timer('lineerSearch benchmark');var input = this.event.target.value;for(var i = 0;i<unsortedhaystack.length - 1;i++) {if (unsortedhaystack[i] === input) {document.getElementById('result').innerHTML = 'result is... "' + unsortedhaystack[i] + '", on index: ' + i + ' of the unsorted array. Found' + ' within ' + i + ' iterations';console.log(document.getElementById('result').innerHTML);t.stop();return unsortedhaystack[i];}}}
function binarySearch () {init();sortHaystack();var t = timer('binarySearch benchmark');var firstIndex = 0;var lastIndex = haystack.length-1;var input = this.event.target.value;
//currently point in the half of the arrayvar currentIndex = (haystack.length-1)/2 | 0;var iterations = 0;
while (firstIndex <= lastIndex) {currentIndex = (firstIndex + lastIndex)/2 | 0;iterations++;if (haystack[currentIndex]  < input) {firstIndex = currentIndex + 1;//console.log(currentIndex + " added, fI:"+firstIndex+", lI: "+lastIndex);} else if (haystack[currentIndex] > input) {lastIndex = currentIndex - 1;//console.log(currentIndex + " substracted, fI:"+firstIndex+", lI: "+lastIndex);} else {document.getElementById('result').innerHTML = 'result is... "' + haystack[currentIndex] + '", on index: ' + currentIndex + ' of the sorted array. Found' + ' within ' + iterations + ' iterations';console.log(document.getElementById('result').innerHTML);t.stop();return true;}}}

定义:-大O表示法是一种表示如果数据输入增加,算法性能将如何执行的表示法。

当我们谈论算法时,算法有3个重要的支柱输入、输出和处理。大O是符号表示法,它说如果数据输入以什么样的速率增加,算法处理的性能会发生变化。

我鼓励你看看这个youtube视频,它用代码示例深入解释了大O符号

算法的基本支柱

例如,假设一个算法需要5条记录,处理相同的记录所需的时间是27秒。现在,如果我们将记录增加到10,算法需要105秒。

简单地说,所花费的时间是记录数量的平方。我们可以用O(n^2)来表示。这种符号表示称为大O符号。

现在请注意,单位可以是输入中的任何东西,可以是字节、记录的位数,性能可以用任何单位来衡量,比如秒、分钟、天等。所以它不是确切的单位,而是关系。

大O符号

例如,看看下面的函数“Function1”,它接受一个集合并对第一条记录进行处理。现在,对于这个函数,无论你放1000、10000或100000条记录,性能都将相同。所以我们可以用O(1)表示它。

void Function1(List<string> data){string str = data[0];}

现在请参阅下面的函数“Function2()”。在这种情况下,流转时长将随着记录的数量而增加。我们可以使用O(n)表示此算法的性能。

void Function2(List<string> data){foreach(string str in data){if (str == "shiv"){return;}}}

当我们看到任何算法的Big O符号时,我们可以将它们分为三类性能:

  1. 日志和常量类别:-任何开发人员都希望看到他们在这一类别中的算法性能。
  2. 线性:-开发人员不希望看到此类别中的算法,直到它是最后一个选项或唯一剩下的选项。
  3. 指数:这是我们不想看到我们的算法的地方,需要返工。

因此,通过查看Big O符号,我们对算法的好区域和坏区域进行分类。

沼泽O分类

我建议您观看这个10分钟的视频,其中讨论了Big O和示例代码

https://www.youtube.com/watch?v=k6kxtzICG_g

什么是“大O”符号的简单英语解释?

我想强调“大O”符号的驱动动机是一回事,当算法的输入大小获得太大时,描述算法度量的方程的一些部分(即常数、系数、项)变成如此微不足道,我们忽略它们。忽略其部分后幸存下来的方程部分被称为算法的“大O”符号

因此,如果输入大小不是太大,那么“大O”符号(上限)的想法就不重要了。


假设您想量化以下算法的性能
int sumArray (int[] nums){int sum=0;   // here we've 1 operationfor(int i=0; i < nums.length;i++){   // we've n timessum += nums[i]; // taking initialization and assignments, 3 ops}return sum;}

在上述算法中,假设您找到T(n)如下(时间复杂度):

T(n) = 3*n + 2

要找到它的“Big O”符号,我们需要考虑非常大的输入大小:

n= 1,000,000   -> T(1,000,000) = 3,000,002n=1,000,000,000  -> T(1,000,000,000) = 3,000,000,002n=10,000,000,000  -> T(10,000,000,000) = 30,000,000,002

让我们为另一个函数F(n) = n提供类似的输入

n= 1,000,000   -> F(1,000,000) = 1,000,000n=1,000,000,000  -> F(1,000,000,000) = 1,000,000,000n=10,000,000,000  -> F(10,000,000,000) = 10,000,000,000

正如你所看到的,当输入大小得到太大时,T(n)大约等于或接近F(n),所以常数2和系数3变得太微不足道了,现在大O"符号的想法出现了,

O(T(n)) = F(n)O(T(n)) = n

我们说T(n)的大O是n,符号是O(T(n)) = n,它是T(n)的上界,因为n得到太大

它表示从长远来看中算法的速度。

打个字面上的比方,你不关心跑步者能以多快的速度冲刺100m短跑,甚至5k跑。你更关心马拉松运动员,最好是超级马拉松运动员(超过这个类比,你必须回到“长跑”的隐喻意义)。

你可以安全地停止阅读这里。

我添加这个答案是因为我很惊讶其余的答案是多么的数学和技术。第一句话中“长期”的概念与任意耗时的计算任务有关。与受人类能力限制的跑步不同,某些算法完成计算任务可能需要数百万年以上。

那些数学上的对数多项式呢?事实证明,算法本质上与这些数学术语有关。如果你测量街区所有孩子的身高,你需要的时间和孩子们一样多。这本质上与n^1n的概念有关,其中n只不过是街区上孩子的数量。在超级马拉松的情况下,你正在测量你所在城市所有孩子的身高,但你必须忽略旅行时间,并假设他们都在一条线上(否则我们会跳过当前的解释)。

假设你想把孩子们的身高按照最短身高到最长身高的顺序排列。如果只是你附近的孩子,你可能只是盯着它,然后得出有序的列表。这是“冲刺”的类比,我们真的不关心计算机科学中的冲刺,因为当你能看到一些东西时,为什么要使用计算机?

但是,如果你在你的城市,或者更好的是,你的国家,排列所有孩子的身高列表,那么你会发现你如何做到这一点本质上与数学日志n^2联系在一起。通过你的列表找到最矮的孩子,把他的名字写在一个单独的笔记本上,然后把它从最初的笔记本上划掉,是本质上与数学n^2联系在一起。如果你想安排你的笔记本的一半,然后是另一半,然后结合结果,你会得到一个与对数联系在一起的方法。

最后,假设你首先必须去商店买卷尺。这是一个在短期冲刺中重要的努力的例子,例如测量街区上的孩子,但是当你测量城市中所有的孩子时,你可以放心地忽略这个成本。这是与低阶多项式项的数学下降的内在联系。

我希望我已经解释过,大O符号仅仅是关于长期的,数学本质上与计算方式有关,数学术语的删除和其他简化以一种相当常识的方式与长期有关。

一旦你意识到这一点,你会发现大O真的非常容易,因为所有困难的高中数学都很容易掉下来。唯一困难的部分是分析算法来识别数学术语,但是通过一些练习,你可以在分析过程中开始丢弃术语,安全地忽略算法的大块,只关注与大O相关的部分。

快乐的大O,这是我最喜欢的关于计算机科学的事情——发现一些事情比我想象的要容易得多,然后能够在谷歌面试中炫耀,当外行人被吓倒时,哈哈。

已经发布了一些很好的答案,但我想以不同的方式做出贡献。如果你想可视化正在发生的一切,你可以假设编译器可以在~1秒内执行接近10^8的操作。如果输入是在10^8中给出的,你可能想设计一个以线性方式运行的算法(比如一个未嵌套的for循环)。下面的表格可以帮助你快速搞清楚你想搞清楚的算法类型;)

给定的输入顺序:您可能想要使用的算法类型

当我们有一个像f(n) = n+3这样的函数,我们想知道当n接近无穷大时图的样子,我们只需删除所有常量和低阶项,因为当n变大时它们并不重要。这给我们留下了f(n) = n,所以为什么我们不能使用这个,为什么我们需要寻找一些在我们的f(n) = n+3函数之上和之下的函数,所以大O和大Omega。

因为当n接近无穷大时,说函数只是f(n) = n是不正确的,所以为了正确,我们描述了f(n) = n+3可能在的区域。我们对图的确切位置不感兴趣,因为低阶项和常数不会显著改变图的增长,所以换句话说,从上界和下界包围的区域是我们f(n)=n+3函数的模糊版本。

仅仅下降的常数和低阶项正是寻找函数的过程,这是下面和上面。

根据定义,一个函数是另一个函数的下界或上限,如果你能找到一个常量,你可以用它乘以f(n) = n函数,这样每n的输出都比原始函数大(或更小):

f(n) = n*C > f(n) = n+3

是的,C = 2会这样做,因此我们的函数f(n) = n可以是我们的f(x) = x+3函数的上限。

下限也一样:

f(n) = n*C < f(n) = n+3

C = -2就可以了

所以f(x) = nf(x) = x+3的上限和下限,当它的O和Omega都比它的Theta大时,这意味着它是紧密结合的。

所以大O也可能是f(x) = x^2,因为它满足条件f(n) = n^2*C > f(n) = n+3。它在我们的f(n) = n+3图之上,但是这个上界和下界之间的区域不像我们之前的边界那么精确。

从(来源)可以读到:

大O符号是一种数学符号描述了限制当参数趋向于特定的时函数的行为值或无穷大。(…)在计算机科学,大O符号用于对算法进行分类根据它们的运行时间或空间需求如何随着输入大小增长。

Big O表示法不代表每个si的函数,而是一组函数具有一定的渐近上界;正如可以从来源中读取的那样:

大O符号根据函数的增长来表征函数速率:具有相同增长率的不同函数可能是使用相同的O符号表示。

非正式地,在计算机科学时间复杂度空间复杂度理论中,人们可以将Big O表示法视为分别具有关于时间和空间的某些最坏情况的算法的分类。例如,O(n)

一个算法被称为线性时间/空间,或O(n)时间/空间,如果它的时间/空间复杂度是O(n)。非正式地,这意味着运行时间/空间最多与输入的大小线性增加(来源)。

O(n log n)为:

如果T(n)=O(n log^k n)某个正常数k,则称算法在拟线性时间/空间中运行;线性时间/空间是k=1(来源)的情况。

尽管如此,通常这种放松的措辞通常用于量化(在最坏的情况下)一组算法与另一组算法在输入大小增加方面的行为。要比较两类算法(e. g.O(n log n)O(n)),人们应该分析这两类算法在最坏的情况下随着输入大小(即, n)的增加的行为;当它趋向于无穷大时分析n

输入图片描述

在上面的图像中,big-O表示绘制函数的渐近最小上界之一,并且不引用集合O(f(n))

例如,在特定输入后的图像中比较O(n log n)O(n)O(n log n)(绿线)比O(n)(黄线)增长得更快。这就是为什么(在最坏的情况下)O(n)O(n log n)更理想,因为可以增加输入大小,前者的增长速度比后者慢。

只是为了以快速简单的方式表达算法的复杂性。大O符号的存在是为了解释任何给定算法的最佳、最差和平均情况时间复杂度。在可能的问题实例的大小上只有数值函数。

在其他方面,精确地使用这些函数是非常困难的,因为它们倾向于:

  • 有太多的颠簸-像二分搜索这样的算法通常运行对于大小正好为n=2k−1的数组(其中k是整数),因为数组分区工作得很好。这个细节不是特别但它警告我们,任何时间的时间复杂度函数算法可能非常复杂,几乎没有上下颠簸如图2.2所示。
  • 需要太多的细节来精确指定-计算确切的数字在最坏的情况下执行的RAM指令需要算法指定到完整计算机程序的细节。此外,精确的答案取决于无趣的编码细节(例如,他是否使用了一个案例语句或嵌套的ifs?)。执行精确的最坏情况分析,例如T(n)=12754n2+4353 n+834lg2 n+13546显然是非常困难的工作,但提供给我们的额外信息很少比观察“时间与n二次增长”。

事实证明,在简单的上限和下限方面进行讨论要容易得多使用Big Oh符号的时间复杂度函数。Big Oh简化了我们的分析通过忽略不影响我们比较的细节水平算法。Big Oh符号忽略了乘法常量之间的差异。函数f(n)=2n和g(n)=n在Big Oh分析中是相同的

https://mimoza.marmara.edu.tr/~msakalli/cse706_12/SkienaTheAlgorithmDesignManual.pdf