似乎Map.remove
将返回一个新的地图结构,使原始地图保持不变。
复杂性如何仍然是O(lg n)?
答案 0 :(得分:6)
以前的大部分地图都是由新地图共享的。只有一小部分是不同的。你可以在Chris Okasaki的论文Purely Functional Data Structures中阅读有关为什么不可变数据结构表现得非常好的所有内容。
答案 1 :(得分:6)
如果这可以给你一个直觉,这里是一个堆栈的实现
使用O(1)
Stack.pop,返回一个新结构,保持
前一个未更改(请参阅我如何弹出test
两次)。
type 'a stack = Stack of 'a list
let empty = Stack []
let push x (Stack xs) = Stack (x :: xs)
let pop = function
| Stack [] -> raise Not_found
| Stack (x::xs) -> x, Stack xs
(* testing the structure in the toplevel *)
# let test = push 1 (push 2 (push 3 empty));;
val test : int stack = Stack [1; 2; 3]
# let (n, test2) = pop test;;
val n : int = 1
val test2 : int stack = Stack [2; 3]
# pop test2;;
- : int * int stack = (2, Stack [3])
# (* but the starting stack 'test' is still available *)
pop test;;
- : int * int stack = (1, Stack [2; 3])
这归结为算法问题。有一种所谓的“纯功能数据结构”,它具有以下特性:您可以以这样的方式实现所需的操作:大多数数据可以在结构的不同副本之间共享 - 请注意,这主要取决于别名,如果这些结构的元素是可变的,这将是可观察的。
在list
示例中,获取列表的尾部会为您提供一个不同的列表,该列表也是前一个列表的一部分,因此不会涉及任何数据副本。对于Map.remove
(或其他地图修改操作),您通常会修改从平衡树的根到您感兴趣的节点的路径,因此此路径(对数高度)将在两个结构,但树的其余部分,即沿该路径的左右子树,将不会被修改,并且可以在两个结构之间共享,从而导致只进行对数内存分配。
但恰恰相反,其他一些结构很难以这种方式持久化。通常,数组是一个非常扁平的结构,包含大量元素。因此,如果您想要返回数组的修改版本,您基本上必须改变现有数组(丢失以前的版本,因此不是持久的)或将整个数组复制到新版本(线性时间和内存成本) )。前面的例子是有效的,因为有一些独立子结构的间接(平衡树的子树,列表的尾部)可以单独使用而无需复制;但是阵列只是扁平结构,没有这种独立的子结构。但这可能是一个有趣的性能权衡:缺乏间接性正是为什么数组访问是恒定时间(忘记这里的缓存),而在平衡树中查找更昂贵(O(log n)
很小但是明智在实际操作中不仅仅是数组的O(1)
。
哈希表是可变的原因是它们是在数组之上实现的。哈希表是一个“桶”数组(实现为列表,或者,如果您明智的话,实现为树数据结构),其中每个桶包含散列到数组中相同键的所有元素。更新哈希表意味着更新存储桶(可以以持久的方式完成),但是您需要更新数组,这是非持久的,原因与上述相同。
请注意,并非所有内容都丢失:您可以提出此类数据结构的持久版本。你可以通过支付O(log n)
成本(通过将可变内存表示为从整数到内容的持久平衡树)来做到这一点,但在大多数情况下,你也可以聪明并且拥有比希望只有有点比不关心持久性的对手慢。涉及各种权衡,但如果您的应用程序需要持久性(例如,您表示需要经常对状态进行快照的系统状态,有时会回溯到早期版本),那么''我很高兴有这些替代品。
在这种情况下,请参阅this discussion of persistent arrays和this blog post以获取HAMT的OCaml实现,HAMT是一个着名的哈希表启发的持久数据结构,由Clojure社区启发给我们(Clojure,一种专注于语言的语言)并发性因此明智地避免了可变状态,这导致了持久数据结构领域的一些相当有趣的工作。)