Javascript中unshift()与push()的时间复杂度

时间:2012-09-03 15:33:06

标签: javascript arrays time push complexity-theory

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

我认为push()方法是O(1),因为你只是将一个项添加到数组的末尾,但我不确定unshift()方法,因为,我想你必须“移动”所有其他现有元素都转发,我想是O(log n)或O(n)?

7 个答案:

答案 0 :(得分:51)

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

答案 1 :(得分:20)

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

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

但是如果你需要保证这样的时间限制,你必须自己动手,如下:

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

答案 2 :(得分:4)

imho它取决于javascript引擎...... 如果它会使用链表,那么unshift应该很便宜......

答案 3 :(得分:3)

对于对v8实现感到好奇的人,这里是source。由于unshift接受任意数量的参数,因此数组将自身移动以容纳所有参数。

UnshiftImpl最终以AddArguments中的start_position调用AT_START,将其踢到此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 :(得分:2)

使用快速非移位和推送实现数组的一种方法是简单地将数据放入C级数组的中间。这就是Perl的做法,IIRC。

另一种方法是使用两个独立的C级数组,因此push会附加到其中一个,而unshift会附加到另一个。我知道,这种方法对前一种方法没有任何实际好处。

无论它是如何实现的,当内部C级阵列有足够的备用内存时,推送或非移位将花费O(1)时间,否则,当必须重新分配时,至少需要O(N)次复制旧数据到新的内存块。

答案 5 :(得分:0)

是的,你说得对。 push() 的默认复杂度为 O(1),unshift() 为 O(n)。因为 unshift() 必须增加 Array 中已经存在的所有元素。但是,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()。与创建具有默认值的数组并将元素推送到该数组相比,它的内存效率更高且更简单。由于我们只使用了所需的内存量,因此不会浪费额外的内存。

答案 6 :(得分:-1)

不变动vs连贯vs点差的基准测试结果

<iframe src="https://measurethat.net/Embed?id=85784" width="100%" height="500px"></iframe>