【问题标题】:Immutable / persistent list in JavaJava中的不可变/持久列表
【发布时间】:2011-07-30 17:39:00
【问题描述】:

作为一个宠物项目,我试图在 Java 中实现一个不可变的列表数据结构,同时尽可能减少副本;我知道 Google 收藏,但这不是我所追求的,因为列表操作只会返回旧列表的新副本。

我想出了两种不同的方法来解决这个问题;两者都基于双向链表,如下所示:

[head: element1] <--> [element2] <--> [tail: element3]

所以每个列表都由元组{head, tail}组成。


首先,让我们研究将元素附加或前置到列表A 的简单情况,从而生成列表B

A:                 [head: element1] <--> [element2] <--> [tail: element3]
B: [head: element0] <--> [element1] <--> [element2] <--> [tail: element3]

这是 O(1)。由于对列表的迭代只发生在之间,A 不会知道任何关于添加到B 的新元素。

当我们尝试在列表中插入或删除任意元素时会变得很有趣。

索引元素方法

每个列表都有一个从 0 开始的唯一顺序 id。每个元素都有一个 {prev, next} 指针数组,对应于列表 id:

  [element1] <--> [element2] <--> [element3] <--> [element4]
A:   [0] <---------> [0] <---------> [0] <---------> [0]
B:   [0] <---------> [1] <-------------------------> [1]
C:   ...

因此,当从 id = 0 的列表 A 中删除 element3 时,element2element4 的 id = 1(列表 B)的 prevnext 指针分别是更改以反映请求操作的结果; element1 保持不变。当迭代索引为x 的列表时,为了获得正确的prevnext 指针,max(elementIdCount, x) 用于计算正确的索引(对于element1 为0,对于@987654343 为1 @如果我们迭代 B id = 1,例如)。

添加或替换元素以相同的方式完成。这也是 O(1),除非需要调整元素 id 数组的大小,这应该很少发生。

最大的问题当然是垃圾回收——一旦一个元素被添加到一个列表中,它永远不会被释放,直到所有对原始列表的修改版本的引用都被释放。这可以通过每 10 次修改复制整个列表来解决。

这种列表特别适合这样的代码结构:

while (...)
    list = list.addElement(...);

因为在任何给定时间只保留一个对列表的引用。

迭代器方法

另一种方法是滥用迭代器以使结果列表看起来像预期的修改版本;因此每个修改后的不可变列表都包含对其“源”列表的引用和一个附加元组{operation, element, position},如下所示:

A: [head: element1] <--> [element2] <--> [tail: element3]
B: source: A, {add, element_to_add, 1}

B 的迭代器然后调用它的源列表迭代器(在本例中为 A 的),除非它遇到已修改(添加、删除或替换)的元素,在这种情况下它返回该元素然后再次使用源迭代器。

这里明显的缺点是嵌套迭代器的深度随着列表的每个修改版本而增长。这意味着不时地制作原始副本也是必要的。

有人对如何改进有任何建议吗?此外,任何指向 60 年代发明的任何可能有用的数据结构的指针都非常受欢迎:)

【问题讨论】:

  • 如何将两次(不同)元素附加到 A 在您的双向链表中起作用?从 A 创建 C 时,您会破坏 B。
  • @Paŭlo Ebermann - 这确实是一个问题,即使你不破坏 B 而是从 A -> B 到 C 的变化。我认为这可以通过不检查元素来解决sourceListId 和 x 之间的索引...

标签: java algorithm functional-programming linked-list immutability


【解决方案1】:

您可以创建一个 head::tail 类似列表,并获得易于创建和良好内存占用的好处,然后提供一个在顶部分层 skip list 的 API,以便在需要时获得有效的随机访问。

就中间的有效变异而言,跳过列表视图可能有一个侧表将变异索引映射到元素,以及一个二进制可搜索数组,将原始索引映射到插入和删除后的索引偏移量。

所有这些映射都提出了一个问题,即如何为高效的某些定义提供高效的不可变映射。我想出的最好方法是使用b trees,它允许 O(log n) 访问可排序的键,并在插入和删除时创建 O(log n) 节点。经过 k 次修改后,基于 b 树的映射的两个持有者之间共享的节点数约为。 (n - k log n) 这对于不经常更新的地图在实践中非常有用。

【讨论】:

    【解决方案2】:

    不可变列表意味着您不能在创建后修改列表项。所以你在滥用符号。您要做的是:拥有一个可变列表并返回其不可变视图。

    Google Guava 可以为您返回不可变视图。

    ImmutableList<T> view = ImmutableList.copyOf(mutableList);
    

    如果您想尽可能减少副本,您可以在请求新视图之前对您的mutableList 进行多次更新。

    【讨论】:

    • 出于所有意图和目的,即使内部实现本身是可变的,它对外部世界也是不可变的,是的。但是在某处你确实必须开始改变状态。
    • 这仍然会复制整个列表(例如,需要 O(size) 操作)。
    • 如果不想复制,请使用 Collections.unmodifiableList。
    猜你喜欢
    • 1970-01-01
    • 2020-05-27
    • 1970-01-01
    • 2011-12-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-07-22
    • 1970-01-01
    相关资源
    最近更新 更多