我正在尝试在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.
中,它没有任何问题
答案 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_list
和merge
的调用是在非尾调用位置进行的。< / p>
问题是:通过进行CPS转换和大量使用累加器,与递归相关的最差堆栈深度是多少?在sort
调用上保存堆栈深度实际上并不是很有趣:因为每个将列表拆分为两个,对于输入列表大小的O(log n)
,最差堆栈深度将是n
。 / p>
相反,split
和merge
如果没有以累加器传递方式编写,则会对堆栈进行线性O(n)
使用,因此它们很重要尾REC。由于您对这些例程的实现是尾随的,因此不必担心堆栈使用,也不需要以CPS形式转换排序例程本身,这使得代码更难以阅读。
(请注意,此对数减小参数特定于mergesort。快速排序可以在最坏的情况下使用线性堆栈,因此将其设置为tail-rec非常重要。)