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

uajslkp6  于 2023-03-06  发布在  Java
关注(0)|答案(9)|浏览(205)

我知道JavaScript中unshift()push()方法之间的区别,但我想知道时间复杂度的区别是什么?
我假设对于push()方法是O(1),因为你只是在数组的末尾添加一个元素,但是对于unshift()方法我不确定,因为我假设你必须向前移动所有其他的元素,我假设这是O(log n)还是O(n)?

pqwbnv8z

pqwbnv8z1#

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

更新

上面没有考虑数组的顺序,如果你想正确比较它们,你必须反转push数组,但是push then reverse在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}`);
u7up0aaq

u7up0aaq2#

据我所知,JavaScript语言规范并没有规定这些函数的时间复杂度。
当然可以用O(1)pushunshift操作来实现一个类似数组的数据结构(O(1)随机访问)。C++ std::deque就是一个例子。因此,使用C++ deque在内部表示Javascript数组的Javascript实现将具有O(1)pushunshift操作。
但是如果你需要保证这样的时间界限,你就必须自己滚动,就像这样:
http://code.stephenmorley.org/javascript/queues/

x6h2sr28

x6h2sr283#

对于那些对v8实现感兴趣的人,这里是源代码,因为unshift接受任意数量的参数,所以数组会自动移位以容纳所有参数。
UnshiftImpl最终使用AT_STARTstart_position调用AddArgumentsAT_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有一个不同的element kinds的概念,这取决于数组包含的内容,这也会影响性能。
实际上很难说性能如何,因为实际上它取决于传递的元素类型、数组中有多少个洞等。如果我深入研究这些问题,也许我可以给予一个明确的答案,但一般来说,我假设由于unshift需要在数组中分配更多空间,一般来说,你可以假设它是O(N)(将根据元素的数量线性缩放),但是如果我错了,请有人纠正我。

mpbci0fu

mpbci0fu4#

是的,你是对的。push()的默认复杂度是O(1)且unshift()为O因为unshift()必须递增已经存在于数组中的所有元素。但是,push()必须在数组的末尾插入元素,所以数组元素的索引都不必改变。但是,push()也可以说是复杂度为O(n)因为动态分配内存。在javascript中,当你创建一个新的Array而没有指定你需要的大小时,它会创建一个默认值的Array。在默认大小被填满之前,push操作需要O(1)复杂性。但是,如果默认大小是满的,编译器必须创建一个新的Contiguous内存块,其大小是默认内存的两倍,并将已经存在的元素复制到新分配的内存中。因此,将元素从一个Contiguous内存块移动到另一个Contiguous内存块需要O(n)时间。
如果你知道你要放入数组的元素的个数,你就可以避免插入一个元素的时间复杂度为O(n)。
1.用所需的大小初始化Array,并用一个伪值填充它。let array = new Array(size).fill(0)
1.迭代您要推送的元素,并通过其索引更改值。

for (let i = 0; i < size; i++) {
  array[i] = i
}

因此,我们改变了元素在其位置的索引,而不是push()。这比创建一个具有默认值的数组并将元素推送到该数组中要节省更多内存,也更简单。由于我们只使用了所需的内存量,因此不会浪费额外的内存。

utugiqy6

utugiqy65#

恕我直言,这取决于javascript引擎...如果它将使用一个链表,unshift应该是相当便宜...

xv8emn3q

xv8emn3q6#

一种同时实现快速unshift和push的数组的方法是简单地将数据放入C级数组的中间,这就是perl的做法,IIRC。
另一种方法是使用两个独立的C级数组,这样push会对其中一个进行追加,而unshift会对另一个进行追加,据我所知,这种方法与前一种方法相比没有什么真实的的好处。
不管它是如何实现的,当内部C级数组有足够的空闲内存时,推送或和去移位将花费O(1)时间,否则,当必须进行重新分配时,将旧数据复制到新的内存块至少需要O(N)时间。

pepwfjgg

pepwfjgg7#

简短回答

  • 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;
  }
}
k3fezbri

k3fezbri8#

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

1aaf6o9v

1aaf6o9v9#

如果我的数组是["cat", "dog"],并且我把"moose"移到第一个位置,那么数组中剩下的所有元素都必须“移动”。也就是说,每一个元素在把"moose"加到第一个位置时,索引都会增加。
如果我的数组是["cat", "dog"],我把"moose"推到数组的末尾,那么所有的东西都可以保留在原来的位置。

相关问题