基于谓词将列表拆分为列表列表

时间:2010-01-15 11:22:53

标签: f#

(我知道this question,但它与序列有关,这不是我的问题)

鉴于此输入(例如):

let testlist = 
    [  
       "*text1";
       "*text2";
       "text3";
       "text4";
       "*text5";
       "*text6";
       "*text7"
    ]

let pred (s:string) = s.StartsWith("*")

我希望能够致电MyFunc pred testlist并获得此输出:

[
    ["*text1";"*text2"];
    ["*text5";"*text6";"*text7"]
]

这是我目前的解决方案,但我不太喜欢嵌套的List.revs(忽略它需要Seq作为输入的事实)

let shunt pred sq =
    let shunter (prevpick, acc) (pick, a) = 
        match pick, prevpick with
        | (true, true)  -> (true, (a :: (List.hd acc)) :: (List.tl acc))
        | (false, _)    -> (false, acc)
        | (true, _)     -> (true, [a] :: acc)

    sq 
        |> Seq.map (fun a -> (pred a, a))
        |> Seq.fold shunter (false, []) 
        |> snd
        |> List.map List.rev
        |> List.rev

5 个答案:

答案 0 :(得分:5)

F#核心库中有一个List.partition函数(如果你想实现它只是为了让它工作而不是自己学习如何编写递归函数)。使用此功能,您可以写下:

> testlist |> List.partition (fun s -> s.StartsWith("*"))
val it : string list * string list =
  (["*text1"; "*text2"; "*text5"; "*text6"; "*text7"], ["text3"; "text4"])

请注意,此函数返回一个元组,而不是返回列表列表。这与您想要的有点不同,但如果谓词返回true或false,那么这更有意义。

返回元组的partition函数的实现也有点简单,因此它可能对学习目的很有用:

let partition pred list = 
  // Helper function, which keeps results collected so
  // far in 'accumulator' arguments outTrue and outFalse
  let rec partitionAux list outTrue outFalse =
    match list with 
    | [] -> 
        // We need to reverse the results (as we collected
        // them in the opposite order!)
        List.rev outTrue, List.rev outFalse
    // Append element to one of the lists, depending on 'pred'
    | x::xs when pred x -> partitionAux xs (x::outTrue) outFalse
    | x::xs -> partitionAux xs outTrue (x::outFalse)

  // Run the helper function
  partitionAux list [] []   

答案 1 :(得分:3)

编辑:使用下面添加的foldBack修改版本。

以下是一些使用列表和尾递归的代码:

//divides a list L into chunks for which all elements match pred
let divide pred L =
    let rec aux buf acc L =
        match L,buf with
        //no more input and an empty buffer -> return acc
        | [],[] -> List.rev acc 
        //no more input and a non-empty buffer -> return acc + rest of buffer
        | [],buf -> List.rev (List.rev buf :: acc) 
        //found something that matches pred: put it in the buffer and go to next in list
        | h::t,buf when pred h -> aux (h::buf) acc t
        //found something that doesn't match pred. Continue but don't add an empty buffer to acc
        | h::t,[] -> aux [] acc t
        //found input that doesn't match pred. Add buffer to acc and continue with an empty buffer
        | h::t,buf -> aux [] (List.rev buf :: acc) t
    aux [] [] L

用法:

> divide pred testlist;;
val it : string list list =
  [["*text1"; "*text2"]; ["*text5"; "*text6"; "*text7"]]

使用列表作为缓冲区的数据结构意味着在输出内容时总是需要反转。如果单个块的大小适中,这可能不是问题。如果速度/效率成为问题,您可以使用Queue<'a>或`List&lt;'a&gt;'对于缓冲区,其附加速度很快。但是使用这些数据结构而不是列表也意味着您失去了强大的列表模式匹配。在我看来,能够模式匹配列表超过了一些List.rev调用的存在。

这是一个流式版本,一次输出一个块的结果。这避免了上一个示例中累加器上的List.rev:

let dividestream pred L =
    let rec aux buf L =
        seq { match L, buf with
              | [],[] -> ()
              | [],buf -> yield List.rev buf
              | h::t,buf when pred h -> yield! aux (h::buf) t
              | h::t,[] -> yield! aux [] t
              | h::t,buf -> yield List.rev buf
                            yield! aux [] t }
    aux [] L

此流媒体版本避免了累加器上的List.rev。使用List.foldBack也可以避免反转累积的块。

更新:这是使用foldBack的版本

//divides a list L into chunks for which all elements match pred
let divide2 pred L =
    let f x (acc,buf) =
        match pred x,buf with
        | true,buf -> (acc,x::buf)
        | false,[] -> (acc,[])
        | false,buf -> (buf::acc,[])

    let rest,remainingBuffer = List.foldBack f L ([],[])
    match remainingBuffer with
    | [] -> rest
    | buf -> buf :: rest

答案 2 :(得分:2)

只需提前反转列表,然后轻松构建结构:

let Shunt p l =
    let mutable r = List.rev l
    let mutable result = []
    while not r.IsEmpty do
        let mutable thisBatch = []
        while not r.IsEmpty && not(p r.Head) do
            r <- r.Tail 
        while not r.IsEmpty && p r.Head do
            thisBatch <- r.Head :: thisBatch
            r <- r.Tail
        if not thisBatch.IsEmpty then
            result <- thisBatch :: result
    result        

外部while处理每个“批处理”,第一个内部while跳过任何与谓词不匹配的内容,然后是另一个while抓取所有那些将它们存储在当前批次中。如果此批次中有任何内容(最后一个可能为空),请将其添加到最终结果中。

这是一个例子,我认为本地命令式代码优于纯功能对应代码。上面的代码很容易编写和推理。

答案 3 :(得分:0)

shunt的另一个版本:

let shunt pred lst =
    let rec tWhile pred lst = 
        match lst with
        | []                    -> [], []
        | hd :: tl when pred hd -> let taken, rest = tWhile pred tl
                                   (hd :: taken), rest
        | lst                   -> [], lst
    let rec collect = function
        | []  -> []
        | lst -> let taken, rest = tWhile pred lst
                 taken :: (collect (snd (tWhile (fun x -> not (pred x)) rest)))
    collect lst

这个避免List.rev,但它不是尾递归 - 所以只适用于小列表。

答案 4 :(得分:0)

另一个......

let partition pred lst = 
    let rec trec xs cont =
        match xs with
        | []               -> ([],[]) |> cont
        | h::t when pred h -> (fun (y,n) -> h::y,n) >> cont |> trec t
        | h::t             -> (fun (y,n) -> y,h::n) >> cont |> trec t
    trec lst id

然后我们可以定义分流:

let shunt pred lst = lst |> partition pred |> (fun (x,y) -> [x;y])