【问题标题】:Linked list partition function and reversed results链表分区函数和反转结果
【发布时间】:2015-09-03 04:39:00
【问题描述】:

我编写了这个 F# 函数来将列表分区到某个点,而不是进一步 - 很像 takeWhilepartition 之间的交叉。

let partitionWhile c l =
    let rec aux accl accr =
        match accr with
        | [] -> (accl, [])
        | h::t ->
            if c h then
                aux (h::accl) t
            else
                (accl, accr)
    aux [] l

唯一的问题是“被拿走”的物品被颠倒了:

> partitionWhile ((>=) 5) [1..10];;
val it : int list * int list = ([5; 4; 3; 2; 1], [6; 7; 8; 9; 10])

除了调用rev之外,有没有一种方法可以编写这个函数来让第一个列表的顺序正确?

【问题讨论】:

    标签: algorithm list f#


    【解决方案1】:

    这是一个基于延续的版本。它是尾递归的,并以原始顺序返回列表。

    let partitionWhileCps c l =
      let rec aux f = function
        | h::t when c h -> aux (fun (acc, l) -> f ((h::acc), l)) t
        | l -> f ([], l)
      aux id l
    

    在 Brian 的回答(以及供参考的累加器版本)之后的讨论中,这里有一些基准:

    let partitionWhileAcc c l =
      let rec aux acc = function
        | h::t when c h -> aux (h::acc) t
        | l -> (List.rev acc, l)
      aux [] l
    
    let test = 
      let l = List.init 10000000 id
      (fun f ->
        let r = f ((>) 9999999) l
        printfn "%A" r)
    
    test partitionWhileCps // Real: 00:00:06.912, CPU: 00:00:07.347, GC gen0: 78, gen1: 65, gen2: 1
    test partitionWhileAcc // Real: 00:00:03.755, CPU: 00:00:03.790, GC gen0: 52, gen1: 50, gen2: 1
    

    Cps 平均约 7 秒,Acc 约 4 秒。简而言之,延续对于这个练习没有任何好处。

    【讨论】:

      【解决方案2】:

      我希望您可以使用延续,但最后调用 List.rev 是最好的方法。

      【讨论】:

      • 除了因为它更容易之外,是否有理由支持累加器 + List.rev 而不是 CPS?
      • 更容易编写,更容易阅读,并且可能更高效。
      • 在我能说什么之前,我将不得不阅读更多关于延续的内容......嗯......
      • 我不知道性能更高,因为使用延续似乎非常有效,但我绝对支持更容易编写/阅读。
      • @David 是的,我仍在试图围绕丹尼尔的答案。这不是我熟悉的形式。尽管与大多数功能习语和一般的编程习语一样,我认为一旦点击它可能会变得相当可读。性能可能是我必须多次测量的东西......
      【解决方案3】:

      我通常更喜欢序列而不是列表,因为它们很懒惰,并且您可以使用 List.toSeqSeq.toList 函数在它们之间进行转换。下面是使用序列实现您的partitionWhile 函数。

      let partitionWhile (c:'a -> bool) (l:'a list) = 
          let fromEnum (e:'a IEnumerator) = 
              seq { while e.MoveNext() do yield e.Current}
          use e = (l |> List.toSeq).GetEnumerator()
          (e |> fromEnum |> Seq.takeWhile c |> Seq.toList)
          ,(e |> fromEnum |> Seq.toList)
      

      【讨论】:

        【解决方案4】:

        你可以这样重写函数:

        let partitionWhile c l =
          let rec aux xs =
            match xs with
              | [] -> ([], [])
              | h :: t ->
                  if c h then
                    let (good, bad) = aux t in
                      (h :: good, bad)
                  else
                    ([], h :: t)
          aux l
        

        是的,正如 Brian 所指出的,它不再是尾递归,但它回答了上述问题。顺便说一句,Haskell 中的 span 在 Hugs 中的实现方式完全相同:

        span p []       = ([],[])
        span p xs@(x:xs')
            | p x       = (x:ys, zs)
            | otherwise = ([],xs)
            where (ys,zs) = span p xs'
        

        在 Haskell 中偏爱这个版本的一个很好的理由是懒惰:在第一个版本中,所有好的元素都在列表反转之前被访问。在第二个版本中,第一个好的元素可以立即返回。

        【讨论】:

        • 仅供参考,这不是尾递归,所以它会在一个非常大的列表中与 StackOverflowException 一起爆炸。
        • 似乎是一种相当冒险的方式来实现可能用于大型列表的算法。 Haskell 是否提供更大的堆栈?
        • @Rei 原因是懒惰。我最好也提一下。
        【解决方案5】:

        我不认为我是唯一一个从 Daniel 的 CPS 解决方案中学到很多东西的人。在试图弄清楚的过程中,它帮助我改变了几个可能(对初学者来说)模棱两可的列表引用,如下所示:

            let partitionWhileCps cond l1 =
        
                let rec aux f l2 = 
                    match l2 with
                    | h::t when cond h ->   aux  (fun (acc, l3) -> f (h::acc, l3))  t
                    | l4 -> f ([], l4)
        
                aux id l1
        

        (请注意,l4 匹配中的“[]”是初始 acc 值。)我喜欢这个解决方案,因为不必使用 List.rev,通过钻到第一个列表的末尾并构建第二个列表倒退。我认为避免 .rev 的另一种主要方法是将尾递归与 cons 操作一起使用。某些语言以与适当的尾递归相同的方式优化“尾递归 mod cons”(但 Don Syme 表示这不会出现在 F# 中)。

        所以这在 F# 中不是尾递归安全的,但它使我的答案成为答案并避免 List.rev (这很难访问两个元组元素,否则将更适合与 cps 方法平行,我想,就像我们只返回第一个列表一样):

            let partitionWhileTrmc cond l1 = 
        
                let rec aux acc l2 =  
                    match l2 with 
                    | h::t when cond h ->  ( h::fst(aux acc t), snd(aux acc t)) 
                    | l3 -> (acc, l3)
        
                aux [] l1
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-07-27
          • 2019-08-13
          • 1970-01-01
          • 2013-02-03
          相关资源
          最近更新 更多