作为一个宠物项目,我正在尝试用Java实现不可变列表数据结构,同时尽量减少副本;我知道Google Collections,但这不是我所追求的,因为列表操作只返回旧列表的新副本。
我已经提出了两种不同的方法来解决这个问题;两者都基于双链表,如下所示:
[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。每个元素都有一个对应于列表ID的{prev, next}
指针数组:
[element1] <--> [element2] <--> [element3] <--> [element4]
A: [0] <---------> [0] <---------> [0] <---------> [0]
B: [0] <---------> [1] <-------------------------> [1]
C: ...
因此,当从id = 0的列表element3
中删除A
时,{id}分别为prev
或next
指针(列表B
)element2
和element4
被改变以反映所请求的操作的结果; element1
保持不变。迭代索引为x
的列表时,为了获得正确的prev
或next
指针,max(elementIdCount, x)
用于计算正确的索引(对于如果我们在element1
上迭代id = 1,则element2
和B
为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年代发明的任何可能有用的数据结构的指针都非常受欢迎:)
答案 0 :(得分:2)
您可以创建一个head :: tail之类的列表,并获得易于创建和良好内存占用的优势,然后提供一个API,在最顶层层叠skip list,以便在需要时获得有效的随机访问。 / p>
就中间的有效变异而言,跳过列表视图可能有一个侧表将变异索引映射到元素,并且二进制可搜索数组将原始索引映射到插入和移除后的索引偏移。
所有这些映射都提出了如何为某些有效定义提供有效的不可变映射的问题。我提出的最好的方法是使用b trees允许O(log n)访问可排序键,并在插入和删除时创建O(log n)节点。在k次修改之后,基于b树的地图的两个持有者之间共享的节点数量大约为。 (n - k log n)这对于不经常更新的地图来说非常好。
答案 1 :(得分:1)
不可变列表表示您无法在创建列表项后对其进行修改。所以你在滥用这种符号。你想要做的是:有一个可变列表并返回它的不可变视图。
Google Guava可以为您返回不可变的视图。
ImmutableList<T> view = ImmutableList.copyOf(mutableList);
如果您希望尽可能减少副本数量,则可以在要求新视图之前对mutableList
进行多次更新。