【问题标题】:ArrayList without the copying overhead?ArrayList 没有复制开销?
【发布时间】:2010-11-25 22:09:26
【问题描述】:

有谁知道List 实现具有恒定时间get(int index)(即实现RandomAccess),但当它像ArrayList 一样增长时不必复制整个列表?

我认为实现很可能是根据其他列表,例如

public class ChunkedList<T> implements List<T>, RandomAccess {
  private LinkedList<ArrayList<T>> chunks;
  public T get(int index) {
    return findCorrectChunk(index).get(computeChunkIndex(index));
  }
}

【问题讨论】:

  • 那么,您有一个列表,您需要定期扩展列表并按索引从中取出元素?
  • AFAIK,当超过当前容量时,ArrayList 的容量会翻倍。因此将有 O(log(n)) 个副本,其中 n 是列表的最终容量。这意味着需要复制整个列表的实际次数确实非常非常少。您可能会在开销变得很大之前很久就开始耗尽内存。 OTOH,如果您必须停止复制并提前知道列表必须保存的元素数量的上限,您可以简单地将最大大小作为参数传递给 ArrayList 的构造函数。
  • 您需要这个有实际原因吗?有一个基准/配置文件显示 arraylist 是你的瓶颈吗?
  • 实际原因是应用程序其他部分的设计非常糟糕。但是,用例是:将 ~1M 字符串添加到列表中,然后通过索引重复查找。 (1M 提前未知 - 取决于数据集)
  • 对于 1M 条目,假设每次容量翻倍,将有

标签: java algorithm arrays list


【解决方案1】:

如果有这样的结构,每个人都会使用它而不是数组。

但是,我认为我在大学讲座中听到的更接近的结构。它具有恒定的访问时间,将元素添加/删除到任意位置的时间大多为 O(sqrt(N)),并且只有当 N 穿过整数值的平方时,才需要 O(N)。摊销时间为 O(sqrt(N))。这就是想法。

此结构中的 N 个项目存储在一个连续数组中,该数组被划分为 sqrt(N) 个连续元素的 sqrt(N) 块(也许最后一个块包含更少的元素)。每个块都是一个ring buffer,第一个元素的位置存储在一个单独的 sqrt(N) 数组中。要访问一个元素,您应该确定它在哪个块中(需要一个除法),并在环形缓冲区内进行适当的移位(求和和模数)。这是一个恒定的访问时间。

要在第 i 个位置之前添加一个元素,请确定该元素将最终进入的块 k,然后标记 k..sqrt(N)-1 范围内每个块中的所有最后一个元素。将 pre-last 块中的标记元素移动到最后一个块中的空闲槽,该块将成为那里的环形缓冲区的头部(访问附加数组以确定确切的位置)。然后将标记的元素从 pre-pre-last 块移动到从 pre-last 块移动的元素的位置。重复此操作,您将在数组中间获得一个空闲槽,用于放置您要添加的元素。

神奇的是,您应该只将附加数组中的值增加一(花费 O(sqrt(N)) 时间),从而使结构一致以便再次访问。 sqrt(N) 的魔力也在这里:您应该对每个 X 块和辅助数组的每个 N/X 元素进行操作。对于 X = sqrt(N),达到 min(X + N/X)。

如果最后一个块中没有地方可以再添加一个元素(即目前使用的 sqrt(N) 太小),重新打包数组,将 sqrt(N) 增加一。这需要 O(N) 时间。每个元素的摊销时间仍然是 O(sqrt(N))。

因此,在数组的任意位置添加一个元素需要 O(sqrt(N))。删除需要同样的时间。访问时间为 O(1)。

就是这样。我不知道它怎么叫,教授也不知道,因为它是他自己发明的。任何参考将不胜感激。 OP 可以实现它,但我敢打赌,有人已经实现了。

【讨论】:

  • 这当然很有趣,但你必须提前知道 sqrt(N) - 虽然,你显然可以做出一个很好的猜测然后坚持下去。
  • 不,实际上你没有。我提到过:您只需使用当前的sqrt(N) 并在sqrt(N) 的整数部分发生变化时重建整个结构。重新打包需要 O(N),就像插入到通常的数组一样。但它不应该经常发生,而且平均成本(如果数组只增长)无论如何都是 O(sqrt(N))。
  • 我错过了什么吗?该解决方案的插入时间为 O(sqrt(N)),重建时间为 O(N),并且重建发生的时间不超过 sqrt(N) 次。这是否比我们有 O(1) 插入时间、O(N) 重建时间并且重建发生不超过 log(N) 次的 ArrayList 更好。
  • ArrayList 只为 将元素*添加到列表的末尾*提供摊销常数时间。我描述的结构需要 O(sqrt(N)) 将元素添加到任意位置。
【解决方案2】:

当然,您可以将列表实现编写为数组数组。关于确切的算法有很多选择。性能理论上是恒定的(忽略缓存效果等)。

在实践中,大多数情况下并没有什么意义。有绳索实现(字符串形成为段数组),但是这些相对较少。副本并没有那么昂贵,对于追加,它会在许多操作中摊销以便消失。

(顺便说一句,在问题示例代码中,LinkedList 不合适,因为它几乎总是如此。)

【讨论】:

    【解决方案3】:

    嗯,这不是一个理想的解决方案,但您可以使用 TreeMap。你的 ChunkedList 将是一个围绕它的包装器。您在 TreeMap 中的键将是 Integer 或 Long 类型,并将保存您的列表索引。访问和插入时间将为 o(log(n))(不是常数,但比 n 好得多)。在内部,TreeMap 的工作方式与 LinkedList 类似,即节点只是与引用链接。

    编辑:类似的东西:

    public class ChunkedList<T> implements List<T>, RandomAccess {
    
        private TreeMap<Integer, T> data = new TreeMap<Integer, T>();
    
        public T get(int index) {
            return data.get(index);
        }
    
        public boolean add(T o) {
            data.put(data.size() + 1, o);
            return true;
        }
    
           // Other operations
    
    }
    

    当然,其他操作会比ArrayList复杂一点,耗时也更长。

    【讨论】:

      【解决方案4】:

      您是否看过只是提示 ArrayList 构造函数的最大大小?

      【讨论】:

        【解决方案5】:

        查看随机访问列表。您可以获得 O(1) 的两端插入和 O(log(n)) 对元素的访问。

        最后,某种树状结构应该提供最佳的查找/插入时间。

        【讨论】:

          【解决方案6】:

          不可能写出这样的数据结构。假设您知道最大值,您可以获得的最接近的方法是将 ArrayList 预先设置为最大大小。有趣的是,如果将 ChunkedList 标记为 RandomAccess,则 Collections.sort() 等算法的性能会更糟

          【讨论】:

            【解决方案7】:

            这听起来像是过早的优化。您是否分析了 add() 函数并显示它很慢?因为ArrayList 每次空间不足时都会将底层数组的大小加倍,因此您不必每次追加时都复制列表。

            您可能正在尝试解决一个不存在的问题。

            【讨论】:

              猜你喜欢
              • 2011-04-29
              • 2021-08-14
              • 2012-07-04
              • 2011-12-06
              • 2011-11-12
              • 2018-02-04
              • 2019-07-01
              • 1970-01-01
              • 2021-08-13
              相关资源
              最近更新 更多