循环嵌套的复杂性

时间:2016-08-27 13:47:00

标签: arrays algorithm time-complexity big-o

我试图用我的算法来计算算法的时间,使用Big O表示法,我无法找到一个非常明确的解释。

基本上,我的算法在于将一个新数组与一个" parent"中的所有其他数组进行比较。阵列。

为此,我有一个for循环,迭代父数组中的所有元素,寻找一个看起来像新创建的数组的数组。

以下是代码:

bool AlreadyExistingArray(Array array)
{
    bool areEqual = true;
    foreach (Array a in arrayEntries)
    {
        if (a.count != array.count)
            continue;

        foreach (int i in array)
        {
            if (!a.contains(i))
            {
                areEqual = false;
                break;
            }
        }
        if (areEqual)
        {
            areEqual = false;
            foreach (int i in a)
            {
                if (!a.contains(i))
                {
                    areEqual = false;
                    break;
                }
            }
        }
    }
    return areEqual;
}

据我所知,每个for循环应该是O(n),但复杂性是否应该组成?由于我处理不同大小的数组,我很确定复杂性不能被认为是O(n ^ 2)。

希望我明白了!否则,请告诉我,我会进一步澄清。

编辑:更改算法。

4 个答案:

答案 0 :(得分:4)

Big-Oh的小背景:

你在这里获得如此多的不同答案的原因是Big-O分析并不像计算程序运行的步数那么简单。向计算机科学家讲授的这个简化版本是Big-O,Big-Omega和Big-Theta渐近边界函数概念的近似(通常是足够的)。实际上有一个formal definition的Big-O可以消除这些含糊之处。

尽管如此,让我们坚持下去。我在评论中的答案是这样的:

  

调用arrayEntries的大小:len(arrayEntries) = n和数组len(array) = m的大小以及数组len(largestOfarray) = k中最大条目的大小(即您调用的最大变量的大小<强>一)。然后你的算法是O(n(m + k))。无论n,m或k中的哪一个都是永远不会改变大小的常数,只需将它们从该等式中移除即可。

让我解释一下。 Big-O的定义大致是:

  

在计算机科学中,大O符号用于根据算法如何响应输入大小的变化对算法进行分类,例如算法的处理时间随着问题大小变得非常大而变化。

当你说算法是O(n^2)时,这意味着什么。你是说你的算法的运行时可以表示为某个函数T(n)(注意它只有一个变量n)并且渐近T(n)增长不快于n^2(也就是说,非常粗略地说,当n变得非常大时,n处的函数f(n) = n^2的梯度将大于n处的T(n)的梯度。

现在在前面的例子中,算法的运行时间取决于一个变量n。这并非总是如此。想象一下,你有一个这样的程序:

void algorithm(Array arrayM, Array arrayN) {
    for(int i : arrayN) {
        // runs len(arrayN) = n times
    }
    for(int j : arrayM) {
        // runs len(arrayM) = m times
    }
}

此算法的运行时是一个函数,它取决于arrayM和arrayN的大小,因此它将表示为T(n,m)。现在,如果这些大小是独立变量(即arrayM的大小与arrayN的大小无关),则此算法的运行时为O(m+n)。但是,如果arrayM的大小确实取决于arrayN的大小(假设它是通过将arrayN的一半元素复制到其中来初始化的),那么len(arrayM) = m实际上取决于n m = n/2。因此,您之前O(m+n)的算法的时间复杂度现在为O(n+n/2) = O(n)。这本质上是因为您的运行时函数T(n,m)现在可以写为T(n, n/2) ~ T(n),即它是一个变量的函数。

您的具体示例:

现在,对于您的程序,我们首先假设 arrayEntries len(arrayEntries) = n的大小和数组 len(array) = m的大小和数组中最大条目的大小(最大可能 a len(largestOfarray) = k是完全独立的变量,彼此不相互依赖。这不是一个不合理的假设,因为你在一篇评论中说过,内部循环并不依赖于外部循环的任何值。因为它是用户输入的数量,并且输入的长度可能是任意长的,因为一个用户可能输入长度为1的东西而另一个用户可能输入1000个字符长的行。

由于n,m和k是独立的,因此您的复杂性就是这样。你的外循环运行n次,在每次迭代中,第一个内循环将运行m次,第二个内循环最差运行k次。因此,您的总复杂度为O(n(m+k))。但这就是全部吗?好吧不太好。

看到n,m和k有一些界限。即,用户输入(k)的长度可能具有限制(即,如果它是从stdin获取的,那么它可能不会超过1000个字符)。如果是这样,你可以合理地说最坏的k可能是1000,所以我们可以将它视为一个常数,然后你的算法的复杂性是O(nm)因为常数k被消除了。此步骤完全取决于您对程序的使用方式,但如果您想要安全,则可以说复杂性为O(n(m+k))

然而这是有问题的,可能没有人认为m和n也是有限的,因为它们的大小有限(即操作系统将为您的程序分配多少内存)因此被处理作为常数?技术上,是的。在某些算法分析中,这有时是有用的(例如在增长缓慢的算法的情况下)。

最终,这完全取决于您认为您的计划如何运作以及合理的假设(即k可以被视为常数)以及要避免哪些假设。在我个人看来,这个算法可能是O(n(m+k)),可能是O(nm),但我不会削减它比这更多; m和n似乎与你的描述完全独立。

重要编辑(代码编辑后,上面的答案不正确):

一个有趣的案例研究实际上是@frenzykryger对此答案所评论的内容;我错过了一个细节,因为你在我写答案时编辑了你的问题。评论者说您更改了外循环的开始,以检查 a 的大小是否等于数组的大小。这意味着你的第二个内环运行的次数(即如上所述的k的大小)现在完全取决于m(调用m是数组的大小),即k = m。因此,您的算法为O(n(m+m)) = O(nm)。现在如果你保证m总是小于n,那么算法将是O(n^2)(m可以被丢弃)。但是如果m是无界的(可以是任何大小),那么算法仍然是O(nm)

最后备注:

正如您所看到的,Big-Oh分析有时候没有一个正确的答案。这一切都取决于你的程序将如何表现,你保证获得什么样的输入,以及许多其他因素。如果所有这些都让你希望有更严格的方式来定义它,那肯定有 - 只是google&#34; Big-Oh正式定义&#34;,阅读一些链接,继续进行数学堆栈交换,你&# 39;让自己有一个保证派对。

答案 1 :(得分:1)

随着应用程序大小的变化,Big O符号与计算增长有关。因此,这里有三个关键问题将决定大O符号,所有这些都与您的应用程序如何缩放有关:

  1. 随着您的应用程序扩展,您的“新”中的元素数量 数组“成长?换句话说,在你的应用程序的早期, 可能在这个“新数组”中有20个元素但是作为 应用程序有更多的用户(或数据或其他),可能是计数 成长为 n 元素?
  2. 随着您的应用程序扩展,数量 “父”数组中所有其他数组的数组增长?它可以从比较一个数组到另外两个数组(作为您的代码 上面显示)将一个数组与 n 其他数组进行比较?
  3. 作为你的 应用程序比例,“所有”中的元素的数量 '父'数组中的其他数组“增长?换句话说,何时? 与这些其他数组相比,每个其他数组都可以来自 包含,比方说,包含 n 元素的20个元素?
  4. 计算上述问题的“是”答案的数量。它们代表您的 n 。三个“是”答案表示 O(n ^ 3)

    关于Big O的更多信息。尽管看起来反直觉,但是将具有500个元素的数组与另一个500个元素的数组进行比较,如果两个数组都没有变化,则时间复杂度为 O(1)。尽管必须进行25,000次比较,但由于比较次数从未改变,因此复杂性降低到1.当您开始想象缓存结果,排序或选择非常有效的数据结构或搜索算法的可能性时,这开始变得有意义。

    现在想象一下根据固定大小的文档检查可变大小的文档(例如,拼写检查器会针对牛津英语词典中的所有171,000个单词检查文档)。该文档有 n 字样,但OED中的171,000个单词肯定会对时间复杂度产生一些影响吗?

    实际上,没有。复杂性将是 O(n),因为唯一的变量是文档的大小。当您阅读一些数据结构时,这也开始变得有意义,这些数据结构使得对于已知单词列表(例如Trie)快速单词查找非常。检查文档的时间与文档大小呈线性关系。

    如果我们将大小 n 的文档与另一个大小为 n的文档进行比较,我们会得到 O(n ^ 2)的复杂性。在这种情况下,当文档大小增加到10,000,000,000时,比较两个大小为1000的文档将不会线性扩展;如果文件的大小是预期会增长的变量,我们可能不得不重新考虑我们的方法。

答案 2 :(得分:1)

最坏情况下的操作次数对算法的时间复杂度很重要,因此循环确实是提示,但两个嵌套循环意味着答案是 O(n²)并不是必需的。算法的最坏情况是,当您的数组等于arrayEntries中的最后一个数组时,它与arrayEntries中的所有其他数组几乎相等(除了最后一个元素)。让 N arrayEntries中的数组数量, M array中的元素数量,让array.contains复杂度为 O(M)。它是 O(M),因为它是数组,因此我假设使用了简单的线性搜索。如果您将使用哈希集替换数组,则array.contains复杂性将分摊 O(1)。如果你对数组进行预排序并使用二进制搜索,那么复杂性将只是 O(log M) - 如果你将使用一些树集,则相同。您使用的数据结构实际上会影响算法的渐近性。

bool AlreadyExistingArray(Array array)
{
    bool areEqual = true;
    foreach (Array a in arrayEntries) // Loop executes N times
    {
        if (a.count != array.count)
            continue;

        foreach (int i in array) // Loop executes M times
        {
            if (!a.contains(i)) // has O(M) complexity because a.count == array.count here
            {
                areEqual = false;
                break;
            }
        }
        if (areEqual)
        {
            areEqual = false;
            foreach (int i in a) // executes M times
            {
                if (!array.contains(i)) // has O(M) complexity
                {
                    areEqual = false;
                    break;
                }
            }
        }
    }
    return areEqual;
}

这里你在另一个循环中循环,该循环调用 O(M)的操作。 所以复杂性就像:

N·M·O(M)+ M·O(M)= O(N·M²)

它将执行第一个嵌套循环 N 次,给出 N·M·O(M),然后在areEqual变为true时执行第二个循环给出 M·O(M)。我们只对这里的“最高”术语感兴趣,所以我们可以扔掉最后的 M·O(M),这会给出答案 O(N·M²) 如果arrayEntries的长度是常数,则复杂度为 O(M²)。如果array的长度不变,则复杂度为 O(N)

有些算法的 O(n)复杂度由嵌套循环表示,所以不要忘记计算操作次数,而不仅仅是循环。寻找算法的大多数难题是很重要的,因为渐近在“普通”情况和“硬”情况下实际上可能是不同的。通常的例子是快速排序算法 - 它在大多数输入上是渐近的 O(n log n)但是在数组已经排序的情况下它是 O(n²)。即使你的算法在每个输入上都具有相同的渐近 - 在“硬”情况下更容易推理它,因为这是运行时最大化并接近其渐近界限的地方。例如,如果来自arrayEntries的第一个数组是等于{{1}的数组,那么对于任何 N ,您的算法将使用 O(M²)复杂度。 }。但这并不意味着对于任何 N ,算法的总体复杂度为 O(M²)

如果您想要熟悉算法,请查阅并阅读Cormen's introduction to algorithms本书。第一章介绍了用于分析不同算法的big-O表示法。

答案 3 :(得分:0)

以更好的方式让我们举一个例子:

你有双重嵌套循环,外部循环依赖于问题大小n,但内部循环依赖于 外循环的索引变量的当前值

 for ( int i = 0; i < n; i++ ) {
     for ( int j = 0; j < i; j++ ) {
     // these statements are executed O(n^2) times
     }
}

让我们来分析这种情况下迭代,通过迭代: 在外环(I = 0)的第一迭代中,内环将迭代0次 在外环(I = 1)的第二迭代中,内环将迭代1周时间 在外环(I = 2)的第三迭代中,内环将迭代2次

在外部循环的最后一次迭代(i = N - 1),内循环将 迭代N - 1次

所以,总次数在内部循环的语句将被执行 将等于整数从1到n的总和 - 1,其是:

((n - 1)* n)/ 2 = n ^ 2/2 - n / 2 = O(n ^ 2)次

所以在你的情况下,复杂度是O(n(k + m))= O(n(n + n))= O(n ^ 2)次;

,其中

  n -> arrayentries size
  k -> array size
  m -> a size

这是因为Big-O表示法不是根据您的实际参数找到确切的值。它是关于确定渐近运行时。