这是否是识别算法的“大O”符号的正确“规则”?


29

我一直在学习有关大O表示法的更多信息,以及如何根据算法编写方式对其进行计算。我遇到了一组有趣的“规则”,用于计算算法“大O”表示法,我想看看自己是在正确的轨道上还是在正确的道路上。

大O表示法:N

function(n) {
    For(var a = 0; i <= n; i++) { // It's N because it's just a single loop
        // Do stuff
    }
}

大O表示法:N 2

function(n, b) {
    For(var a = 0; a <= n; a++) {
        For(var c = 0; i <= b; c++) { // It's N squared because it's two nested loops
            // Do stuff
        }
    }
}

大O标记:2N

function(n, b) {
    For(var a = 0; a <= n; a++) {
        // Do stuff
    }
    For(var c = 0; i <= b; c++) { // It's 2N the loops are outside each other
        // Do stuff
    }
}

大O符号:NLogN

function(n) {
    n.sort(); // The NLogN comes from the sort?
    For(var a = 0; i <= n; i++) {
        // Do stuff
    }
}

我的示例和后续符号正确吗?我还应该注意其他符号吗?


3
称其为经验法则而不是公式,您可能在正确的道路上。当然,这完全取决于“做什么”的确切作用。Log(N)通常来自执行某种二进制/类树分区的算法。这是有关该主题的出色博客文章。
Daniel B

15
没有2N大-O表示法之类的东西。
vartec

15
@JörgWMittag,因为根据大O的定义,O(2n)= O(n)
棘轮怪胎

3
@JörgWMittag:确实不是钓鱼的地方。
vartec 2013年

3
@vartec-我不相信JörgWMittag是有目的的。在我最近的研究中,我注意到严格的Big-O表示法与混合了Big-O,Theta和其他派生词的“通用白话”之间的混淆。我并不是说常用用法是正确的;只是它经常发生。

Answers:


26

形式上,big-O符号描述复杂程度。

要计算big-O表示法:

  1. 确定算法复杂度的公式。例如,假设有两个循环,其中一个嵌套在其中,然后又有三个未嵌套的循环:2N² + 3N
  2. 删除除最高期限以外的所有内容: 2N²
  3. 删除所有常量:

换句话说,两个循环嵌套在另一个循环中,然后另外三个未嵌套的循环是O(N²)

当然,这假定循环中的内容很简单。例如,如果您sort()在循环内部,则必须将循环的复杂性乘以sort()基础语言/库所使用的实现的复杂性。


严格来讲, “删除所有常量”将2N³变为N。“删除所有加法和乘法常数”将更接近事实。
Joachim Sauer

@JoachimSauer:N²= N * N,那里没有常数。
vartec

@vartec:根据相同的论点2N = N+N
Joachim Sauer

2
@JoachimSauer,您的“严格来说”绝对是非常规的。参见en.wikipedia.org/wiki/Constant_(mathematics)。在讨论多项式时,“常数”始终仅指代系数,而不指代指数。
李·李

1
@vartec,请参阅上面的评论。您在此处使用“常量”绝对是正确和常规的。
李·李

6

如果要分析这些算法,则需要定义// dostuff,因为这确实可以改变结果。假设dostuff需要常数O(1)的操作数。

以下是一些使用这种新符号的示例:

对于第一个示例,线性遍历:这是正确的!

上):

for (int i = 0; i < myArray.length; i++) {
    myArray[i] += 1;
}

为什么它是线性的(O(n))?当我们向输入(数组)添加其他元素时,发生的操作数量与我们添加的元素数量成正比。

因此,如果需要执行一个操作来增加内存中某个位置的整数,我们可以使用f(x)= 5x = 5个其他操作对循环所做的工作进行建模。对于20个其他元素,我们执行20个其他操作。数组的单遍往往是线性的。桶分类之类的算法也是如此,它们能够利用数据结构在数组的一次通过中进行分类。

您的第二个示例也将是正确的,如下所示:

O(N ^ 2):

for (int i = 0; i < myArray.length; i++) {
    for (int j = 0; j < myArray.length; j++) {
        myArray[i][j] += 1;
    }
}

在这种情况下,对于第一个数组中的每个其他元素,我们必须处理j的全部。将i加1实际上是将(j的长度)加到j。因此,您是对的!此模式为O(n ^ 2),或者在我们的示例中为O(i * j)(如果i == j,则为n ^ 2,这在矩阵运算或平方数据结构中通常是这种情况。

您的第三个示例是,情况随原料的不同而改变。如果代码是按编写的方式进行的,则填充实际上是一个O(n),因为我们有2个遍历,大小为n,而2n减少为n。相互之间的循环不是可以产生2 ^ n代码的关键因素。这是一个2 ^ n函数的示例:

var fibonacci = function (n) {
    if (n == 1 || n == 2) {
        return 1;
    }

    else {
        return (fibonacci(n-2) + fibonacci(n-1));
    }
}

该函数为2 ^ n,因为对函数的每次调用都会对函数(Fibonacci)产生两次额外的调用。每次调用该函数时,我们要做的工作量都会加倍!它生长得非常快,就像从头上抽出九头蛇一样,每次都有两个新芽萌芽!

对于最后一个示例,如果您正在使用像merge-sort之类的nlgn排序,那么正确的是此代码将为O(nlgn)。但是,您可以利用数据的结构在特定情况下(例如,在已知的有限范围内的值(例如1至100)上)进行更快的排序。但是,您认为最高的代码占主导地位是正确的;因此,如果O(nlgn)排序紧接耗时少于O(nlgn)的任何操作,则总时间复杂度将为O(nlgn)。

在JavaScript中(至少在Firefox中),Array.prototype.sort()中的默认排序确实是MergeSort,因此您要在最终场景中使用O(nlgn)。


您的斐波那契示例实际上是斐波那契吗?我知道这并不与您要提出的观点相抵触,但是这个名称可能会误导他人,因此,如果实际上不是斐波那契,那会分散您的注意力。
Paul Nikonowicz

1

您的第二个示例(从0到n的外循环,从0到b的内循环)将是O(nb),而不是O(n 2)。规则是,您正在计算n次,对于每个计算,您正在计算其他事物b次,因此此函数的增长仅取决于n * b的增长。

您的第三个示例就是O(n)-您可以删除所有常量,因为它们不随n增长,而增长就是Big-O表示法的全部含义。

对于最后一个示例,是的,您的Big-O表示法一定会来自sort方法,如果它是基于比较的(通常是这种情况),则它是最有效的形式O(n * logn) 。


0

回想一下,这是运行时的近似表示。“经验法则”是近似的,因为它不精确,但出于评估目的给出了良好的一阶近似。

实际的运行时间将取决于yadda的堆空间,处理器,指令集的速度,使用前缀或后缀增量运算符等的速度。适当的运行时分析将使您可以确定接受程度,但了解基础知识后,您可以从一开始就进行编程。

我确实同意您在正确的方向上了解如何将Big-O从教科书合理化为实际应用。这可能是很难克服的障碍。

渐近增长率在大型数据集和大型程序上变得非常重要,因此对于典型示例,您证明它对适当的语法和逻辑并不那么重要。


-1

大哦,按定义,这意味着:对于函数f(t),存在一个函数c * g(t),其中c是任意常数,对于t> n,f(t)<= c * g(t),其中n是一个任意常数,则f(t)存在于O(g(t))中。这是一种数学符号,在计算机科学中用于分析算法。如果您感到困惑,我建议您查看闭包关系,这样一来,您可以在更详细的视图中看到这些算法如何获得这些big-oh值。

此定义的一些结果:O(n)实际上等于O(2n)。

另外,还有许多不同类型的排序算法。比较排序的最小Big-Oh值是O(nlogn),但是有很多排序的big-oh较差。例如,选择排序具有O(n ^ 2)。某些非比较排序可能具有更好的big-oh值。例如,桶分类具有O(n)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.