【问题标题】:What is Order of Growth and How do you compute it?什么是增长顺序以及如何计算它?
【发布时间】:2020-10-30 08:10:18
【问题描述】:

我为什么要问这个问题

我最近开始读 Scp,我已经工作了 我去Section 1.2.3。 我无法理解成长秩序的一些细节。请多多包涵 我的问题太长了。另请注意,我以前从未处理过分析算法。

我在 Scp 中读到的内容以及我对它的看法

以下是 Scp 的几段文字:

n 的确切值是多少?

n 成为衡量问题大小的参数。在我们之前的示例中,我们将 n 视为给定的数字 函数将被计算,但还有其他可能性。例如, 如果我们的目标是计算一个数的平方根的近似值, 我们可能会将 n 视为所需的位数精度。

对于牛顿法求平方根,采用Section 1.7 中给出的以下过程:

(define (sqrt x)
  (sqrt-iter 1.0 x))

这是(sqrt-iter)

(define (sqrt-iter guess x)
  (if (good-enough? guess x)
      guess
      (sqrt-iter (improve guess x)
                 x)))

good-enough? 检查guess 的近似值是否足够好

(define (good-enough? guess x)
  (< (abs (- (square guess) x)) 0.001))

现在,根据 Scp,0.001 应该是 n,但不应该在 (sqrt x) 中输入 n 吗? 迭代次数根据输入x变化,即需要的迭代次数 会根据数字的大小而变化。

这是我的 python 等效证明:

In [33]: sqrt(2)
1
1.5
1.4166666666666665
achieved 1.4142156862745097 in 3 iterations

In [34]: sqrt(4)
1
2.5
2.05
2.000609756097561
achieved 2.0000000929222947 in 4 iterations

In [35]: sqrt(1024)
1
512.5
257.2490243902439
130.61480157022683
69.22732405448895
42.00958563100827
33.19248741685438
32.02142090500024
achieved 32.0000071648159 in 8 iterations

所以0.001 不应该是 k1k2,因为它是一个恒定的值并且独立于 n(这里是我们输入到sqrt的值)?

R(n)不是恒定的吗?

R(n) 为处理大小为 n 的问题所需的资源量。 R(n) 可能 测量使用的内部存储寄存器的数量、执行的基本机器操作的数量等等。在计算机中 一次只执行固定数量的操作,所需时间为 与执行的基本机器操作的数量成正比。

在这里,Sicp 说 R(n) 可能是执行的基本操作的数量,但是 执行的操作数量不是因操作系统而异吗?作为 Linux 机器可能 执行一组步骤,另一台 FreeBSD 机器,另一台 Windows 机器? 你很快就会明白我为什么这么说。

增长顺序

我们说R(n)有增长顺序Θ(f (n)),写成R(n) = Θ(f (n ))(读作“theta of f (n)”),如果存在独立于 n 的正常数 k1k2 使得 k1 f (n) ≤ R(n) ≤ k2 f (n) 对于任何足够大的值 n. (换句话说,对于较大的 n,值 R(n) 夹在 k1 f (n)k2 f (n).)

现在,据我了解,我们通过做一些代数来计算资源的增长 从函数R(n)f(n) 接收值(R 是一个函数吗?f 我在想什么?我不知道!)

阶乘和斐波那契数列示例

例如,使用线性递归过程计算阶乘 在第 1.2.1 节中描述的步骤数与 输入 n。因此,此过程所需的步骤随着 Θ(n) 的增长而增长。我们也 看到所需的空间随着 Θ(n) 的增长而增长。对于迭代阶乘, 步数仍然是 Θ(n),但空间是 Θ(1),即常数。这 树递归斐波那契计算需要 Θ(ϕn) 步长和空间 Θ(n), 其中 φ 是 1.2.2 节中描述的黄金比例。

现在这对我来说是没有意义的部分 - 我认为一个步骤是什么? 我是否考虑使用替换模型的替换次数? 或者我是否考虑评估的表达式数量?还是我考虑什么时候 算术运算吗?

我知道在递归过程和迭代 Θ(1) 中,空间是 Θ(n)(因为解释器每次递归都需要记住 1 件事)(作为解释器,带有 x 的某个值并继续.) 我不明白斐波那契计算(树递归)如何使用 Θ(n)。

我也不知道前面的n与步数有什么关系。

我的问题

所以这里列出了我所有与成长顺序有关的问题:

  1. 算法中的哪个精确值是 n?

  2. R(n)会发生什么? (当然假设它是一个函数)n 是否除/乘一个值?

  3. 我认为一个步骤是什么?

  4. 如何计算步数和空间的增长顺序。

简而言之:

什么是增长顺序,我如何计算它?

【问题讨论】:

标签: time-complexity scheme lisp complexity-theory sicp


【解决方案1】:

你问的是一个很长的话题。我会尽量简短地回答。然而,这是一个基本的计算机科学概念,在任何算法书籍的开头,您都会找到关于增长顺序(又名 Big-O、Big-Omega 和 Big-theta 符号)的章节。如果你有兴趣,我强烈推荐这本书:https://www.amazon.ca/Introduction-Algorithms-Thomas-H-Cormen/dp/0262033844

为了回答您的问题,科学家无法比较他们的代码,因为原子操作在不同的机器上需要不同的时间。大量因素会影响代码在机器上的运行时间,例如操作系统负载、操作系统调度、在 CPU 上实现的原子操作等。因此,他们提出了增长顺序的理论定义。

有一件事肯定会影响运行时间,那就是代码输入的大小,通常由n 注明。由于n 可能非常大,因此这些符号也称为渐近符号。所以,当我们谈论增长的顺序时,我们假设n 是任意大的(我们不关心小的输入大小)。简单地说,增长的顺序是您的程序执行的原子步骤(也称为基本步骤)的数量。什么是原子的?任何需要 1 或 2 或恒定数量的 CPU 操作的操作(通过常数,我的意思是它不依赖于n,输入大小)。让我给你举个例子。 这段代码:

c = a + b

有一个原子步骤,它有一个求和和一个赋值。 另一个例子:

for i in 1..n:
   print(i)

此代码有一个原子步骤print(i) 并重复它n 次(让n 成为输入大小)。所以我们说这个程序有n原子操作(即它的增长顺序是O(n))。

所以,到目前为止,我希望您了解什么是原子操作以及增长的顺序(原子步数)是什么。但是,通常情况下,计算增长的顺序并不容易,并且涉及大量数学。例如这段代码:

for i in 1..n:
   for j in i..n:
      print(i+j)

在这段代码中,由于j 依赖于i,我们将拥有n + (n-1) + ... + 1 = n*(n+1)/2 原子操作。如果您计算该公式,则它是n^2 + ...。由于n^2 在结果中具有最大的指数,因此我们只关心该项。在非常大的输入大小中,该术语占主导地位,我们说它的增长顺序是O(n^2)(我知道很多细节都丢失了)。 因此,当我们说程序 A 的增长顺序为 O(n) 而程序 B 的增长顺序为 O(n^2) 时,我们可以肯定,对于较大的输入大小,程序 B 将比程序 A 慢得多(我们终于可以不用在机器上运行来比较代码了)。

总而言之,增长的顺序是当输入大小很大时原子操作的数量,我们不关心小操作,我们只关心最大的操作块。

我在这里的话可能会冒犯科学家和工程师。致我的科学家朋友们:当我在这里解释增长顺序时,我在数学上并不准确(如果你关心确切的数学定义,请阅读我提到的书)。致我的工程师朋友们:是的,科学家们在计算Big-o 时忽略的那些小步骤实际上在实践中很重要,但科学家需要简化以建立基础,然后讨论细节。

【讨论】:

  • 我明白你在说什么 - 增长顺序基本上是计算原子步骤的数量。我们也取方程的度数……但是,for i in 1...?我不太明白您所说的1... 是什么意思,我假设您的意思是range(n)。您的回答很有帮助,谢谢。
  • 是的,1..n,我的意思是范围(n)。
【解决方案2】:

如果您想编写有效的代码,您必须了解算法的增长顺序。互联网上有很多关于这个主题的优秀资源,有些在数学上比其他的更准确,但这是我自己的数学上不准确的解释:

假设您有一些数据,例如来自数据库的 100 行,您想要处理它。现在,您可以使用这些数据实际做什么?

也许您只想打印第一行。这有多难?嗯,不是很难。您只需要一个操作即可打印(first_row)。如果你有 1000 行,你仍然可以只用一个操作打印第一行,这种情况意味着不管你处理多少数据,它仍然会花费你相同(恒定)的时间, O(1).

那么我们有另一个非常重要的增长顺序O(log(n))。这对于在排序数据中搜索的算法来说是典型的。如果您有 100 行已排序的名称,并且您正在寻找某个名称,则只需要进行几次比较。每次比较时,您将数据切成两半,在 6 次操作中找到最坏情况下的名称。如果您正在查看 1000 个已排序的行,则大约需要 9-10 次比较。这在搜索大数据时非常有效,您不必担心在排序数据的大数据集中搜索。它总是非常快。

现在,如果您想打印所有数据,则必须在每一行上调用 print。对于 100 行,您必须执行 100 次操作,对于 1000 行,您必须执行 1000 次操作。这意味着您需要的工作量与您正在处理的数据量成正比。这将是 O(n)

O(n*n) 例如,当您尝试在数据中查找重复行时。为此,您需要将每一行与其他每一行进行比较。因此,您必须对 100 行进行 10 000 次比较,对 1000 行进行 1000 000 次比较!正如您在这种情况下所看到的,所需的操作数量开始快速增长,并且已经是一种增长,可能会限制您在现代计算机上可以在几秒钟内处理的数据量(您通常想要的)。这就是为什么当你看到两个 for 循环相互出现时,你必须开始小心了。这也称为多项式增长。

O(e^n) 是指数增长,而且增长非常、非常、非常……非常快。考虑一种情况,您有 100 行随机数。如果您要找到这些数字的所有子集,总和为 50,则需要进行 1267650600228229401496703205376 次操作。对于 1000 行,它将是一个包含 300 个零的数字。这对于作为程序员的你来说意味着,如果你试图处理 1000 个这样的数字,你永远不会得到结果。

O(n!) 是一种增长更快的阶乘复杂度,您不会使用这种算法处理任何大数据集。一个典型的例子是用蛮力搜索解决旅行商问题。这意味着如果有 100 行包含 100 个城市,并且您想知道访问它们的顺序是什么,那么您将采用最短路径来计算每种可能的旅行组合的距离,并且它将意味着执行一些包含 157 个零的操作,而对于 1000 个城市,该数字将包含数千个零。这是一个你无法想象的疯狂数字。考虑一下宇宙中大约有 10^78 到 10^82 个原子!换句话说,您可以在您的计算机上为几个城市优化推销员的路径。

您应该能够根据它们的增长速度对增长顺序进行排序。将其绘制成图表并在您将编写一些算法时始终考虑这张图片是很有帮助的一个循环到另一个循环。

【讨论】:

  • 提一下会很有帮助 - 甚至更好的展示! - 对数图更容易比较多项式复杂性,以及对数图,用于指数(即 X-logY;甚至提到 logX-Y,用于 log(n) 周围的复杂性。:)
  • @Najiva 谢谢。但是,您的回答与 Amin 向我解释的原子步骤无关。但是我明白你的意思是随着某些程序消耗的资源增加的速度
  • 我认为公认的智慧是二次真的“坏”,线性是“好”,线性是“好”,对数是“优秀”。 “公平”我会称之为 n^1.5 左右的东西。肯定多项式和指数是两个不同的类别。
猜你喜欢
  • 2021-11-19
  • 2013-04-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-03-21
相关资源
最近更新 更多