【问题标题】:Javascript ES6 computational/time complexity of collectionsJavascript ES6 集合的计算/时间复杂度
【发布时间】:2015-09-14 12:07:25
【问题描述】:

ES6 规范为键控集合(Set、Map、WeakSet 和 WeakMap)提供了多少时间复杂度(以大 O 表示法)?

我的期望和大多数开发人员的期望是规范和实现将使用widely accepted 高性能算法,在这种情况下Set.prototype.hasadddelete 都为 O(1) in平均情况。 MapWeak– 等效项也是如此。

实施的时间复杂度是否是强制性的,对我来说并不完全清楚,例如在ECMAScript 2015 Language Specification - 6th Edition — 23.2 Set Objects

除非我误解它(我当然很有可能这样做),看起来 ECMA 规范要求实现(例如 Set.prototype.has)使用线性时间(O(n) ) 算法。让我感到非常惊讶的是,规范不会强制甚至不允许更高性能的算法,我很想解释为什么会这样。

【问题讨论】:

  • 所有的 O(1) 算法也是 O(n) 的,因此引入规范性能较低的实现不会造成任何伤害。性能较差的实现可能对资源受限的系统感兴趣,因为它们很可能需要更少的代码/空间。
  • @arturgrzesiak 密钥集合的 O(1) 性能通常通过 O(1) 哈希加上 O(n) 碰撞桶来实现。 O(n) 最坏情况对于大多数实际用途而言在天文数字上是罕见的。这些技术的空间复杂度一般为 O(n)。
  • “集合对象必须使用哈希表或其他机制实现,平均而言,这些机制提供的访问时间与集合中元素的数量呈次线性关系。”——来自该页面。

标签: javascript time-complexity ecmascript-6


【解决方案1】:

问题Is the Set.has() method O(1) and Array.indexOf O(n)? 被列为此问题的重复项,但事实并非如此(我已投票重新开放)。无论如何,我都会在此处添加这些基准,因为对该问题的答复的基准未能显示Set#hasArray#indexOf 之间的全部性能差异。

以下内容适用于 Chrome 93:

您发现对于较小的数据集,Array#indexOf 实际上优于 Set#hasMap#has;但是,对于较大的数据集,Set#hasMap#has 的速度要快几个数量级。这与您对 O(n) 与 O(1) 操作的预期非常一致。

有趣的是,尽管两者都是 O(n),Array#includes 对于小型数据集比 Array#indexOf 慢得多,但对于大型数据集执行非常相似。据推测,Array#indexOf 利用了一些 Array#includes 没有的优化。

同时,Object#hasOwnProperty 在所有情况下都略优于Set#hasMap#has(至少在 Chrome 93 中)。

基准代码

const [small, medium, large] = [1e3, 1e5, 1e7]

const configs = [
    { size: small, iterations: large },
    { size: medium, iterations: medium },
    { size: large, iterations: small },
]

for (const { size, iterations } of configs) {
    const arr = Array.from({ length: size }, (_, i) => String(i))
    const obj = Object.fromEntries(arr.map(k => [k, true]))
    const set = new Set(arr)
    const map = new Map(Object.entries(obj))

    const valsToTest = Array.from(
        { length: iterations },
        (_, i) => String(Math.floor(Math.random() * size)),
    )

    const title = `dataset size: ${size.toLocaleString()}; iterations: ${iterations.toLocaleString()}`

    console.log(`\n-> ${title}`)

    for (const [target, method] of [
        [arr, 'indexOf'],
        [arr, 'includes'],
        [set, 'has'],
        [map, 'has'],
        [obj, 'hasOwnProperty'],
    ]) {
        const subtitle = `${target.constructor.name}#${method}`

        console.time(subtitle)

        for (const val of valsToTest) {
            target[method](val)
        }

        console.timeEnd(subtitle)
    }
}

我的结果(Chrome 93)


-> dataset size: 1,000; iterations: 10,000,000
Array#indexOf: 185.100ms
Array#includes: 11302.700ms
Set#has: 367.400ms
Map#has: 375.000ms
Object#hasOwnProperty: 252.800ms

-> dataset size: 100,000; iterations: 100,000
Array#indexOf: 10794.100ms
Array#includes: 10476.800ms
Set#has: 6.600ms
Map#has: 6.800ms
Object#hasOwnProperty: 1.900ms

-> dataset size: 10,000,000; iterations: 1,000
Array#indexOf: 12798.900ms
Array#includes: 12435.400ms
Set#has: 0.800ms
Map#has: 0.800ms
Object#hasOwnProperty: 0.300ms

【讨论】:

  • 您链接的问题确实问“Set.has() 真的是 O(1) 吗?”,这绝对是规范的副本。
  • @Bergi 它要求比较Set#hasArray#indexOf,而这个问题没有。如果我谷歌set.has vs array.indexof time complexity,这个问题就是第一个结果。我在这里的答案是对该问题的基于基准(而不是基于规范)的答案,并进行了一些其他比较以进行良好的衡量。不管这个问题是否被认为是重复的,希望有人觉得这个答案很有用。
【解决方案2】:

对于任何好奇的人,我做了一个非常快速的基准测试:

const benchmarkMap = size => {
  console.time('benchmarkMap');
  var map = new Map();
  for (var i = 0; i < size; i++) map.set(i, i);
  for (var i = 0; i < size; i++) var x = map.get(i);
  console.timeEnd('benchmarkMap');
}

const benchmarkObj = size => {
  console.time('benchmarkObj');
  var obj = {};
  for (var i = 0; i < size; i++) obj[i] = i;
  for (var i = 0; i < size; i++) var x = obj[i];
  console.timeEnd('benchmarkObj');
}

var size = 1000000;

benchmarkMap(size);
benchmarkObj(size);

我运行了几次并产生了以下结果:

(2017 MacBook Pro,2.5 GHz i7,带 16G RAM)

benchmarkMap: 189.120ms
benchmarkObj: 44.214ms

benchmarkMap: 200.817ms
benchmarkObj: 38.963ms

benchmarkMap: 187.968ms
benchmarkObj: 41.633ms

benchmarkMap: 186.533ms
benchmarkObj: 35.850ms

benchmarkMap: 187.339ms
benchmarkObj: 44.515ms

【讨论】:

  • @domdambrogia 如果您将设置与获取分开,我得到:Map Set = 124,Map Get = 40,Object Set = 26,Object Get = 1(这些是比率而不是 ms)
  • @AJP 我没想到,也用这些统计数据来分解它。感谢您的意见,这是一个很好的贡献。我会看看我是否可以在我有时间的时候将其添加到我的答案中。谢谢!
  • 将作业与阅读分开以了解两者中哪一个更快阅读会很有趣。
  • 2017 MacBook Pro, 2.5 GHz i7 w/16G RAM” - 嗯,这很酷,但是你对哪个 javascript 引擎进行了基准测试?
  • 有趣的是,当添加delete 操作和混合操作时,Map 的性能要好得多。 jsfiddle.net/23hrp0eq
【解决方案3】:

来自that very paragraph 您的链接:

Set 对象必须使用 [机制] 实现,平均而言,该机制提供的访问时间与集合中的元素数量呈次线性关系。

MapsWeakMapsWeakSets 会出现相同的句子。

看起来 ECMA 规范要求实现(例如 Set.prototype.has)使用线性时间 (O(n)) 算法。

没有:

Set 对象规范中使用的数据结构仅用于描述 Set 对象所需的可观察语义。它不是一个可行的实现模型。

可观察的语义主要与可预测的迭代顺序有关(仍然可以是implemented efficient and fast)。规范确实期望实现使用哈希表或类似的具有恒定访问权限的东西,尽管树(具有对数访问复杂性)也是允许的。

【讨论】:

  • 感谢您挑选出来。当我读到那段时,我的眼睛一定已经呆滞了。 :) 那么算法是 O(log(n)) 还是 O(1),但不是强制要求的(假设它们在 O(n) 之下)?
  • @BrianM.Hunt:正确。
猜你喜欢
  • 2021-03-03
  • 1970-01-01
  • 1970-01-01
  • 2011-06-21
  • 2021-08-25
  • 2021-01-08
  • 1970-01-01
相关资源
最近更新 更多