【问题标题】:Efficient insertion/deletion algorithm for an array数组的高效插入/删除算法
【发布时间】:2010-09-01 19:16:25
【问题描述】:

我订阅了一个数据馈送,并使用 INSERT/DELETE 消息上的索引值创建和维护一个结构。我想问一下组装好的专家,他们是否知道任何可以有效处理零碎更新的算法——通常批量更新包含两到六个这样的消息。

该数组的估计大小约为 1000 个元素。

批量更新以按索引排序的消息列表形式到达,该列表规定在给定索引处插入或删除项目。我希望数组中的大部分流失都更接近其开始而不是结束。

我突然想到,通过一些基本的处理,我可以确定受批处理影响的范围和整体大小增量,因此只移动数组的未受影响的尾部一次。

同样,我可以在第一个元素之前和最后一个元素之后保留一定量的可用空间,以尽可能减少复制量。

其他优化包括识别更新,如下所示:

DELETE 10, INSERT 10 - effectively a replace which requires no copying  
INSERT 10, DELETE 11 - as above  
DELETE 10, DELETE 10, DELETE 10 - bulk deletion can be optimised into one copy operation  
INSERT 11, INSERT 12, INSERT 13 - bulk insertion can be optimised into one copy operation  

等等。

但是,我担心执行识别步骤的开销 - 它带有前瞻和回溯的味道,这可能比简单地执行复制需要更多时间。

鉴于数组的预期大小,树结构似乎重量级:一些基本性能测试表明二叉树或自平衡树(在本例中为红黑树列表实现)仅在大约 15K 之后才开始显示性能优势- 20K 元素:数组副本在较小的尺寸下明显更快。我可能应该补充一点,我正在使用 Java 进行此实现。

欢迎任何提示、提示或建议。

干杯

迈克

【问题讨论】:

  • 在各种 cmets 中,您都在谈论速度,但您是否对结果进行了基准测试?在处理一个使用非常频繁的列表(大约 15 个线程)时,我不小心弄乱了 delete 方法,列表增长到大约 100,000 个元素。我的应用程序仍然运行良好。我相信你的也会。

标签: java arrays algorithm performance random-access


【解决方案1】:

始终权衡代码清晰度与优化。如果现在没有性能问题,只需确保代码清晰即可。如果将来出现性能问题,那么您就会知道它的确切性质。现在为它做准备是一种猜测。

如果您需要进行大量操作,链表可能是值得的。

但是,对于简单清晰的代码,我会使用 apache commons collection utils 来处理原始数组或数组列表:

myArray = ArrayUtils.add(myArray, insertionIndex, newItem);

ArrayList<> mylist = new ArrayList<>(Arrays.asList(myArray));
myList.add(insertionIndex, newItem);

【讨论】:

  • 嗨,彼得,感谢您的回答。不幸的是,链接列表似乎不适合我们收到的按索引更新。迈克
  • @Michael:ArrayList 不是链表。它建立在一个数组上,并具有O(1) 索引。
  • 我的建议是使用数组或 ArrayList 操作,而不是链表。
  • 但我的评论旨在参考您的建议,即链接列表可能值得。 @Gunslinger,我打算反映我们需要随机访问,正如你所说,一个 ArrayList 会给我们 - 话虽如此,如果不访问底层数组,我们就无法通过在开始和结束时留下可用空间来优化,我们也不能在一次操作中识别正在发生突变的范围并块复制尾随部分。此外,我们不能对多个项目进行批量更新。
【解决方案2】:

一般来说,如果您有按索引顺序列出的更改,您可以构建一个只复制一次的简单循环。这是一些伪代码:

array items;
array changes; // contains a structure with index, type, an optional data members
array out; // empty, possibly with ensureCapacity(items.length)
int c = 0, delta = 0;
// c is the current change
//delta tracks how indexing has changed by previous operations
for (i = 0; i < items.length; i++) {
    if c < changes.length {
        curchange = changes[c]
        if (i + delta) == curchange.index {
            c++;
            if (curchange.type == INSERT) {
                out.add(curchange.data)
                delta--;
            } else {
                delta++;
                continue; // skip copying i
            }
        }
    }
    out.add(items[i])
}
for (; c < changes.length; c++) { // handle trailing inserts
    assert(c.index == out.length && c.type == INSERT)
    out.add(c.data);
}

遍历输入数组一次,然后构建包含所有更改的输出数组。

请注意,这不会处理同一位置的多个插入。这样做会使代码更复杂一点,但并不难。

但是,它始终会在整个阵列中运行,每批次一次。稍微强硬的更改是保留一个临时变量并使用两个索引变量就地进行更改;然后,如果您到达更改列表的末尾,您可以提前跳出循环而不触及列表的其余部分。

【讨论】:

  • 你的回答跟我想的差不多。
【解决方案3】:

最简单的方法是运行更新并在应用更新时将数组复制到新数组中。

1000并没有那么大,可能不值得进一步优化。

为了让您的生活更轻松,请使用ArrayList

【讨论】:

  • 我曾想过这样做,但可能必须同时携带一千个左右的模型 - 虽然盒子无疑可以应付负载,但我真的很想找到解决这个问题的“最佳”方式。感谢您的回答!
  • 如今,即使是一百万也没有那么大。如果您进一步优化,请确保对其进行分析,看看它是否真的明显更好。
【解决方案4】:

除了对各个更新进行排序(如您已经提到的)以尝试整合事物之外,我不知道我会费心。坦率地说,1000 个元素在大范围内算不上什么。我有一个包含 2500 万个元素的系统,使用简单的批量复制,它(就我们的目的而言)远远不够快。

所以,我不会戴上“未成熟优化”的帽子,但我可能会先在书架上看一眼。

【讨论】:

    【解决方案5】:

    使用链表 (java.util.LinkedList) 可能值得研究。在特定索引处获取元素当然是昂贵的,但它可能比执行数组复制更好。

    【讨论】:

    • 非常正确 - 我已经考虑过了。正如您所说,这是一个课程的例子,像跳过列表这样的结构可能会降低链表结构的索引成本。
    【解决方案6】:

    有一个非常简单的数据结构,名为“笛卡尔树”或“Treaps”,它允许对数组进行 O(log N) 的拆分、连接、插入和删除(以及更多操作)。

    2-3 棵树也很容易实现(我实现的一个稍微复杂一点的工具在第一次编译后只有一个错误)并且符合您的目的。

    【讨论】:

    • 您是否认为数据结构开销会在 1,000 个元素的典型大小上超过 ArrayList 的平坦速度?感谢您的回答,jkff,我现在正在研究这两种结构。
    • 我不确定 1000 个元素的速度,这需要进行基准测试。不过,我猜这棵树会快一些。
    【解决方案7】:

    如果空间不是约束并且您不会有重复,请选择 Set 数据结构,特别是 Java 的 HashSet。这种数据结构的强大之处在于插入和删除在 O(1) 时间内完成,如果性能是“标准”,这将最适合您。

    此外,每当您谈到数组时,除了它们的快速检索之外,您还面临着可能发生的大量数组副本的严重限制,这不仅会占用空间(用于数组增长)而且效率也会很差,因为每个插入/删除可能需要 O(n) 时间。

    【讨论】:

    • 这些是按元素插入和删除,而不是按索引。这个答案的有用性取决于迈克尔为什么使用索引。
    • 对于索引列表,他需要HashMap,而不是HashSetHashMaps 是 O(1),但有一个显着的常数。只有 1000 个元素,我倾向于认为即使是 ArrayList 也会更快。
    • 我正在使用索引值,因为这就是数据馈送所包含的内容 - 无疑有更有效的方式来描述更改,但这就是我必须使用的全部内容。更糟糕的是(我并没有给大家带来负担)列表实际上是无序的,并且索引值只有在所有先前的更新都应用于数组后才有效。
    【解决方案8】:

    如果这确实是您的数据集的样子,您可能会考虑使用 Collection(如 HashMap)进行重复跟踪。 Array 将是您按顺序列出的每个活动的有序列表,而您的 Collection 将是该数组的索引。

    例如:

    类事件队列 { 向量事件队列; 哈希映射事件映射; 公共同步事件 getNextEvent() { 事件事件 = eventQueue.remove(0); eventMap.remove(event.getId()); // 这将是 'INSERT 10' 中的 10 // 在来自 OP 的样本中 } 公共同步添加事件(事件 e) { if(eventMap.containsKey(e.getId()) { // 替换已经存在的事件 int idx = eventMap.get(e.getId()); eventQueue.removeElementAt(idx); eventQueue.add(idx, e); } 别的 { // 添加新事件 eventQueue.add(e); eventMap.add(e.getId(), eventQueue.size()); // 可能相差一个... } } 公共布尔 isReady() { 返回 eventQueue.size() > 0; } } 类 FeedListener 扩展线程 { 事件队列队列; EventFeed 提要; ... 公共无效运行() { 在跑步的时候) { 睡眠(睡眠时间); 如果(饲料.isEventReady()){ queue.addEvent(feed.getEvent()); } } } } 抽象类 EventHandler 扩展线程 { 事件队列队列; ... 公共无效运行() { 在跑步的时候) { 睡眠(睡眠时间); 如果(队列.isReady()) { 事件事件 = queue.getNextEvent(); 处理事件(事件); } } } 公共抽象无效句柄事件(事件事件); }

    【讨论】:

      猜你喜欢
      • 2014-01-23
      • 2014-01-06
      • 2014-12-23
      • 2021-06-06
      • 1970-01-01
      • 1970-01-01
      • 2017-07-17
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多