【问题标题】:how to insert in the middle of a list, being tail-call friendly but without hurting performance?如何在列表中间插入,对尾调用友好但不影响性能?
【发布时间】:2018-01-17 10:13:25
【问题描述】:

所以我有这个似乎对非尾调用友好的函数,对吧?

let rec insertFooInProperPosition (foo: Foo) (bar: list<Foo>): list<Foo> =
    match bar with
    | [] -> [ foo ]
    | head::tail ->
        if (foo.Compare(head)) then
            foo::bar
        else
            head::(insertFooInProperPosition foo tail)

然后我试图弄清楚如何使用累加器,以便函数完成的最后一件事是调用自身,我想出了这个:

let rec insertFooInProperPositionTailRec (foo: Foo) (headListAcc: list<Foo>) (bar: list<Foo>): list<Foo> =
    match bar with
    | [] -> List.concat [headListAcc;[ foo ]]
    | head::tail ->
        if (foo.Compare(head)) then
            List.concat [headListAcc; foo::bar]
        else
            insertFooInProperPosition foo (List.concat [headListAcc;[head]]) tail

let rec insertFooInProperPosition (foo: Foo) (bar: list<Foo>): list<Foo> =
    insertFooInProperPositionTailRec foo [] bar

但是,据我了解,使用 List.concat 会使这个函数的效率大大降低,对吧?那么我该如何正确地进行这种转换呢?

【问题讨论】:

    标签: recursion f# tail-recursion tail-call-optimization tail-call


    【解决方案1】:

    如果需要使用递归,您的解决方案看起来不错。然而,这个任务可以在没有递归的情况下完成(而且速度更快一些)。

    要连接两个列表,应复制第一个列表,其最后一个元素应指向第二个列表的第一个元素。这是 O(N),其中 N 是第一个列表的大小。尾部增长的列表需要多个连接,生成每个 N 的遍历列表,这使得复杂度呈二次方(希望我在这里)。

    与将项目添加到列表中的递归方法不同,更快的方法可能是找到插入索引,然后一次性复制它之前的所有项目,然后将其与新项目和列表的其余部分连接起来。这只需要通过列表的 3 次,所以 O(N)。

    let insertFooInProperPosition (foo : Foo) (bar : Foo list) : Foo list =
        bar
        |> List.tryFindIndex (fun v -> v.Compare foo)
        |> function | None -> List.append bar [ foo ]
                    | Some i -> let before, after = List.splitAt i bar
                                List.concat [ before; [ foo ]; after ]
    

    【讨论】:

    • 谢谢!但是,考虑到第一个版本(非尾调用友好版本)仅通过了 1 次,列表中的 3 次通过似乎有点过分! :'-(
    • 不是真的,因为它从递归中返回,这也很重要。
    • 递归算作内存消耗,但不完全是 O(x) 性能,对吧?顺便说一句,你觉得我的替代方案怎么样?
    • “递归算作内存消耗” - 如果它不是尾部优化的,那么是的,需要分配堆栈帧。此外,如果需要为每个项目调用它是 O(list length)。
    【解决方案2】:

    不幸的是,您无法从头到尾构建 F# 列表(除非您使用 F# Core 库内部的函数,这些函数在后台使用了突变)。因此,最好的想法可能是从旧列表中构建一个新列表,在我们进行时添加下一个元素,并在正确的位置插入foo。最后将新列表反转,得到与旧列表相同的顺序:

    let insertFoo (foo : Foo) bar =
        let rec loop acc = function
            | [] -> prep (foo :: acc) []
            | x :: xs ->
                if foo.Compare x
                then prep (x :: foo :: acc) xs
                else loop (x :: acc) xs
        and prep acc = function
            | [] -> acc
            | x :: xs -> prep (x :: acc) xs
        loop [] bar |> List.rev
    

    我猜@knocte 使用等效解决方案会更快……

    【讨论】:

    • 如果你把prep放在loop之前,我猜你不需要and
    • 没什么特别的……没有and,我将不得不将prep移到loop上方……
    【解决方案3】:

    @AlexAtNet 的解决方案看起来还不错,但是如果你还想要递归,你可以避免这么多concat 这样的调用:

    let rec insertFooInProperPositionTailRec (foo: Foo)
                                             (headListAcc: list<Foo>)
                                             (bar: list<Foo>)
                                             : list<Foo> =
        match bar with
        | [] -> List.rev (foo::headListAcc)
        | head::tail ->
            if (foo.Compare(head)) then
                let newAcc = List.rev headListAcc
                [ yield! newAcc
                  yield! foo::bar ]
            else
                let newAcc = head::headListAcc
                insertFooInProperPositionTailRec foo newAcc tail
    
    let rec insertFooInProperPosition (foo: Foo) (bar: list<Foo>): list<Foo> =
        insertFooInProperPositionTailRec foo [] bar
    

    不确定它是否比@AlexAtNet 的性能更高,嗯...

    【讨论】:

    • 是的,从头部累积列表然后反转它是一个不错的技巧,使用堆栈是另一种选择,这使得它非常接近非尾部优化解决方案中发生的情况。
    猜你喜欢
    • 1970-01-01
    • 2021-03-17
    • 2017-11-15
    • 1970-01-01
    • 1970-01-01
    • 2012-01-30
    • 2011-03-16
    • 1970-01-01
    • 2011-01-05
    相关资源
    最近更新 更多