На веб-сайте Haskell представлена очень привлекательная функция быстрой сортировки из 5 строк , как показано ниже.
quicksort [] = []
quicksort (p:xs) = (quicksort lesser) ++ [p] ++ (quicksort greater)
where
lesser = filter (< p) xs
greater = filter (>= p) xs
Они также включают «Истинную быструю сортировку в C» .
// To sort array a[] of size n: qsort(a,0,n-1)
void qsort(int a[], int lo, int hi)
{
int h, l, p, t;
if (lo < hi) {
l = lo;
h = hi;
p = a[hi];
do {
while ((l < h) && (a[l] <= p))
l = l+1;
while ((h > l) && (a[h] >= p))
h = h-1;
if (l < h) {
t = a[l];
a[l] = a[h];
a[h] = t;
}
} while (l < h);
a[hi] = a[l];
a[l] = p;
qsort( a, lo, l-1 );
qsort( a, l+1, hi );
}
}
Ссылка под версией C ведет на страницу, на которой указано : «Быстрая сортировка, указанная во Введении, не является« настоящей »быстрой сортировкой и не масштабируется для более длинных списков, как это делает код c».
Почему указанная выше функция Haskell не является настоящей быстрой сортировкой? Как он не масштабируется для более длинных списков?
O(N^2)
время выполнения.Ответы:
Настоящая быстрая сортировка имеет два прекрасных аспекта:
Короткий пример Haskell демонстрирует (1), но не (2). Как выполняется (2), может быть неочевидно, если вы еще не знакомы с техникой!
источник
Истинная быстрая сортировка на месте в Haskell:
источник
unstablePartition
очень похож наpartition
forquicksort
, но не гарантирует, что элемент вm
позиции th является справедливымp
.Вот транслитерация «настоящего» кода быстрой сортировки на C в Haskell. Соберитесь.
Было весело, правда? Я фактически вырезал это большое значение
let
в начале, а такжеwhere
в конце функции, определив все помощники, чтобы сделать предыдущий код более красивым.И вот тупой тест, чтобы убедиться, что это работает.
Я не очень часто пишу императивный код на Haskell, поэтому я уверен, что есть много способов очистить этот код.
Ну и что?
Вы заметите, что приведенный выше код очень и очень длинный. Его суть примерно равна длине кода C, хотя каждая строка часто более подробна. Это потому, что C тайно делает много неприятных вещей, которые вы можете принять как должное. Например,
a[l] = a[h];
. Это обращается к изменяемым переменнымl
иh
, затем обращается к изменяемому массивуa
, а затем изменяет изменяемый массивa
. Святая мутация, Бэтмен! В Haskell изменение и доступ к изменяемым переменным явны. «Поддельный» qsort привлекателен по разным причинам, но главная из них - это то, что он не использует мутацию; это добровольное ограничение упрощает понимание с первого взгляда.источник
На мой взгляд, утверждение, что это «не настоящая быстрая сортировка», является преувеличением. Я считаю, что это правильная реализация алгоритма быстрой сортировки , но не особо эффективная.
источник
Я думаю, что этот аргумент пытается привести к тому, что причина, по которой обычно используется быстрая сортировка, заключается в том, что она на месте и в результате довольно удобна для кеширования. Поскольку у вас нет этих преимуществ со списками Haskell, его основная причина существования исчезла, и вы также можете использовать сортировку слиянием, которая гарантирует O (n log n) , тогда как с быстрой сортировкой вам либо придется использовать рандомизацию, либо сложную схемы разбиения, позволяющие избежать времени выполнения O (n 2 ) в худшем случае.
источник
Благодаря ленивому вычислению программа на Haskell не выполняет (почти не может ) делать то, что кажется.
Рассмотрим эту программу:
На нетерпеливом языке сначала
quicksort
бегал, потомshow
, потомputStrLn
. Аргументы функции вычисляются перед запуском этой функции.В Haskell все наоборот. Функция запускается первой. Аргументы вычисляются только тогда, когда функция их фактически использует. И составной аргумент, например список, вычисляется по частям по мере использования каждой его части.
Итак, первое , что происходит в этой программе, - это
putStrLn
запускается.Реализация GHC
putStrLn
работает путем копирования символов аргумента String в выходной буфер. Но когда он входит в этот цикл,show
еще не запущен. Поэтому, когда речь идет , чтобы скопировать первый символ из строки, Хаскелл оценивает долю отshow
иquicksort
вызовов , необходимых для вычисления этого символа . ЗатемputStrLn
переходит к следующему персонажу. Таким образом, выполнение всех трех функцийputStrLn
-show
, иquicksort
- чередуется.quicksort
выполняется постепенно, оставляя график неоцененных переходов, поскольку он запоминает, где он остановился.Теперь это сильно отличается от того, что вы могли бы ожидать, если вы когда-либо были знакомы с любым другим языком программирования. Трудно представить себе, как на
quicksort
самом деле ведет себя Haskell с точки зрения доступа к памяти или даже порядка сравнений. Если бы вы могли наблюдать только за поведением, а не за исходным кодом, вы бы не узнали, что он делает, как быструю сортировку .Например, версия C быстрой сортировки разделяет все данные перед первым рекурсивным вызовом. В версии для Haskell первый элемент результата будет вычислен (и может даже появиться на вашем экране) до того, как будет завершена работа первого раздела, то есть до того, как будет выполнена какая-либо работа
greater
.PS Код Haskell был бы более похож на быструю сортировку, если бы выполнял то же количество сравнений, что и быстрая сортировка; код в том виде, в котором он написан, выполняет в два раза больше сравнений, потому что
lesser
иgreater
указаны для независимого вычисления, выполняя два линейных сканирования по списку. Конечно, в принципе компилятор может быть достаточно умен, чтобы исключить лишние сравнения; или код можно изменить для использованияData.List.partition
.PPS Классическим примером алгоритмов Haskell, которые ведут себя не так, как вы ожидали, является решето Эратосфена для вычисления простых чисел.
источник
primes = unfoldr (\(p:xs)-> Just (p, filter ((> 0).(`rem` p)) xs)) [2..]
, его ближайшая проблема , возможно, была бы более ясной. И это до того, как мы рассмотрим переход на настоящий алгоритм сита.putStrLn
какое-то преобразованное приложениеshow
к преобразованному приложению илиquicksort
в литерал списка --- и именно это он делает! (перед оптимизацией --- но когда-нибудь сравните код C с оптимизированным ассемблером!). Может быть, вы имеете в виду «благодаря ленивому вычислению программа на Haskell не делает то, что похожий на вид код делает на других языках»?Я считаю, что причина, по которой большинство людей говорят, что красивая Haskell Quicksort не является «настоящей» Quicksort, заключается в том, что она не на месте - очевидно, этого не может быть при использовании неизменяемых типов данных. Но есть также возражение, что это не «быстро»: отчасти из-за дорогостоящего ++, а также из-за утечки места - вы цепляетесь за список ввода, выполняя рекурсивный вызов меньших элементов, и в некоторых случаях - например, когда список уменьшается - это приводит к квадратичному использованию пространства. (Вы могли бы сказать, что запуск в линейном пространстве - это самое близкое к "на месте" использование неизменяемых данных.) Есть изящные решения обеих проблем, используя накопление параметров, кортежи и слияние; см. S7.6.1 Ричарда Берда '
источник
Это не идея изменения элементов на месте в чисто функциональных настройках. Альтернативные методы в этом потоке с изменяемыми массивами потеряли дух чистоты.
Есть как минимум два шага для оптимизации базовой версии (которая является наиболее выразительной версией) быстрой сортировки.
Оптимизируйте конкатенацию (++), которая является линейной операцией, с помощью аккумуляторов:
Оптимизируйте тройную быструю сортировку (трехстороннее разделение, упомянутое Бентли и Седжвиком) для обработки повторяющихся элементов:
Объедините 2 и 3, обратитесь к книге Ричарда Берда:
Или, альтернативно, если дублированных элементов не большинство:
К сожалению, медиана трех не может быть реализована с таким же эффектом, например:
потому что он по-прежнему плохо работает в следующих 4 случаях:
[1, 2, 3, 4, ...., n]
[n, n-1, n-2, ..., 1]
[m-1, m-2, ... 3, 2, 1, m + 1, m + 2, ..., n]
[n, 1, n-1, 2, ...]
Все эти 4 случая хорошо обрабатываются императивным подходом медианы трех.
На самом деле, наиболее подходящим алгоритмом сортировки для чисто функциональной настройки по-прежнему является сортировка слиянием, но не быстрая сортировка.
Для получения подробной информации, пожалуйста, посетите мои текущие письма по адресу: https://sites.google.com/site/algoxy/dcsort
источник
Нет четкого определения того, что является настоящей быстрой сортировкой, а что нет.
Они называют это не настоящей быстрой сортировкой, потому что она не выполняет сортировку на месте:
источник
Потому что выбор первого элемента из списка приводит к очень плохому выполнению. Используйте медианное значение 3: первое, среднее, последнее.
источник
O(n^2)
Попросите кого-нибудь написать quicksort на Haskell, и вы получите, по сути, ту же программу - очевидно, что это quicksort. Вот некоторые преимущества и недостатки:
Плюсы: он улучшает "настоящую" быструю сортировку, будучи стабильным, т. Е. Сохраняет порядок следования между равными элементами.
Pro: Тривиально обобщить на трехстороннее разбиение (<=>), которое позволяет избежать квадратичного поведения из-за того, что какое-то значение встречается O (n) раз.
Pro: его легче читать, даже если нужно было включить определение фильтра.
Против: использует больше памяти.
Con: Это дорого обобщать выбор поворота путем дальнейшего отбора проб, которые могли бы избежать квадратичного поведения на некоторых низкоэнтропийных порядками.
источник