【问题标题】:Time complexity of unshift() vs. push() in JavascriptJavascript 中 unshift() 与 push() 的时间复杂度
【发布时间】:2012-08-28 08:36:03
【问题描述】:

我知道 JavaScript 中的 unshift()push() 方法有什么区别,但我想知道时间复杂度有什么区别?

我想push() 方法是 O(1),因为你只是在数组末尾添加一个项目,但我不确定 unshift() 方法,因为,我想你必须“移动”所有其他现有元素都向前,我想是 O(log n) 还是 O(n)?

【问题讨论】:

  • 时间复杂度是什么意思?执行时间?
  • 通过智能稀疏数组实现,unshift 可能接近恒定时间,但我想知道是否值得让普通数组访问复杂化。我个人认为我从来没有给unshift写过电话。
  • @therao - 他的意思是 standard computer science definition 在大 O 术语中。

标签: javascript arrays time push complexity-theory


【解决方案1】:

push() 更快。

js>function foo() {a=[]; start = new Date; for (var i=0;i<100000;i++) a.unshift(1); return((new Date)-start)}
js>foo()
2190
js>function bar() {a=[]; start = new Date; for (var i=0;i<100000;i++) a.push(1); return((new Date)-start)}
js>bar()
10

function foo() {a=[]; start = new Date; for (var i=0;i<100000;i++) a.unshift(1); return((new Date)-start)}
console.log(foo())

function bar() {a=[]; start = new Date; for (var i=0;i<100000;i++) a.push(1); return((new Date)-start)}
console.log(bar());

更新

上面没有考虑数组的顺序。如果要正确比较它们,则必须反转推送的数组。但是,使用此 sn-p 在 chrome 上使用 ~10ms 对我来说,推然后反向仍然更快:

var a=[]; 
var start = new Date; 
for (var i=0;i<100000;i++) {
  a.unshift(1);
}
var end = (new Date)-start;
console.log(`Unshift time: ${end}`);

var a=[];
var start = new Date;
for (var i=0;i<100000;i++) {
  a.push(1);
}

a.reverse();
var end = (new Date)-start;
console.log(`Push and reverse time: ${end}`);

【讨论】:

  • 集合越大,差异越大,在我的机器上,macpro,使用上面@Shanti的代码,i
  • @TheHe 似乎是对的,我的第一个测试是在 Chrome 上运行的(我在上面的评论),然后我在 Safari 上的同一台机器上运行了相同的测试,push(...) 快了 10% .我没想到javascript引擎之间有如此大的差异。哼! (刚刚意识到这个 q 已经 2 岁了,Safari 已经走了很长一段路,我在 MacPro 2014 型号上使用Safari 7.1.6。)
  • 在 Chrome 48 Win10 上,unshift/shift 比 push/pop 慢 94%。
  • 如果有人好奇,使用pushshift 比使用unshiftpop 更快。
  • 在 safari 13.0 上 unshift 需要 8 毫秒,push 需要 3 毫秒
【解决方案2】:

据我所知,JavaScript 语言规范并未规定这些函数的时间复杂度。

当然可以使用 O(1) pushunshift 操作来实现类似数组的数据结构(O(1) 随机访问)。 C++ std::deque 就是一个例子。因此,使用 C++ 双端队列在内部表示 Javascript 数组的 Javascript 实现将具有 O(1) pushunshift 操作。

但如果你需要保证这样的时间限制,你将不得不自己滚动,像这样:

http://code.stephenmorley.org/javascript/queues/

【讨论】:

  • 那么 V8 的复杂性是什么?
【解决方案3】:

对于对 v8 实现感兴趣的人,这里是 source。因为unshift 接受任意数量的参数,所以数组将自行移动以容纳所有参数。

UnshiftImpl 最终以 start_positionAT_START 调用 AddArguments,这会将其踢到 else statement

  // If the backing store has enough capacity and we add elements to the
  // start we have to shift the existing objects.
  Isolate* isolate = receiver->GetIsolate();
  Subclass::MoveElements(isolate, receiver, backing_store, add_size, 0,
                         length, 0, 0);

并将其带到MoveElements

  static void MoveElements(Isolate* isolate, Handle<JSArray> receiver,
                           Handle<FixedArrayBase> backing_store, int dst_index,
                           int src_index, int len, int hole_start,
                           int hole_end) {
    Heap* heap = isolate->heap();
    Handle<BackingStore> dst_elms = Handle<BackingStore>::cast(backing_store);
    if (len > JSArray::kMaxCopyElements && dst_index == 0 &&
        heap->CanMoveObjectStart(*dst_elms)) {
      // Update all the copies of this backing_store handle.
      *dst_elms.location() =
          BackingStore::cast(heap->LeftTrimFixedArray(*dst_elms, src_index))
              ->ptr();
      receiver->set_elements(*dst_elms);
      // Adjust the hole offset as the array has been shrunk.
      hole_end -= src_index;
      DCHECK_LE(hole_start, backing_store->length());
      DCHECK_LE(hole_end, backing_store->length());
    } else if (len != 0) {
      WriteBarrierMode mode = GetWriteBarrierMode(KindTraits::Kind);
      dst_elms->MoveElements(heap, dst_index, src_index, len, mode);
    }
    if (hole_start != hole_end) {
      dst_elms->FillWithHoles(hole_start, hole_end);
    }
  }

我还想指出,v8 有一个不同的 element kinds 概念,具体取决于数组包含的内容。这也会影响性能。

实际上很难说性能是什么,因为实际上它取决于传递的元素类型,数组中有多少孔等。如果我再深入研究一下,也许我可以给出一个明确的答案,但在一般我假设因为unshift需要在数组中分配更多空间,一般你可以假设它是O(N)(将根据元素的数量线性缩放)但如果我错了请有人纠正我。

【讨论】:

    【解决方案4】:

    恕我直言,这取决于 javascript 引擎... 如果它将使用链表,那么 unshift 应该很便宜...

    【讨论】:

    • 如果 Array 是使用链表实现的,那么大多数网站上的性能都会通过地板...
    • 正确。但是对于使用链表的 unshift 操作,您会得到 O(1) 复杂度。所以这取决于用例。但大多数网站宁愿优化推送而不是 unshift。
    • 认为没有网站会优化(改变底层抽象数据类型)Array 结构?所以它完全取决于 JS-VM 的内部结构、优化和底层数据类型。
    【解决方案5】:

    实现具有快速 unshift 和 push 的数组的一种方法是将数据简单地放入 C 级数组的中间。这就是 perl 的做法,IIRC。

    另一种方法是使用两个单独的 C 级数组,以便 push 附加到其中一个,而 unshift 附加到另一个。据我所知,这种方法与前一种方法相比并没有真正的好处。

    不管它是如何实现的,当内部 C 级数组有足够的空闲内存时,push 或 unshift 将花费 O(1) 时间,否则,当必须进行重新分配时,至少需要 O(N) 时间来复制旧数据到新内存块。

    【讨论】:

      【解决方案6】:

      是的,你是对的。 push() 的默认复杂度为 O(1),unshift() 的默认复杂度为 O(n)。因为unshift() 必须递增数组中已经存在的所有元素。但是,push() 必须在数组末尾插入一个元素,因此 Array 元素的索引不必更改。但是,push() 也可以说是复杂度为 O(n),因为内存是动态分配的。在 javascript 中,当您创建一个新的 Array 而不指定所需的大小时,它将创建一个默认值的 Array。在填充默认大小之前,推送操作需要 O(1) 复杂度。但是,如果默认大小已满,编译器必须创建一个新的连续内存块,它是默认内存大小的两倍,并将已经存在的元素复制到新分配的内存中。因此,将元素从一个连续的内存块移动到另一个连续的内存块需要 O(n) 时间。

      如果您知道要放入数组中的元素数量,则可以避免插入元素的 O(n)。

      1. 用所需的大小初始化数组,并用一个虚拟值填充它。 let array = new Array(size).fill(0)
      2. 遍历要推送的元素并按其索引更改值。
      for (let i = 0; i < size; i++) {
        array[i] = i
      }
      

      因此,我们更改了元素在其位置的索引,而不是push()。与创建具有默认值的数组并将元素推送到其中相比,它的内存效率更高且复杂性更低。由于我们只使用了所需的内存量,因此不会浪费额外的内存。

      【讨论】:

        猜你喜欢
        • 2016-02-13
        • 1970-01-01
        • 2018-08-02
        • 2011-10-31
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-06-14
        • 2012-08-14
        相关资源
        最近更新 更多