【问题标题】:Big O of JavaScript arraysJavaScript 数组的大 O
【发布时间】:2012-07-15 21:22:59
【问题描述】:

JavaScript 中的数组很容易通过添加和删除项来修改。它在某种程度上掩盖了大多数语言数组是固定大小的事实,并且需要复杂的操作来调整大小。似乎 JavaScript 可以轻松编写性能不佳的数组代码。这就引出了一个问题:

在数组性能方面,我可以从 JavaScript 实现中获得什么性能(就大 O 时间复杂度而言)?

我假设所有合理的 JavaScript 实现最多有以下大 O。

  • 访问 - O(1)
  • 追加 - O(n)
  • 前置 - O(n)
  • 插入 - O(n)
  • 删除 - O(n)
  • 交换 - O(1)

JavaScript 允许您使用new Array(length) 语法将数组预填充到特定大小。 (额外的问题:以这种方式创建一个数组 O(1) 还是 O(n))这更像是一个传统的数组,如果用作一个预先确定大小的数组,可以允许 O(1) 追加。如果添加了循环缓冲区逻辑,则可以实现 O(1) 前置。如果使用动态扩展数组,O(log n) 将是这两种情况的平均情况。

在某些方面,我可以期待比我的假设更好的性能吗?我不希望在任何规范中概述任何内容,但实际上,所有主要实现都可能在幕后使用优化的数组。是否有动态扩展数组或其他一些性能提升算法在起作用?

附言

我想知道这是因为我正在研究一些排序算法,其中大多数在描述它们的整体大 O 时似乎假设追加和删除是 O(1) 操作。

【问题讨论】:

  • 具有大小的数组构造函数在现代 JavaScript 实现中几乎没有用处。在那个单一的参数形式中,它几乎什么都不做。 (它设置了.length,仅此而已。)数组与普通的 Object 实例并没有太大区别。
  • 设置length属性和预分配空间是完全不同的两件事。
  • @Pointy:当我期望在new Array(10) 上设置array[5] 是 O(1) 时,我是否期望过高?
  • 虽然ECMAScript没有定义一个Array对象是如何实现的(它只定义了一些语义规则),但不同的实现很可能会针对预期的情况进行优化(例如对于小于一些 n 的数组有一个“真正的数组”支持)。我对实现不是那么精通,但如果这不是在某个地方完成的话,我会真的很惊讶 ...
  • @KendallFrey “最佳答案”可能会为不同的 n / 访问模式编写一些 jsperf 测试用例,看看结果如何;-)

标签: javascript arrays algorithm big-o time-complexity


【解决方案1】:

保证

任何数组操作都没有指定的时间复杂度保证。数组的执行方式取决于引擎选择的底层数据结构。引擎也可能有不同的表示,并根据某些启发式在它们之间切换。初始数组大小可能是也可能不是这样的启发式。

现实

例如,V8 使用(截至今天)hashtablesarray lists 来表示数组。它还对对象有各种不同的表示,因此无法比较数组和对象。因此数组访问总是比 O(n) 好,可能甚至和 C++ 数组访问一样快。追加是 O(1),除非您达到数据结构的大小并且必须对其进行缩放(即 O(n))。前置更糟糕。如果您执行delete array[index] 之类的操作(不要!),删除可能会更糟,因为这可能会迫使引擎更改其表示。

建议

将数组用于数值数据结构。这就是他们的目的。这就是引擎将优化它们的原因。避免使用稀疏数组(或者如果必须,请期待更差的性能)。避免使用混合数据类型的数组(因为这会产生内部表示 more complex)。

如果您真的想针对某个引擎(和版本)进行优化,请查看其sourcecode 以获得绝对答案。

【讨论】:

  • 等一下,我们可以有混合数据类型的数组吗? Javascript 太酷了!
  • @Anurag 完全正确,但在 99% 的情况下您不需要此功能
  • @Jonas-Wilms,您能否详细说明unless you reach the size of the datastructure and it has to be scaled 部分?我们通常不会在 JS 中创建数组时设置数组长度,这是否意味着附加到任何以常规方式创建的数组(如 let a = [1,2,3])需要数据结构缩放?
  • @grreeenn 我猜不是,引擎可能会直接分配更大尺寸的数组列表
【解决方案2】:

注意:虽然这个答案在 2012 年是正确的,但今天的引擎对对象和数组使用非常不同的内部表示。这个答案可能对也可能不对。

与大多数使用数组实现数组的语言相比,在 Javascript 中,数组是对象,值存储在哈希表中,就像常规对象值一样。因此:

  • 访问 - O(1)
  • 追加 - 摊销 O(1)(有时需要调整哈希表的大小;通常只需要插入)
  • 通过unshift 前置 - O(n),因为它需要重新分配所有索引
  • 插入 - 如果值不存在,则摊销 O(1)。 O(n) 如果您想移动现有值(例如,使用 splice)。
  • 删除 - 如果您想通过 splice 重新分配索引,则需要 O(1) 分期以删除一个值,O(n)。
  • 交换 - O(1)

通常,设置或取消设置 dict 中的任何键的摊销 O(1),对于数组也是如此,无论索引是什么。任何需要对现有值重新编号的操作都是 O(n),因为您必须更新所有受影响的值。

【讨论】:

  • 前面不应该是 O(n) 吗?因为所有的索引都需要移动。插入和删除相同(在任意索引处,并移动/折叠元素)。
  • 另外,length 是设置在 Array 突变上,还是 get 会获得长度并可能记住它?
  • 值得一提的是这个答案不再正确。现代引擎不会将数组(或具有索引整数键的对象)存储为哈希表(但就像...在 C 中一样的数组),除非它们是稀疏的。让你开始here is a 'classical' benchmark illustrating this
  • 这是由标准定义的还是只是 JS 引擎中的常见实现? V8 怎么样?
  • @BenjaminGruenbaum 如果您能对它们的存储方式有所了解,那就太好了。或者提供一些来源。
猜你喜欢
  • 2015-04-04
  • 2011-04-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-09-20
  • 1970-01-01
相关资源
最近更新 更多