【问题标题】:Understanding O(1) vs O(n) Time Complexity Intuitively直观地理解 O(1) 与 O(n) 时间复杂度
【发布时间】:2017-11-13 02:31:45
【问题描述】:

我理解O(1)是constant-time,意思是运算不依赖于输入大小,O(n)是linear time,意思是运算随输入大小线性变化。

如果我有一种算法可以简单地直接进入数组索引,而不是逐个遍历每个索引来找到所需的索引,那将被视为常数时间而不是线性时间,对吗?这是教科书上说的。但是,直观地说,我不明白计算机如何以这种方式工作:计算机是否仍需要从 0 到(可能)n 逐个遍历每个索引才能找到特定的索引给它?但是,这和线性时间算法不一样吗?

编辑

我对 ElKamina 的回答详细说明了我的困惑如何扩展到硬件:

但是计算机不是必须检查它在前往的过程中的位置吗? 指数?例如,如果它试图找到索引 3,“我在索引 0 所以我需要地址 0 + 3", "好的,现在我在地址 1,所以我必须 前进 2", "好的,现在我在地址 2,所以我必须前进 1”,“好吧,现在我在索引 3”。这些和什么不一样 线性时间算法呢?电脑怎么可以不做 顺序?

【问题讨论】:

  • 这实际上更像是一个硬件或一般软件问题,因为它归结为解释内存的工作原理。我倾向于将您指向Super User,但您可能必须先重写问题,如果您谷歌它,您可能会找到一个不错的 RAM 解释。

标签: arrays algorithm memory time-complexity


【解决方案1】:

理论

假设您有一个数组,它按照事件发生的顺序存储事件。如果每个事件在计算机内存中占用相同数量的空间,您就知道该数组从哪里开始,并且您知道您感兴趣的事件编号,那么您可以预先计算每个事件的位置。

假设您想存储记录并通过电话号码键入它们。由于有很多数字,您可以计算每个数字的hash。您可能应用的最简单的散列是将电话号码视为常规号码,并将其取模为您想要存储号码的数组的长度。同样,您可以假设每条记录占用相同数量的空间,您知道记录的数量,您知道数组从哪里开始,并且您知道感兴趣的事件的偏移量。通过这些,您可以预先计算每个事件的位置。

如果数组项的大小不同,则用指向实际项的指针填充数组。然后,您的查找有两个阶段:找到适当的数组元素,然后将其跟踪到有问题的项目。

就像我们可以使用 shmancy GPS 系统来告诉我们地址在哪里,但我们仍然需要做开车到那里的工作,访问内存的问题是不知道物品在哪里,它正在到达那里。

回答您的问题

考虑到这一点,您的问题的答案是查找几乎从来都不是免费的,但也很少是 O(N)

磁带内存:O(N)

Tape memory 需要 O(N) 寻道,原因很明显:您必须卷起和解卷磁带以将其定位到所需位置。它很慢。它还便宜且可靠,因此今天仍在长期备份系统中使用。 Special algorithms 考虑到磁带的物理性质,可以稍微加快对它的操作。

请注意,根据上述内容,磁带的问题不在于我们不知道要查找的东西在哪里。问题是让物理介质到达那里。一个好的磁带算法的本质是尽量减少在一组操作中假脱机和取消假脱机的磁带总量。

说到这个,如果我们用两个较短的磁带而不是一个长磁带怎么办:这将减少点对点的旅行时间。如果我们有四盘磁带呢?

磁盘内存:O(N),但更小

硬盘驱动器通过将磁带变成一系列环,大大减少了寻道时间。现在,即使磁盘上有 N 个内存空间,只要将驱动器磁头和磁盘移动到适当的位置,就可以在短时间内访问任何一个内存空间。 (弄清楚如何用大哦符号表达这一点是一个挑战。)

同样,如果您使用更快的磁盘或更小的磁盘,您可以optimize performance

RAM:O(1),但有一些注意事项

几乎所有回答这个问题的人都会关注 RAM,因为这是程序员最常使用的东西。查看他们的答案以获得更全面的解释。

但是,简单地说,RAM 是上述想法的自然延伸。 RAM 包含 N 个项目,我们知道我们想要的项目在哪里。然而,这一次没有什么需要机械地移动来让我们到达那个项目。此外,我们发现通过使用更多短磁带或更小、更快的驱动器,我们可以更快地获得我们想要的内存。 RAM 将这个想法发挥到了极致。

出于实际目的,您可以将 RAM 视为小内存存储的集合,它们全部串在一起。您的计算机不知道特定项目在 RAM 中的确切位置,只知道它所属的集合。所以它会抓取整个集合,由数千或数百万字节组成。它将它存储在类似于 L3 缓存的东西中。

但是该缓存中的特定项目在哪里?同样,您可以认为计算机并不真正知道,它只是抓取保证包含该项目的子集并将其传递给 L2 缓存。

同样,对于 L1 缓存。

而且,在这一点上,我们已经从千兆字节(或太字节)的 RAM 增加到了 3-30 KB。而且,在这个级别上,您的计算机(最终)知道该项目的确切位置并抓取它进行处理。

这种分层行为意味着访问 RAM 中的相邻项目比随机访问整个 RAM 中的不同点要快得多。磁带驱动器和硬盘也是如此。

但是,磁带驱动器和硬盘不同,丢失所有缓存的最坏情况时间不依赖于内存量(或者,至少,非常弱依赖:路径长度,光速等)!出于这个原因,你可以将其视为内存大小的O(1)操作。

比较速度

知道了这一点,我们可以通过查看Latency Numbers Every Programmer Should Know来谈谈访问速度:

Latency Comparison Numbers
--------------------------
L1 cache reference                           0.5 ns
Branch mispredict                            5   ns
L2 cache reference                           7   ns                      14x L1 cache
Mutex lock/unlock                           25   ns
Main memory reference                      100   ns                      20x L2 cache, 200x L1 cache
Compress 1K bytes with Zippy             3,000   ns        3 us
Send 1K bytes over 1 Gbps network       10,000   ns       10 us
Read 4K randomly from SSD*             150,000   ns      150 us          ~1GB/sec SSD
Read 1 MB sequentially from memory     250,000   ns      250 us
Round trip within same datacenter      500,000   ns      500 us
Read 1 MB sequentially from SSD*     1,000,000   ns    1,000 us    1 ms  ~1GB/sec SSD, 4X memory
Disk seek                           10,000,000   ns   10,000 us   10 ms  20x datacenter roundtrip
Read 1 MB sequentially from disk    20,000,000   ns   20,000 us   20 ms  80x memory, 20X SSD
Send packet CA->Netherlands->CA    150,000,000   ns  150,000 us  150 ms

在更人性化的术语中,这些看起来像:

Minute:

L1 cache reference                  0.5 s         One heart beat (0.5 s)
Branch mispredict                   5 s           Yawn
L2 cache reference                  7 s           Long yawn
Mutex lock/unlock                   25 s          Making a coffee

Hour:

Main memory reference               100 s         Brushing your teeth
Compress 1K bytes with Zippy        50 min        One episode of a TV show (including ad breaks)

Day:

Send 2K bytes over 1 Gbps network   5.5 hr        From lunch to end of work day

Week:

SSD random read                     1.7 days      A normal weekend
Read 1 MB sequentially from memory  2.9 days      A long weekend
Round trip within same datacenter   5.8 days      A medium vacation
Read 1 MB sequentially from SSD    11.6 days      Waiting for almost 2 weeks for a delivery

Year:

Disk seek                           16.5 weeks    A semester in university
Read 1 MB sequentially from disk    7.8 months    Almost producing a new human being
The above 2 together                1 year

Decade:

Send packet CA->Netherlands->CA     4.8 years     Average time it takes to complete a bachelor's degree

【讨论】:

  • 但是 RAM 的重点是,因为在最坏的情况下会丢失所有完全不依赖于地址之间距离的缓存,所以访问总共可以被认为是 O(1) .
【解决方案2】:

任何时间复杂度计算的基础都是成本模型。成本模型往往过于简单化;例如,我们通常根据需要相互比较的元素数量来讨论排序算法的时间复杂度。

得出对数组的索引是O(1)的基本假设是随机存取内存;我们可以通过在内存总线的地址线上对 N 进行编码来访问位置 N,并且该位置的内容会返回到数据总线上。如果内存是顺序访问(例如,从磁带访问),我们会假设不同的成本模型。

【讨论】:

    【解决方案3】:

    将计算机内存想象成桶,假设您有 10 个桶。 如果有人告诉你从 8 号桶里捡东西,你不会先把手伸进 1 号到 7 号桶里,而是直接把手伸进 8 号桶里。

    数组的工作方式相同,在大多数语言中映射到某种形式的内存布局。所以例如如果你有一个 10 的字节数组,那将是 10 个连续字节。 其他类型的大小可能会有所不同,具体取决于内容是值类型/结构还是引用类型,其中数组将由指针组成。

    【讨论】:

      【解决方案4】:

      我们假设内存是“随机存取内存”(也称为 RAM),而不是磁带或磁盘内存。在 RAM 中,您可以在恒定时间内访问任何地址。有关其工作原理的更多信息,请参阅相应的 wiki 文章。

      此外,数组的元素是按顺序存储的。假设我们想在 Java 中存储占用 4 个字节的整数。如果我们想查找第 k 个元素,我们将直接查看内存中的start + 4 * k 位置。

      您也可以通过其他方式实现数组。例如,您可以使用链表实现数组,在这种情况下,访问元素需要 O(n) 时间。但这不是数组的典型实现方式。

      【讨论】:

      • 但是计算机不必检查它在索引过程中的位置吗?例如,如果它试图找到索引 3,“我在索引 0,所以我需要地址 0 + 3”,“好的,现在我在地址 1,所以我必须向前移动 2”,“好的,现在我在地址 2,所以我必须向前移动 1","好的,现在我在索引 3"。这些和线性时间算法不一样吗?电脑怎么能不按顺序做呢?
      • @ThePointer No. 阅读并尝试了解随机存取存储器 (RAM) 的工作原理。我建议从 Wikipedia 文章开始。
      【解决方案5】:

      这里没有人足够详细地解释为什么(IMO),您可以在O(1)时间内详细访问它,所以我会尝试:

      作为我之前的说明,这可能是对计算机中硬件变得多么复杂的轻描淡写,但希望它是沿着正确的道路前进的。您将在涉及硬件核心的计算机组织课程中介绍这一点。

      当你有电路时,通过计算机的电压传播得非常快,而返回的结果取决于时钟的脉冲。以这张图为例:

      https://upload.wikimedia.org/wikipedia/commons/3/3d/Square_array_of_mosfet_cells_read.png

      以下内容缺少您可以从教科书或课程(或在线)中正确学习的部分,但遗漏这些细节仍应让您有足够的高级概述,以便大致了解其工作原理:

      您作为位发送的地址将在图像的左侧上升,并且根据您发送的地址大小,电压将正确地发送到具有您想要的数据的正确存储单元。当电池接收到电压时,它会将值放回底部(这也基本上是即时的),现在您已经读取了“存储在内存中的值”,因为您想要的数据已经到达。由于电压的传播速度有多快,由于电路中电压变化的速度,您几乎可以立即得到结果。这意味着 它不依赖于遍历它之前的元素,因为你可以直接进入它,这就是 RAM 背后的想法。瓶颈来自锁存器的时钟脉冲,当您参加计算机组织课程时,您会看到我们在做什么以及为什么这样做。

      这就是为什么我们认为它在 O(1) 时间内是可行的。

      现在,操作系统和计算机组织课程将向您展示它是如何在后台连接的,为什么它比我写的更复杂(甚至可能不是那样)不再准确),但希望能让您直观了解为什么我们可以在恒定时间内做到这一点。

      由于复杂性表示法隐藏了常量(从上面我们可以假设它是常数时间去内存中的任何偏移量),那么我们可以跳转到 O(1 中的任何数组偏移量是有意义的) 从高层次的角度来看时间——这是复杂性分析旨在为我们做的——与之相比。这也是为什么我们不需要遍历内存中的每个元素来到达我们想要的地方,正如你所说的是 O(n)。

      【讨论】:

        【解决方案6】:

        假设您正在谈论的数据结构是一个向量/数组,您可以通过递增用于迭代它的任何内容来轻松达到索引“x”。

        假设您有一个结构“A”的向量,其中 A 占用 20 个字节,假设您想要到达索引 28,并且您知道该向量从内存位置“x”开始,而您只需要转到 x + 20 个字节这就是你的元素。

        对于像列表这样的数据结构,查找时间将为 O(n),因为它不是连续分配的,您必须从一个指针跳转到另一个指针。

        对于二叉树,它的 O(log2(n)) ... 等等

        所以这里的答案是它取决于你的结构。我建议您阅读一些有关基本数据结构的书籍,这些书籍可能会极大地帮助您从理论上了解您正在使用的各种概念。

        【讨论】:

          猜你喜欢
          • 2015-05-25
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-10-20
          • 2019-12-09
          • 1970-01-01
          相关资源
          最近更新 更多