Functional programming 使用continuation/CPS在OCaml中实现尾部递归合并排序
我试图在Functional programming 使用continuation/CPS在OCaml中实现尾部递归合并排序,functional-programming,ocaml,continuation-passing,Functional Programming,Ocaml,Continuation Passing,我试图在OCaml中实现一个尾部递归合并排序 因为Mergesort自然不是尾部递归,所以我使用CPS来实现它 我的实现也受到了 下面是我的代码 当我编译它,并使用1000000整数运行它时,它给出了堆栈溢出的错误为什么? 编辑 以下是我用于测试的代码: 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
OCaml
中实现一个尾部递归
合并排序
因为Mergesort
自然不是尾部递归,所以我使用CPS
来实现它
我的实现也受到了
下面是我的代码
当我编译它,并使用
1000000
整数运行它时,它给出了堆栈溢出
的错误为什么?
编辑
以下是我用于测试的代码:
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;;
在http://try.ocamlpro.com/
,它给出了以下错误:异常:RangeError:超出了最大调用堆栈大小。
在本地ocaml顶级
中,读取注释时没有任何问题,您的堆栈溢出
错误似乎很难重现
然而,您的代码并不完全处于CPS或尾部递归状态:在merge\u sort
中,对split\u list
和merge
的调用是在非尾部调用位置进行的
问题是:通过进行CPS转换并大量使用累加器,与递归相关的最差堆栈深度是多少?在sort
调用上保存堆栈深度实际上不是很有趣:当每个调用都将列表一分为二时,对于n
输入列表的大小,最差的堆栈深度将是O(logn)
相反,split
和merge
如果不是以累加器传递样式编写的,则会对堆栈进行线性O(n)
使用,因此它们对tail-rec很重要。由于这些例程的实现是tail-rec,因此不必担心堆栈的使用,也不需要将排序例程本身转换为CPS形式,这会使代码更难阅读
(请注意,此对数递减参数特定于mergesort。在最坏的情况下,快速排序可以使用线性堆栈,因此将其设置为tail-rec可能很重要。)添加另一个答案来说明另一个问题:回答者之间的许多困惑似乎是因为您不使用标准的OCaml编译器,而是使用Tryocam网站,该网站在javascript之上运行一个独特的OCaml后端,因此具有略微不同的优化和运行时特征
我可以可靠地再现这样一个事实:在上,您在长度1\u 000\u 000
的列表上显示的CPS样式函数mergeSort\u CPS
失败,错误如下:
Exception: InternalError: too much recursion.
我的分析是,这不是因为缺少tail-rec,而是因为Javascript后端缺乏对CPS转换调用tail-rec的非明显方式的支持:递归通过lambda抽象边界(但仍处于tail-position)
在直接、非尾部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);;
正如我在另一个答案中所说,这段代码的堆栈深度是对数的,因此使用它不会产生堆栈溢出(tail rec不是一切)。Javascript后端可以更好地处理更简单的代码
请注意,通过使用更好的实现split
(仍然使用您对merge
的定义),您可以显著加快执行速度,从而避免重复遍历列表。长度
然后拆分:
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);;
我无法重现你的行为。当我在1000000个整数的列表上尝试您的代码时,它会对它们进行正确排序。它在顶级和编译代码中对我有效。使用默认堆栈设置的i386(32位)中的ocamlc和ocamlopt,尝试了10000000个整数的代码。没有堆栈溢出。仔细检查您的构建,如果它复制了,请澄清您的环境。@JeffreyScofield我尝试了我的代码,它给出了这样一个错误。我想这是该站点的一个bug。我不知道该站点的详细信息,但在许多虚拟机语言环境(JavaScript、Java等)中很难获得正确的尾部调用。很可能您使用的站点正在将OCaml翻译成JavaScript或类似的语言。如果您想深入了解OCaml,您可能应该使用INRIA版本。我在中尝试了我的代码,它给出了这样一个错误。我猜这是站点中的一个bug。我认为我的split_list
和merge
是尾部递归的。是的,但是它们在sort
例程中用于非尾部位置,所以你仍然需要担心,比如说,是否递归调用sort
(这里不是这种情况)。我如何转换整个代码,以便它们在排序
例程中的尾部位置使用,因此我有100%纯尾部递归
?@JacksonTale您确定这会解决您的问题吗?我怀疑你错了,这个bug在其他地方,例如在构建传递给排序函数的输入时。你能展示你测试的完整代码吗?你能进一步解释一下新的split
功能吗?我真的不明白它是怎么工作的。@JacksonTale我建议你拿支笔和纸,用手执行split[][1;2;3]
。好的,你的split
版本不关心元素的顺序,也不关心left
或right
列表的顺序,但无论如何,它适合mergesort,因为mergesort只是合并两个列表,并不关心这些事情,对吗?如果我们需要一个<代码> SPLITYLISTRONU/COD>,只需在中间剪切一个列表,那么您的版本将不适合我知道<代码>列表。长度< /代码>将有<代码> O(n)< /代码>或只是<代码> o(1)< /代码>。
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);;