在F#/ OCaML中实现类似quicksort的函数的尾递归版本

Yet*_*eek 5 f# ocaml functional-programming tail-recursion

是否可以实现快速排序算法的尾递归版本(通过延续模式)?如果是的话,如何实现呢?

正常(未优化)版本:

let rec quicksort list =
 match list with
 | [] -> []
 | element::[] -> [element]
 | pivot::rest -> let ``elements smaller than pivot``, ``elements larger or equal to pivot``= 
                    rest |> List.partition(fun element -> element < pivot)
                  quicksort ``elements smaller than pivot`` @ [pivot] @ quicksort ``elements larger or equal to pivot``
Run Code Online (Sandbox Code Playgroud)

gas*_*che 13

直接风格:

let rec quicksort list =
    match list with
    | [] -> []
    | [element] -> [element]
    | pivot::rest ->
        let left, right = List.partition (fun element -> element < pivot) rest in
        let sorted_left = quicksort left in
        let sorted_right = quicksort right in
        sorted_left @ [pivot] @ sorted_right
Run Code Online (Sandbox Code Playgroud)

我的第一个,天真的翻译与Laurent的版本非常相似,除了有点奇怪地显示带有continuation的调用实际上是一种绑定:

let rec quicksort list cont =
    match list with
    | [] -> cont []
    | element::[] -> cont [element]
    | pivot::rest ->
        let left, right = List.partition (fun element -> element < pivot) rest in
        quicksort left (fun sorted_left ->
        quicksort right (fun sorted_right ->
        cont (sorted_left @ [pivot] @ sorted_right)))
let qsort li = quicksort li (fun x -> x)
Run Code Online (Sandbox Code Playgroud)

与Laurent相反,我发现很容易检查cont没有被遗忘:从直接样式转换的CPS函数具有线性使用延续的属性,在每个分支中只有一次,在尾部位置.很容易检查没有忘记这样的电话.

但事实上,对于大多数快速排序运行(假设你得到一个大致对数的行为,因为你并不是运气不好或者你先输入了输入),调用堆栈不是问题,因为它只是以对数方式增长.更令人担忧的是,频繁调用@它的左参数是线性的.一种常见的优化技术是将函数定义为不返回列表,而是将"输入添加到累加器列表":

let rec quicksort list accu =
    match list with
    | [] -> accu
    | element::[] -> element::accu
    | pivot::rest ->
        let left, right = List.partition (fun element -> element < pivot) rest in
        let sorted_right = quicksort right accu in
        quicksort left (pivot :: sorted_right)
let qsort li = quicksort li []
Run Code Online (Sandbox Code Playgroud)

当然这可以再次变成CPS:

let rec quicksort list accu cont =
    match list with
    | [] -> cont accu
    | element::[] -> cont (element::accu)
    | pivot::rest ->
        let left, right = List.partition (fun element -> element < pivot) rest in
        quicksort right accu (fun sorted_right ->
        quicksort left (pivot :: sorted_right) cont)
let qsort li = quicksort li [] (fun x -> x)    
Run Code Online (Sandbox Code Playgroud)

现在最后一个技巧是通过将延续转换为数据结构来"延迟"延续(假设数据结构的分配比闭包的分配稍微有效):

type 'a cont =
  | Left of 'a list * 'a * 'a cont
  | Return
let rec quicksort list accu cont =
    match list with
    | [] -> eval_cont cont accu
    | element::[] -> eval_cont cont (element::accu)
    | pivot::rest ->
        let left, right = List.partition (fun element -> element < pivot) rest in
        quicksort right accu (Left (left, pivot, cont))
and eval_cont = function
  | Left (left, pivot, cont) ->
    (fun sorted_right -> quicksort left (pivot :: sorted_right) cont)
  | Return -> (fun x -> x)
let qsort li = quicksort li [] Return
Run Code Online (Sandbox Code Playgroud)

最后,我选择了function .. fun样式,eval_cont以显示那些只是CPS版本的代码片段,但以下版本可能通过arity-raising更好地优化:

and eval_cont cont accu = match cont with
  | Left (left, pivot, cont) ->
    quicksort left (pivot :: accu) cont
  | Return -> accu
Run Code Online (Sandbox Code Playgroud)