Javascript 中 unshift() 和 push() 的时间复杂度比较问题

82
我知道在JavaScript中,unshift()push()方法的区别在于它们是从数组的不同位置添加元素的,但我想知道它们的时间复杂度有什么不同?
我认为push()方法的时间复杂度是O(1),因为你只是将一个元素添加到数组的末尾,但我不确定unshift()方法的时间复杂度,因为我认为您必须将所有现有元素向前“移动”,我认为这是O(log n)或O(n)?

时间复杂度是什么意思?执行时间呢? - i--
通过智能稀疏数组实现,unshift 可以接近常数时间,但我不得不想知道是否值得为了复杂化正常的数组访问而这样做。我个人认为我从来没有写过一个 unshift 的调用。 - Pointy
21
他的意思是使用大O符号来描述算法时间复杂度的标准计算机科学定义 - Nemo
9个回答

71

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());


更新

以上内容没有考虑数组的顺序。如果您想要正确比较它们,您必须颠倒被推入的数组。然而,在 Chrome 浏览器中,使用 push 然后 reverse 仍然比这个代码片段中的其他方法快约 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<150000时,unshift比数组更慢250倍以上;jsperf示例引用的是只有4个元素的数组。 - snowcode
1
@TheHe 看起来是正确的,我第一次测试是在Chrome上运行的(参见我上面的评论),然后我在同一台机器上使用Safari运行了相同的测试,“push(…)”速度快了10%。我没有预料到JavaScript引擎之间会有这样大的差异。哎呀!(刚刚意识到这个问题已经两年了,而且Safari已经取得了长足的进步,我正在使用MacPro 2014模型上的“Safari 7.1.6”。) - snowcode
在Chrome 48 Win10上,unshift/shift比push/pop慢94%。 - Chris Moschini
1
如果有人好奇的话,使用pushshift比使用unshiftpop更快。 - douggard
在Safari 13.0中,unshift需要8毫秒,而push只需要3毫秒。 - acrogenesis

28

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

当然,可以用 O(1) 随机访问来实现类似数组的数据结构,并且具有 O(1) 的 pushunshift 操作。C++的 std::deque 就是一个例子。如果Javascript实现使用C++ deques来内部表示Javascript数组,那么它将具有O(1)的pushunshift操作。

但是,如果您需要保证此类时间复杂度,您必须自己编写代码,例如:

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


13
那么在V8中的复杂性是什么? - light24bulbs

17

对于对v8实现感到好奇的人,这里是源代码。因为unshift接受任意数量的参数,所以数组会自动移位以容纳所有参数。

UnshiftImpl最终使用AddArgumentsstart_position等于AT_START的值调用它,这将引发else语句

  // 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有一个元素种类的概念,具体取决于数组包含的内容。这也会影响性能。
实际上很难说性能如何,因为它取决于传递的元素类型、数组中有多少空洞等。如果我深入探究一下,也许我可以给出一个明确的答案,但一般来说,我认为由于unshift需要在数组中分配更多的空间,在一般情况下,你可以假设它是O(N)(将根据元素数量线性扩展),但如果我错了,请纠正我。

6

是的,你说得对。默认情况下,push()的复杂度为O(1),而unshift()的复杂度为O(n)。因为unshift()必须增加数组中已经存在的所有元素。但是,push()必须在数组末尾插入一个元素,因此不需要更改任何数组元素的索引。但是,由于动态分配内存,push()的复杂度也可以称为O(n)。在JavaScript中,如果您创建一个没有指定大小的新数组,则会创建一个默认值的数组。直到填充了默认大小,推动操作需要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()。这比创建带有默认值并将元素推入其中的数组更加内存高效和简单。由于仅使用所需的内存,因此不会浪费任何额外的内存。


1
关于 .push 的 O(n) 复杂度的说法是错误的。 它实际上是摊销的 O(1)。 请熟悉摊销算法复杂度 https://dev59.com/OXVC5IYBdhLWcg3wtzyt#249695 - deorst
JS数组的值不必占用连续的内存位置,这是错误的说法。请参阅 https://dev59.com/TWIj5IYBdhLWcg3wYkNY#20323491 - JCollier

4
使用同时快速unshift和push的数组的一种实现方法是将数据直接放入C级别的数组中间。我记得perl就是这样做的。
另一种方法是有两个单独的C级别数组,使得push将数据附加到其中一个数组,而unshift则将数据附加到另一个数组。据我所知,这种方法与前一种方法相比没有任何实际的好处。
无论如何实现,当内部C级别数组有足够的空闲内存时,push或unshift将花费O(1)的时间,否则,在必须进行重新分配时,复制旧数据到新的内存块至少需要O(N)的时间。

3

在我看来,这取决于JavaScript引擎的使用情况...如果它使用了链表,unshift操作应该是相当便宜的...


18
如果使用链表实现数组,大多数网站的性能将会急剧下降 - Steven Lu
1
对于链表的push操作,时间复杂度为O(1),而对于unshift操作,也是O(1)。因此,它取决于使用情况。但大多数网站更倾向于优化push操作而不是unshift操作。 - Harry Moreno
1
你认为没有任何网站会优化(在更改底层抽象数据类型时)Array构造函数吗?因此,这完全取决于JS-VM的内部结构、优化和底层数据类型。 - TheHe

3

简短回答

  • push的时间复杂度:O(n)
  • unshift的时间复杂度:O(m + n)

其中:

  • m是现有数组的长度。
  • n是要添加的元素数量。
  • pop的时间复杂度:O(1)
  • shift的时间复杂度:O(n),其中n为数组的长度

详细回答

这是JavaScript中的一个数组数据结构实现,希望这能让事情更清晰明了。

class XArray {
  constructor() {
    Object.defineProperties(this, {
      length: {
        writable: true,
        enumerable: false,
        configurable: false,
        value: 0,
      },
    });

    /** Configure the output of the Array object to return only values */
    const runtimeConsole = console;
    console = {
      ...console,
      log: function (data) {
        if (XArray.isArray(data)) {
          runtimeConsole.log(Object.values(data));
        } else runtimeConsole.log(data);
      },
    };
  }

  /**
   * Adds element(s) to the end of the array
   *
   * Time Complexity: O(n)
   * @param  {...any} elements
   */
  push(...elements) {
    for (const element of elements) {
      this[this.length] = element;
      this.length++;
    }
    return this.length;
  }

  pop() {
    const element = this[this.length - 1];
    delete this[this.length - 1];
    this.length--;
    return element;
  }

  /**
   * Adds elements to the beginning of the array
   *
   * Time Complexity: O(m + n)
   *
   * @param  {...any} elements
   */
  unshift(...elements) {
    for (let i = this.length - 1; i >= 0; i--) {
      this[i + elements.length] = this[i];
    }
    for (const index in elements) {
      this[index] = elements[index];
      this.length++;
    }
    this.length;
  }

  shift() {
    const element = this[0];
    this.length--;

    for (let i = 0; i < this.length; i++) {
      this[i] = this[i + 1];
    }
    delete this[this.length];
    return element;
  }

  static isArray(array) {
    return array instanceof XArray;
  }
}

0

Unshift的时间复杂度为O(n)。如果我的数组是["cat", "dog"],并且我在第一个位置上unshift "moose",那么数组中剩余的所有元素都必须“移动”。也就是说,每个元素在将"moose"添加到第一个位置后,其索引都会增加。

Push的时间复杂度为O(1)。如果我的数组是["cat", "dog"],并且我将"moose"推入数组的末尾,那么所有元素都可以保持原位。


0

unshift 比 push 慢,因为它还需要在添加第一个元素后将所有元素向左移一位。所以对于 unshift,时间复杂度为 o(n),而 push 的时间复杂度为 o(1)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接