使用不可变列表时,内存会发生什么?

时间:2016-01-24 10:41:43

标签: f#

使用不可变列表时内存会发生什么?

调用List.Append?

时是否执行了深层复制

用于描述F#列表的此操作的Big-O表示法是什么?

将节点添加到列表末尾是否仍为O(1)?

如果没有,那么如果使用不可变列表违反链表的预期性能,为什么使用不可变列表会有用呢?

请考虑以下声明:

_modules <- _modules |> List.append moduleItems

源代码:

type CreationViewModel() =
    inherit ViewModelBase()

    let mutable (_modules:Module list) = []

    member this.Modules
        with get()      = _modules
        and set(value)  = _modules <- value

    member this.Add moduleItem = 
        _modules <- moduleItem :: _modules

    member this.AddList moduleItems = 
        _modules <- _modules |> List.append moduleItems

2 个答案:

答案 0 :(得分:3)

调用List.append时,不会复制项目本身,但指针是第一个需要的列表。

这是因为列表结构类似于

a::b::c::.....

所以当你加入两个名单时,你可以从

开始
a::b::c::d::[] and e::f::g::h::[]

a::b::c::d::e::f::g::h::[]

需要重写第一个列表。

因此,O(n) n是第一个列表的大小

答案 1 :(得分:1)

F#列表是单链接的,非常类似于以下定义。

type SList<'a> =
| Cons of 'a * SList<'a>
| Nil

因此,在O(1)时间内添加一个新元素到列表的头部,通过创建一个新的head元素并让它引用现有的tail元素。

> Cons ( "something", Nil );;
val it : SList<string> = Cons ("something", Nil)

可用于创建列表,如下所示:

> let test = Cons (1, Cons (2, Cons (3, Nil)));;
val test : SList<int> = Cons (1,Cons (2,Cons (3,Nil)))

但是,在列表末尾添加一个新元素需要遍历整个列表以找到结束,这需要花费O(N)时间,以便让最后一个节点引用新的最后一个节点。

一个简单的实现可以定义如下。即使它将列表复制两次,它仍然是O(N),因为成本与列表的大小呈线性关系。

let rec fold f state = function
    | Cons(v,xs) -> fold f (f state v) xs
    | Nil -> state

let reverse xs =
    fold (fun st v -> Cons (v, st)) Nil xs

let append x xs =
    reverse ( Cons (x, (reverse xs) ) )

请注意,上面定义的列表是不可变的。

在头部添加一个新元素会引用一个永远不会被修改的现有尾部,因此可以在多个列表(每个列表具有不同的头元素)之间安全地共享尾部。

将一个元素附加到列表的末尾会产生一个新列表,以保留原始列表的不变性:将最后一个元素更改为指向新的最后一个元素意味着更新前一个元素以指向更新的元素,等等列表中的每个元素。

那么为什么F#列表没有双重链接?

除了历史原因(ML仅提供单链表),我相信不可变的双链表实现需要在 结束时添加元素时复制整个列表,因为需要为任一操作调整每个节点的next和prev元素,因此总是花费O(N)时间。

不可变的单链表仅在追加到最后时支付O(N),因此效率通常更高。

由于最后一点,使用不可变列表的代码通常会避免顺序追加多个元素,即O(M * N)。相反,这样的代码通常会反转O(N)成本的列表,向头部添加几个元素(向后结束)以获得O(1)成本,然后反转结果以获得原始排序,从而产生总体O(N) )。操作。