理解大 O 表示法的扩展定义

Posted

技术标签:

【中文标题】理解大 O 表示法的扩展定义【英文标题】:Understanding the Expanded Definition of Big O Notation 【发布时间】:2022-01-12 19:37:45 【问题描述】:

我了解 Big O 总体上要实现的目标(某事物的最坏情况运行时)及其重要性,但我对它以更复杂的术语表达的方式感到困惑。举个例子(但我到处都看到类似的):

0 =n0

来源:https://www.geeksforgeeks.org/difference-between-big-oh-big-omega-and-big-theta/

认为我知道在这种情况下 f(n) 是什么:f 是我们要测试的函数; n 是该函数的输入; f(n) 的输出是运行时间。 f 和 n 本身是函数的通用占位符和将在实践中使用的输入,而不是细节(例如 f(n) 中的 n 并不意味着线性时间复杂度只是因为 n是 n^1;它只是用来表示输入到函数中的任何东西,在这个抽象定义中,它可以是(并导致)任何东西。

    到目前为止是正确的吗?

    开头的0是什么?那只是说“必须至少有一行代码可以运行”吗?也就是说,首先要运行一些东西?

    什么是g()?如果 f(n) 是我想出的,那么 g() 是我需要想出的吗?

    我知道 c 在这种情况下的意思是“常数”,但什么是常数,从哪里来?是与输入大小无关的指令数吗?

例如。如果我们正在查看以下函数的时间复杂度:

function example (int x)
    x = x * 2
    for (int i = 0, i <= x, i++)
        println(x);
        x += 1;
        x -= 1; // functionally arbitrary; just so I can wrap my head around it
        println("Testing");
    
    println("I'm done now");

这些行中的任何一行都计入 c 的值吗?哪一个?还是我完全不合时宜?

    n0 是什么?没有输入?最小的输入?输入乘以0?输入 n 时的第一个条目是零索引数组? n0 甚至与 n 有关吗?

一旦我了解了每个部分是什么,我想我就可以理解所表达的内容了。在那之前,我有点失落!

【问题讨论】:

【参考方案1】:

符号是 f(n) = O(g(n)) 其中:

f() 是我们正在研究的算法的执行时间 n 是算法输入的大小或数量级 g() 是另一个表示时间的函数

因此,例如,如果我们的算法导致对数组进行排序,f(4) 将是对长度为 4 的数组进行排序所花费的时间。这个函数显然不仅取决于算法和 n,还取决于在我们作为输入提供的特定数组上。这里“大 O”就派上用场了。

f(n) =n_0)

现在您会注意到您的公式也有一个常数“c”。这只是一个比例因子,意味着在表示法中写 O(n) 或 O(100*n) 没有区别。

现在,f(n) = O(n) 意味着对于任何给定的 c AND 对于 n>=n_0,在给定 n 处的执行时间总是小于 c*n。例如 50*log(n) = O(n) 且 n_0 = 100。从 n_0 开始 f() 总是更小,因为它增长得更慢

零只是因为它指的是需要正执行时间的实际算法。

【讨论】:

谢谢,帮了大忙!我想我更接近了,但有几件事我不明白。最大的可能是 f() 和 g() 之间的区别,特别是因为它们都在 n 上运行并且是关于运行时的。你能解释一下是什么让它们与众不同吗?考虑到正在测试的细节,f() 是否是实际最差的运行时间,而 g() 通过纯粹抽象地查看输入的大小是假设的最差运行时间? @PaoloJ42 其次,如果 c 可以取代输入的比例(因为幅度是输入最重要的因素),为什么只用 g() 提到它?它不会与 f() 一起出现在某处,例如。 f(c*n)?最后,我想我开始得到 n0,但我真的不知道该怎么说。它在我链接的页面上的图表中,并显示了 f(n) 始终小于或等于 cg(n) 的点。是不是说,在某些时候,算法的时间复杂度永远保持不变,但对于某些算法,可能存在一些输入大小会破坏预期的时间复杂度? 很抱歉,如果这会通知您两次;最初没有在我的第一条评论中提及,并且不确定编辑是如何工作的! @PaoloJ42 @vfr4bgt5nhy6 我不知道你是否熟悉它,但这个符号与数学极限有一些共同的概念。目的不是在输入复杂度(即 n)变得非常大时给出限制运行时的值,目的是在 n 增长时找到 TIME GROWTH BEHAVIOR 的上限。 g() 表示无论 c 的尺度是多少,增长速度都快于 f 的函数族。 也就是说,n总是小于20*n,但是增长是一样的,所以它不满足n = O(20n),而保持n=O(n^2)。 c 可以在等式 c1*f...c2*g 的两边,但定义 c=c2/c1 并且你得到相同的结果。当然,有一些非常具体的输入可能与建模的 f() 有很大的偏差,但这些通常是孤立的极端情况,与这种评估无关【参考方案2】:

This post 可能只是回答 f(n) = O(g(n)) 在概念上代表什么,以及您提到的正式定义表达该概念的方式。

直接解决你的一些观点:

    是的,f(n) 可以是 n 的任何函数,其中 n 代表算法输入的 大小,f(n) 给出算法的运行时间n.大小在不同的上下文中可能意味着不同的东西,但输入的任何方面都可能导致算法的运行时间发生变化。它可以是排序算法的数组大小,也可以是回文检查器的输入字符串的长度等。

    0 只是表示任何有效的时间复杂度函数 f 或 g 都应该是非负的。对于任何输入大小 n,没有算法在负时间运行。

    通常你说一个函数 f 属于另一个函数 g 所代表的复杂度类。因此,就输入大小 n 而言,g 实际上可以是执行时间的任何其他函数。有不同种类的复杂度类,但对于“Big O”,f(n) = O(g(n)) 意味着 f(n) 不超过 g(n) 的常数倍,因为 n 趋于无穷大.或者,您可以说 f(n) 不会比 g(n) 增长得更快,因为 n 变得任意大。因此,如果您说算法是 O(n^2),那么您所声称的是它在给定输入大小 f(n) 上的执行时间不会超过 n^2 超过一个常数因子随着 n 变得任意大。

例如,假设您有一个算法,它对您传递给它的输入数组的每个元素执行 2 次操作(乘法,然后对结果数组进行赋值):

function squareValues(array) 
  let result = [];
  for (let [i, value] of array.entries()) 
    let squaredValue = value * value;
    result[i] = squaredValue;
  
  return result; 

console.log(squareValues([1,2,3,4]));

您可以将此算法的运行时间表示为 f(n) = 2n 的输入大小的函数,其中 n 是数组的长度(它对输入数组的每个元素执行 2 次操作)。您可以更准确地说 f(n) = 2n + 2,因为它每次运行时确实会执行 2 次额外操作(在第一行创建结果数组,然后在最后一行返回它),但这不会不会影响它的大 O 复杂度等级。

然后你可以说 f(n) = O(n) (这里是 g(n) = n),因为 f(n)=2n 不会比 g(n)=n 增长得快无穷大(当 n 趋于无穷时,2n 保持在 n 的常数倍数 2 内)。我们也可以说 f(n) = 2n + 2 在 O(n) 中,因为随着 n 变得任意大,f(n)=2n+2 仍然大约只比 g(n)=n 大 2 倍。类似地,你可以说 f(n) = O(n^2),因为当 n 变得任意大时,2n 的增长速度不会比 n^2 快(当 n 趋于无穷大时,n^2 的增长速度比 2n 快)。

    (和 5:) 正式定义中的常数 c 与语句 f(n) = O(g(n)) 表示 f 的增长速度不超过一个常数因子比 g 快的想法有关无穷。对所有 n >= n0 说 f(n) = n0 部分只是一种说法,即当 n 趋于无穷时,该陈述为真(因为对于大于或等于某个特定 n 值 n0 的所有 n,它都是真的)。​​

使用我们的示例,f(n) = 2n + 2, g(n) = n。 f(n) = O(g(n)) 因为

2n+2 <= 3n for all n > 4

我们展示了 f(n)=2n+2 的增长速度不超过 g(n)=n 的常数倍,因为 n 使用 c=3 和 n0=4 趋于无穷大。因为对于每个大于 4 的 n 值,f(n) 小于 g(n) 的 3 倍,所以当 n 趋于无穷大时,f(n) 的增长速度不会超过 g(n) 的常数倍(3 倍)。因此 f(n) = O(g(n)): 2n+2 = O(n)。然后你会说我们的算法,它的运行时间我们用输入数组长度 n 表示为 f(n)=2n+2,是 O(n)。

【讨论】:

以上是关于理解大 O 表示法的扩展定义的主要内容,如果未能解决你的问题,请参考以下文章

大O表示法

大O表示法

不理解大 O 表示法 O(∑ i=0 n−1 (∑ j=i+1 n (j−i)))=O(∑ i=0 n−1 2 (1+n−i)(n− i))=O(n^3)

大O表示法总结

算法复杂度表示(大O表示法)

复盘笔记二分查找和大O表示法