Хвостовая рекурсивная сортировка слиянием в OCaml

Я пытаюсь реализовать хвостовую рекурсивную функцию сортировки списков в 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
;;

Тем не менее, похоже, что на самом деле это не хвостовая рекурсия, поскольку я сталкиваюсь с ошибкой «Переполнение стека во время оценки (циклическая рекурсия?)».

Не могли бы вы помочь мне определить нерекурсивный вызов в этом коде? Я довольно много искал, но не нашел. Может, это привязка let в функции sort?


person Clément    schedule 27.03.2010    source источник


Ответы (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)
person Brian    schedule 27.03.2010
comment
О, я думаю, что понимаю; Благодарность! Но тогда как я могу сделать свою функцию рекурсивной? - person Clément; 27.03.2010
comment
Не могли бы вы объяснить, почему продолжения действительно делают функцию хвостовой рекурсивной? Или они просто перемещают процесс захвата кадра стека из стека (потенциально переполняющегося) в сгенерированное закрытие? - person Dario; 27.03.2010
comment
Хммм, я думаю, это должно сработать, но я действительно не понимаю, что делает функция k. Не могли бы вы немного объяснить? Большое спасибо! Я тестировал его, но это не решает проблему переполнения ... Есть идеи, почему? - person Clément; 27.03.2010
comment
Я не проверял код, поэтому, возможно, я ошибся. :) Лучшее объяснение стратегии продолжения, которое у меня есть, находится здесь: lorgonblog. space.live.com/blog/cns!701679AD17B6D310!170.entry - person Brian; 27.03.2010
comment
Думаю, это ошибка оптимизации tail-rec в Caml. В любом случае док. отлично, спасибо большое! - person Clément; 01.04.2010

Сортировка слиянием по своей сути не является хвостовой рекурсивной. Для сортировки требуется два рекурсивных вызова, а при любом выполнении любой функции не более одного динамического вызова. может быть в хвостовой позиции. (split также вызывается из не хвостовой позиции, но поскольку он должен использовать постоянное пространство стека, что должно быть в порядке).

Убедитесь, что вы используете последнюю версию OCaml. В версиях 3.08 и более ранних List.rev может разрушить стек. Эта проблема исправлена ​​в версии 3.10. Используя версию 3.10.2, я могу отсортировать список из десяти миллионов элементов, используя ваш код. На это уходит пара минут, но я не взрываю стек. Так что я надеюсь, что ваша проблема просто в том, что у вас старая версия OCaml.

Если нет, следующим хорошим шагом будет установка переменной окружения

OCAMLRUNPARAM=b=1

который даст трассировку стека, когда вы взорвете стек.

Я также хотел бы знать длину сортируемых вами массивов; хотя сортировка слиянием не может быть хвостовой рекурсивной, ее не хвостовая природа должна стоить вам логарифмического пространства стека.

Если вам нужно отсортировать более 10 миллионов элементов, возможно, вам стоит поискать другой алгоритм? Или, если хотите, вы можете вручную выполнить сортировку слиянием с CPS-преобразованием, что даст хвостовую рекурсивную версию за счет выделения контиций в куче. Но я предполагаю, что в этом не будет необходимости.

person Norman Ramsey    schedule 27.03.2010
comment
Хммм, раз уж сплит не на последней позиции, он засчитывается? (Я имею в виду, насколько я понимаю, компилятор должен уметь обнаруживать хвостовую рекурсивную функцию и преобразовывать ее в цикл; тогда только последний вызов будет иметь значение) Кроме того, использование продолжений должно сделать функцию хвостовой рекурсивной, не следует не так ли? - person Clément; 29.03.2010
comment
Я использую OCaml v11.0, и я взрываю стек при запуске кода на 10 ^ 6 элементах. Мне нужно отсортировать от 5 до 10 миллионов элементов. - person Clément; 29.03.2010
comment
Наконец, моя проблема в том, что даже при использовании продолжений я взрываю стек. Есть идеи, почему? - person Clément; 29.03.2010
comment
Кстати, есть ли способ получить обратную трассировку при запуске программ верхнего уровня? - person Clément; 29.03.2010
comment
@CFP: установка переменной окружения должна дать вам обратную трассировку. Если нет, просто скомпилируйте тестовый пример и запустите его как отдельный двоичный файл. - person Norman Ramsey; 29.03.2010