使用continuation / CPS在OCaml中实现尾递归MergeSort

时间:2013-02-25 16:41:59

标签: functional-programming ocaml continuation-passing

我正在尝试在tail-recursive中实施MergeSort OCaml

由于Mergesort自然不是尾递归,所以我使用CPS来实现它。

此外,我的实施受到Tail-recursive merge sort in OCaml

的启发

以下是我的代码


let merge compare_fun l1 l2 = 
  let rec mg l1 l2 acc =
    match l1, l2 with
      | ([], []) -> List.rev acc
      | ([], hd2::tl2) -> mg [] tl2 (hd2::acc)
      | (hd1::tl1, []) -> mg tl1 [] (hd1::acc)
      | (hd1::tl1, hd2::tl2) ->
         let c = compare_fun hd1 hd2
         in 
         if c = 1 then mg l1 tl2 (hd2::acc)
         else if c = 0 then mg tl1 tl2 (hd2::hd1::acc)
         else mg tl1 l2 (hd1::acc)
  in 
  mg l1 l2 [];;

let split_list p l = 
  let rec split_list p (acc1, acc2) = function
    | [] -> (List.rev acc1, List.rev acc2)
    | hd::tl ->
      if p > 0 then split_list (p-1) (hd::acc1, acc2) tl
      else split_list (p-2) (acc1, hd::acc2) tl
  in 
  split_list p ([], []) l;;

let mergeSort_cps compare_fun l =
  let rec sort_cps l cf =  (*cf = continuation func*)
    match l with 
      | [] -> cf []
      | hd::[] -> cf [hd]
      | _ ->
        let (left, right) = split_list ((List.length l)/2) l
        in 
        sort_cps left (fun leftR -> sort_cps right (fun rightR -> cf (merge compare_fun leftR rightR)))
  in 
  sort_cps l (fun x -> x);;

当我编译它并使用1,000,000 integers运行它时,它会给出错误stackoverflow。的为什么吗


修改

以下是我用于测试的代码:

let compare_int x y =
  if x > y then 1
  else if x = y then 0
  else -1;;

let create_list n = 
  Random.self_init ();
  let rec create n' acc =
    if n' = 0 then acc
    else 
      create (n'-1) ((Random.int (n/2))::acc)
  in 
  create n [];;

let l = create_list 1000000;;

let sl = mergeSort_cps compare_int l;;
<{1>}中的

,它出现了此错误:http://try.ocamlpro.com/

Exception: RangeError: Maximum call stack size exceeded.

,它没有任何问题

2 个答案:

答案 0 :(得分:2)

添加另一个答案来提出一个单独的观点:回答者之间的混淆似乎是由于您不使用标准的OCaml编译器,而是运行独特的OCaml后端的TryOCaml网站。 javascript,因此优化和运行时特性略有不同。

我可以可靠地重现这样一个事实:在TryOCaml website上,您显示的CPS样式函数mergeSort_cps在长度为1_000_000的列表上失败,并出现以下错误:

Exception: InternalError: too much recursion.

我的分析是,由于缺乏尾部回复,这是,而是由于缺乏对Javascript后端的支持,CPS的非显而易见的方式-translated调用是tailrec:递归遍历lambda抽象边界(但仍处于尾部位置)。

转换 direct 中的代码,非tail-rec版本会让问题消失:

let rec merge_sort compare = function
  | [] -> []
  | [hd] -> [hd]
  | l ->
    let (left, right) = split_list (List.length l / 2) l in
    merge compare (merge_sort compare left) (merge_sort compare right);;

正如我在其他答案中所说,这段代码具有对数堆栈深度,因此使用它不会产生StackOverflow(tail-rec不是所有东西)。 Javascript后端处理的代码更简单。

请注意,通过使用更好的实现split(仍然使用merge的定义)可以避免双遍List.length然后拆分:

let split li =
  let rec split ls rs = function
    | [] -> (ls, rs)
    | x::xs -> split rs (x::ls) xs in
  split [] [] li;;

let rec merge_sort compare = function
  | [] -> []
  | [hd] -> [hd]
  | l ->
    let (left, right) = split l in
    merge compare (merge_sort compare left) (merge_sort compare right);;

答案 1 :(得分:0)

阅读评论时,您的Stack_overflow错误似乎难以重现。

尽管如此,您的代码并非完全采用CPS或尾递归:在merge_sort中,对split_listmerge的调用是在非尾调用位置进行的。< / p>

问题是:通过进行CPS转换和大量使用累加器,与递归相关的最差堆栈深度是多少?在sort调用上保存堆栈深度实际上并不是很有趣:因为每个将列表拆分为两个,对于输入列表大小的O(log n),最差堆栈深度将是n。 / p>

相反,splitmerge如果没有以累加器传递方式编写,则会对堆栈进行线性O(n)使用,因此它们很重要尾REC。由于您对这些例程的实现是尾随的,因此不必担心堆栈使用,也不需要以CPS形式转换排序例程本身,这使得代码更难以阅读。

(请注意,此对数减小参数特定于mergesort。快速排序可以在最坏的情况下使用线性堆栈,因此将其设置为tail-rec非常重要。)