Реализация хвостовой рекурсивной версии функции быстрой сортировки в F#/OCaml

Можно ли реализовать хвостовую рекурсивную версию алгоритма быстрой сортировки (через шаблон продолжения)? И если да, то как это реализовать?

Обычная (не оптимизированная) версия:

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``

person Yet Another Geek    schedule 12.04.2011    source источник
comment
Второй случай не является строго обязательным, так как третий случай подойдет для этого. Я бы не назвал быструю сортировку хвостовой рекурсией против своей природы с оптимизированными продолжениями. Вы должны понимать, что все, что вы не размещаете в стеке, просто попадет в кучу.   -  person Pascal Cuoq    schedule 12.04.2011
comment
В этом нет необходимости, но поскольку я не знаю, как это будет скомпилировано, я подумал, что это сэкономит несколько вызовов функций. (хотя это не так важно из-за времени выполнения O (1))   -  person Yet Another Geek    schedule 12.04.2011
comment
Я имел в виду обычную версию как неоптимизированную. Я понимаю, что в любом случае может быть больше использования памяти. Существуют также некоторые ограничения со стеком, которого нет в куче, и, таким образом, это может помочь уменьшить количество переполнений стека.   -  person Yet Another Geek    schedule 12.04.2011
comment
У вас есть реальное приложение для этого? List.sort не должен вызывать переполнение стека в F#. В OCaml, я думаю, быстрее конвертировать в массив и сортировать массив (когда у вас огромный список).   -  person Laurent    schedule 12.04.2011
comment
Мне просто интересно, возможно ли это сделать.   -  person Yet Another Geek    schedule 12.04.2011
comment
То, чего у вас нет, не совсем быстрая сортировка — опорная точка должна выбираться случайным образом. То, что у вас есть, будет демонстрировать худшее поведение O (n ^ 2) при отсортированном или почти отсортированном вводе. Вы можете получить более быструю сортировку, используя одну из следующих стратегий: 1) вместо этого используйте сортировку слиянием или 2) выгрузите список в массив, отсортируйте указанный массив, преобразуйте обратно в список. И последнее: list1 @ [x] @ list2 работает медленно, вместо этого используйте list1 @ x::list2.   -  person Juliet    schedule 13.04.2011
comment
Если предположить, что список перемешивается и, следовательно, должен быть отсортирован, выбор первого элемента в списке так же хорош, как выбор элемента внутри сводной точки. Обычный подход состоит в том, чтобы выбрать три числа (например, первое, среднее и последнее) и выбрать число, которое находится между наибольшим и наименьшим из этих трех чисел. Он будет работать плохо только в том случае, если список уже отсортирован, но в связанном списке, таком как F #, другой подход потребует гораздо больше работы. Что касается последнего, это было предложено только для демонстрации, так как никогда не следует использовать эту версию быстрой сортировки.   -  person Yet Another Geek    schedule 13.04.2011
comment
@Juliet: Этот алгоритм не является настоящей быстрой сортировкой, потому что он не использует секционирование на месте для сокращения операций ввода-вывода и, следовательно, он чрезвычайно медленный.   -  person J D    schedule 25.06.2011
comment
@Jon Я предполагаю, что List.partition был оптимизирован, поскольку он является частью стандартной библиотеки. Конечно, стандартный способ сделать это — преобразовать список в массив и выполнить редактирование на месте, но это не похоже на функциональный способ сделать это. С другой стороны, я использовал это не для скорости, а для того, чтобы узнать больше о CPS.   -  person Yet Another Geek    schedule 25.06.2011
comment
@Geek: Quicksort по своей сути является нечистым алгоритмом, потому что он основан на разделении на месте, чтобы минимизировать ввод-вывод, поэтому нет смысла создавать чистый алгоритм, который выглядит похоже, но жертвует эффективностью, и по-прежнему называть его быстрой сортировкой. Обязательно используйте его, чтобы узнать о CPS. Я просто предупреждаю, что называть это быстрой сортировкой неправильно.   -  person J D    schedule 04.07.2011


Ответы (3)


Прямой стиль:

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

Мой первый, наивный перевод очень похож на версию Лорана, за исключением немного странного отступа, чтобы показать, что вызовы с продолжениями на самом деле являются своего рода привязкой:

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)

В отличие от Лорана, я легко проверяю, что 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 []

Конечно, это можно снова превратить в 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)    

Теперь последний трюк — «дефункционализировать» продолжения, превратив их в структуру данных (предположим, что выделение структур данных немного более эффективно, чем выделение замыкания):

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

Наконец, я выбрал стиль function .. fun для eval_cont, чтобы было очевидно, что это всего лишь фрагменты кода из версии CPS, но следующая версия, вероятно, лучше оптимизирована за счет повышения арности:

and eval_cont cont accu = match cont with
  | Left (left, pivot, cont) ->
    quicksort left (pivot :: accu) cont
  | Return -> accu
person gasche    schedule 12.04.2011
comment
Почему выделение структур данных должно быть более эффективным, чем выделение замыкания? Должно быть почти так же, нет? - person Laurent; 12.04.2011
comment
Попробовал вашу последнюю функцию, чуть быстрее наивного перевода, но особой разницы нет. Обе версии по-прежнему являются чрезвычайно медленными функциями сортировки (как и исходная версия). В любом случае, это все еще веселое упражнение. - person Laurent; 12.04.2011
comment
@Laurent Меня больше интересовало бы сравнение функции, использующей аккумулятор в прямом стиле, по сравнению с. самый простой. - person gasche; 12.04.2011
comment
@Laurent, Дефункционализация по Рейнольдсу — это последний шаг в компиляции функционального языка более высокого порядка для кода, который не требует оборудования, необходимого для реализации замыканий. Это фактически позволяет легко компилировать такие языки, как Scheme, которые имеют первоклассные продолжения вплоть до C или ассемблера. - person ; 13.04.2011

Быстрая попытка, кажется, работает:

let rec quicksort list cont =
    match list with
    | [] -> cont []
    | element::[] -> cont [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``
            (fun x -> quicksort ``elements larger or equal to pivot`` (fun y -> cont (x @ [pivot] @ y)))

> quicksort [2; 6; 3; 8; 5; 1; 9; 4] id;;
val it : int list = [1; 2; 3; 4; 5; 6; 8; 9]

Изменить:

Конечно, этот код крайне неэффективен. Я надеюсь, что никто не будет использовать его в реальном коде. Код было нетрудно написать, но продолжения могут быть трудными для чтения и могут быть подвержены ошибкам (легко забыть вызов cont). Если вы хотите больше поиграть, вы можете написать монаду-продолжение (Брайан написал запись в блоге об этом).

person Laurent    schedule 12.04.2011

Монада продолжения (украдена отсюда) также можно использовать (обычно это делает код более читабельным):

type ContinuationMonad() =
    // ma -> (a -> mb) -> mb
    member this.Bind (m, f) = fun c -> m (fun a -> f a c)
    // a -> ma
    member this.Return x = fun k -> k x
    // ma -> ma
    member this.ReturnFrom m = m
let cont = ContinuationMonad()

// Monadic definition of QuickSort
// it's shame F# doesn't allow us to use generic monad code
// (we need to use 'cont' monad here)
// otherwise we could run the same code as Identity monad, for instance
// producing direct (non-cont) behavior
let rec qsm = function
     |[]    -> cont.Return []
     |x::xs -> cont {
        let l,r = List.partition ((>=)x) xs
        let! ls = qsm l 
        let! rs = qsm r
        return (ls @ x :: rs) }

// Here we run our cont with id
let qs xs = qsm xs id     

printf "%A" (qs [2;6;3;8;5;1;9;4])
person Ed'ka    schedule 14.04.2011
comment
Я знаю, что оператор (›=) взят из Haskell, но что он делает? - person Yet Another Geek; 15.04.2011
comment
Это обычный арифметический оператор F# больше или равно (не имеет ничего общего с оператором Haskell (››=), который представлен здесь методом this.Bind) - person Ed'ka; 16.04.2011
comment
Ах да, возможно, мне следовало уделить больше внимания сематике вызова функции, а не синтаксису - person Yet Another Geek; 16.04.2011