【问题标题】:Tail-recursive merge sort in OCamlOCaml 中的尾递归合并排序
【发布时间】:2010-03-27 14:17:13
【问题描述】:

我正在尝试在 OCaml 中实现一个尾递归列表排序功能,我想出了以下代码:

let tailrec_merge_sort l =
  let split l = 
    let rec _split source left right =
      match source with
        | [] -> (left, right)
        | head :: tail -> _split tail right (head :: left) 
    in _split l [] []
  in

  let merge l1 l2 = 
    let rec _merge l1 l2 result =
      match l1, l2 with
        | [], [] -> result
        | [], h :: t | h :: t, [] -> _merge [] t (h :: result)
        | h1 :: t1, h2 :: t2 ->
            if h1 < h2 then _merge t1 l2 (h1 :: result)
            else            _merge l1 t2 (h2 :: result)
    in List.rev (_merge l1 l2 [])
  in

  let rec sort = function
    | [] -> []
    | [a] -> [a]
    | list -> let left, right = split list in merge (sort left) (sort right)
  in sort l
;;

但它似乎实际上并不是尾递归,因为我遇到了“评估期间堆栈溢出(循环递归?)”错误。

您能帮我找出这段代码中的非尾递归调用吗?我已经搜索了很多,没有找到它。是不是 sort 函数中的 let 绑定?

【问题讨论】:

    标签: sorting ocaml tail-recursion


    【解决方案1】:

    归并排序本质上不是尾递归的。一个排序需要两个递归调用,并且在任何函数的任何执行中,最多一个 动态调用可以在尾部位置。 (split 也从非尾部位置调用,但因为它应该使用常量堆栈空间,所以应该可以)。

    确保您使用的是最新版本的 OCaml。 在 3.08 及更早版本中,List.rev 可能会破坏堆栈。此问题已在 3.10 版中修复。使用版本 3.10.2,我可以使用您的代码对 一千万个元素 的列表进行排序。这需要几分钟,但我不会破坏堆栈。所以我希望你的问题只是你有一个旧版本的 OCaml。

    如果没有,下一步是设置环境变量

    OCAMLRUNPARAM=b=1
    

    当你破坏堆栈时,它将给出堆栈跟踪。

    我还想知道您正在排序的数组的长度;虽然归并排序不能是尾递归的,但它的非尾性质应该会消耗您对数堆栈空间。

    如果您需要对超过 1000 万个元素进行排序,也许您应该考虑使用不同的算法?或者,如果您愿意,您可以手动进行 CPS 转换合并排序,这将产生一个尾递归版本,代价是在堆上分配连续。但我的猜测是没有必要。

    【讨论】:

    • 嗯,由于 split 不在最后一个位置,它算吗? (我的意思是,据我了解,编译器应该能够检测到尾递归函数并将其转换为循环;然后,只有最后一次调用才重要)此外,使用延续应该使函数尾递归,应该不是吗?
    • 我使用的是 OCaml v11.0,当我在 10^6 个元素上运行我的代码时,我破坏了堆栈。我需要对 5 到 1000 万个元素进行排序。
    • 最后,我的问题是,即使使用延续,我也会破坏堆栈。知道为什么吗?
    • 顺便问一下,有没有办法在运行顶级程序时获得回溯?
    • @CFP:设置环境变量应该可以为您提供回溯。如果没有,只需编译您的测试用例并将其作为独立的二进制文件运行。
    【解决方案2】:

    表达式

    merge (sort left) (sort right)
    

    不是尾递归的;它递归调用(左排序)和(右排序),而调用(合并)中有剩余的工作。

    我认为您可以使用额外的延续参数来修复它:

      let rec sort l k =
        match l with
        | [] -> k [] 
        | [a] -> k [a] 
        | list -> let left, right = split list in sort left (fun leftR -> sort right (fun rightR -> k (merge leftR rightR)))
      in sort l (fun x -> x)
    

    【讨论】:

    • 哦,我想我明白了;谢谢!但是,我怎样才能让我的函数递归呢?
    • 你能解释一下为什么延续确实使函数尾递归吗?还是他们只是将捕获堆栈帧的过程从(可能溢出的)堆栈移动到生成的闭包?
    • 嗯,我想这应该可行,但我不太明白 k 函数的作用。你能解释一下吗?非常感谢!我已经测试过了,但它并没有解决溢出问题......知道为什么吗?
    • 我没有测试代码,所以我可能犯了错误。 :) 我对延续策略的最佳解释是:lorgonblog.spaces.live.com/blog/cns!701679AD17B6D310!170.entry
    • 我猜这是Caml中tail-rec优化的一个错误。反正医生。非常好,非常感谢!
    猜你喜欢
    • 2015-01-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-11-06
    • 2019-01-22
    相关资源
    最近更新 更多