F#mergesort尾递归

时间:2018-03-13 19:12:16

标签: f#

我试着为mergesort写一个尾递归代码。代码编译并运行。但是,输出是错误的。它只输出一个整数。我想知道如何修复此代码,以便对整数列表进行排序和输出。

let rec merge L L2 P = 
  match L, L2 with
  | [], [] -> P
  | [], _  -> L2
  | _,  [] -> L
  | hd::t1, hd2::t2 ->
    if hd <= hd2 then
       merge t1 L2 (P @ [hd])
    else
       merge L t2 (P @ [hd2])


//
// mergesort:
//
let rec ms L  = 
  match L with
  | []    -> []
  | e::[] -> L
  | _     -> 
    let mid = List.length L / 2
    let (L, L2) = List.splitAt mid L
    merge (ms L) (ms L2) []

2 个答案:

答案 0 :(得分:5)

您的问题在功能merge中:假设您对列表[2;1]进行排序。它变为merge [2] [1] [],然后变为merge [] [2] [1],最后变为match的第二种情况[2]。 match的第二和第三个案例必须以某种方式解释P

事实上,你绝对不需要操纵merge中的3个列表,如果我们将它重构为两个就足够了:

let rec merge l1 l2 =
    match (l1,l2) with
    | (x,[]) -> x
    | ([],y) -> y
    | (x::tx,y::ty) ->
        if x <= y then x::merge tx l2
        else y::merge l1 ty 

并将ms的最后一行更改为merge (ms L) (ms L2) - 此变体确实按预期工作:

ms List<int>.Empty返回[]

ms [2;1]返回[1;2]

e.t.c

更新:正如@kvb所指出的,上面的merge函数不是尾递归的。这是正确的,并且通过引入通过acc函数填充的累加器continuation,将其重构为尾递归版本需要更多参与:

let merge l1 l2 =
  let rec mergeAux continuation l1 l2 = 
    match l1, l2 with
    | l1, [] -> continuation l1
    | [], l2 -> continuation l2
    | x::tx, y::ty ->
      if x <= y then mergeAux (fun acc -> continuation(x::acc)) tx l2
      else mergeAux (fun acc -> continuation(y::acc)) l1 ty
  mergeAux id l1 l2

现在实现是尾递归的,很容易检查:

let rand = System.Random() in
    List.init 1000000 (fun _ -> rand.Next(-10000,10000)) |> ms
>
val it : int list =
  [-10000; -10000; -10000; -10000; -10000; -10000; -10000; ...

答案 1 :(得分:1)

即使对已接受的答案进行了更改,您仍然没有尾递归合并排序。合并排序merge (ms L) (ms L2)的最后一行调用ms两次,然后调用merge。为了使函数成为尾递归,您的函数必须以最多一次递归调用结束。这种情况是需要延续的地方。通过传递一个延续,你可以调用ms并传递一个延续,使第二次调用ms并传递第二个调用另一个调用merge的延续。我实际上会删除merge函数的延续,因为它不需要它会使merge函数比使用累加器参数实现它更难阅读。最后,为了便于从外部调用,我会将merge函数和ms函数嵌套在mergeSort函数中,该函数只需要一个列表参数,不需要公开剩下的细节给来电者。我在F#中实现完全尾递归合并排序的方法如下:

let mergeSort ls =
    let rec merge l1 l2 res = 
        match l1, l2 with
        | [], [] -> res |> List.rev
        | x::xs, [] -> merge xs [] (x::res)
        | [], y::ys -> merge [] ys (y::res)
        | x::xs, y::ys when x < y -> merge xs (y::ys) (x::res)
        | xs, y::ys -> merge xs ys (y::res)

    let rec ms ls cont =
        match ls with
        | [] -> cont []
        | [x] -> cont [x]
        | xs ->
            let ys, zs = List.splitAt ((List.length xs) / 2) xs
            ms ys (fun ys' -> ms zs (fun zs' -> cont (merge ys' zs' [])))
    ms ls id

请注意,有一些方法可以更有效地在内存使用方面做到这一点,由于内存分配较少,这可能也有助于提高速度,但由于这超出了本问题的范围,我不打算进入在答案中。