Javascript:有效地将项目移入和移出固定大小的数组

时间:2018-01-02 12:19:17

标签: javascript arrays caching

如果我想要一个固定大小 N 的数组用于缓存最新的 N 项目,那么一次限制 N 已达到,我必须删除最早的项目,同时添加最新项目。

注意:我不关心最新项目是否在数组的开头或结尾,只要项目按照添加顺序删除。

显而易见的方法是:

  • push()shift()(以便cache[0]包含最旧的项目)或
  • unshift()pop()(以便cache[0]包含最新项目)

基本理念:

var cache = [], limit = 10000;

function cacheItem( item ) {

    // In case we want to do anything with the oldest item
    // before it's gone forever.
    var oldest = [];

    cache.push( item );

    // Use WHILE and >= instead of just IF in case the cache
    // was altered by more than one item at some point.
    while ( cache.length >= limit ) {
        oldest.push( cache.shift() );
    }

    return oldest;
}

但是,我已经阅读了shiftunshift的内存问题,因为它们会改变数组的开头并移动其他所有内容,但不幸的是,一个必须使用这些方法来做到这一点!

Qs的:

  1. 还有其他方法可以做得更好吗?
  2. 如果我提到的两种方式是最好的,我需要注意哪些特定的优点/缺点?

  3. 结论

    在对数据结构做了一些更多的研究之后(我从来没有在其他语言中编程,所以如果它不是Javascript原生的,我可能还没有听说过它!)并在多个浏览器中进行一系列基准测试小型和大型数组以及大量的读/写,这是我发现的:

    • Bergi提出的'循环缓冲'方法是最好的表现(由于答案和评论中解释的原因),因此它已被接受为答案。但是,它不那么直观,并且很难编写自己的“额外”功能(因为您总是需要考虑offset)。如果您要使用此方法,我建议使用已创建的方法,例如this circular buffer on GitHub
    • 'pop / unpush'方法更直观,表现相当好,接受最极端的数字。
    • 遗憾的是,'copyWithin'方法对性能很糟糕(在多个浏览器中测试过),很快就会产生无法接受的延迟。它也没有IE支持。这是一个如此简单的方法!我希望它能更好。
    • Felix Kling的评论中提出的'链表'方法实际上是一个非常好的选择。我最初忽视它,因为它似乎是我不需要的许多额外的东西,但令我惊讶的是......

    我实际需要的是Least Recently Used (LRU) Map(使用双向链表)。现在,由于我没有在原始问题中指定我的额外要求,我仍然将Bergi的答案标记为该特定问题的最佳答案。但是,因为我需要知道缓存中是否已经存在某个值,如果是这样,将其标记为缓存中的最新项,我必须添加到循环缓冲区的add()方法中的附加逻辑(主要是{ {1}})使它不比'pop / unpush'方法更有效。然而,在这些情况下,LRUMap的性能会使其他两种水中的水都爆炸!

    总结一下:

    1. 链接列表 - 大多数选项,同时仍保持良好的性能
    2. 循环缓冲区 - 仅添加和获取
    3. 的最佳性能
    4. Pop / Unpush - 最直观,最简单的
    5. indexOf() - 目前表现糟糕,没有理由使用

3 个答案:

答案 0 :(得分:5)

  

如果我有一个缓存最近N个项目的数组,一旦达到限制N,我将不得不在添加最新项目时删除最旧项目。

您不希望复制数组中的内容,每次都需要O(n)步。

相反,这是ring buffer的完美用例。只需保留列表的“开始”和“结束”的偏移量,然后使用该偏移量访问缓冲区并以其模数为其模数。

var cache = new Array(10000);
cache.offset = 0;

function cacheItem(item) {
    cache[cache.offset++] = item;
    cache.offset %= cache.length;
}
function cacheGet(i) { // backwards, 0 is most recent
    return cache[(cache.offset - 1 - i + cache.length) % cache.length];
}

答案 1 :(得分:1)

您可以使用Array#copyWithin

  

copyWithin() 方法浅析将数组的一部分复制到同一数组中的另一个位置并返回它,而不修改其大小。

     

<强>描述

     

copyWithin的工作方式类似于C和C ++的memmove,是一种用于移动Array数据的高性能方法。这尤其适用于同名的TypedArray方法。序列被复制并粘贴为一个操作;即使复制和粘贴区域重叠,粘贴的序列也将具有复制的值。

     

copyWithin函数故意泛型,它不要求其值为Array对象。

     

copyWithin方法是一种可变方法。它不会改变this的长度,但会在必要时更改其内容并创建新属性。

var array = [0, 1, 2, 3, 4, 5];

array.copyWithin(0, 1);

console.log(array);

答案 2 :(得分:1)

您需要splice现有并使用unshift(作为最新项目)将其放在前面中。如果您的缓存中已存在,那么您可以unshiftpop

function cacheItem( item )  
{
    var index = cache.indexOf( item ); 
    index != -1 ?  cache.splice( index, 1 ) : cache.pop();
    cache.unshift( item );
}

必须是StringNumber,否则您需要使用{{1}编写自己的indexOf实现定位和对象(如果 item 是一个对象)。