In Wikibooks' Haskell, there is the following claim:
Data.List offers a sort function for sorting lists. It does not use quicksort; rather, it uses an efficient implementation of an algorithm called mergesort.
What is the underlying reason in Haskell to use mergesort over quicksort? Quicksort usually has better practical performance, but maybe not in this case. I gather that the in-place benefits of quicksort are hard (impossible?) to do with Haskell lists.
There was a related question on softwareengineering.SE, but it wasn't really about why mergesort is used.
I implemented the two sorts myself for profiling. Mergesort was superior (around twice as fast for a list of 2^20 elements), but I'm not sure that my implementation of quicksort was optimal.
Edit: Here are my implementations of mergesort and quicksort:
mergesort :: Ord a => [a] -> [a] mergesort [] = [] mergesort [x] = [x] mergesort l = merge (mergesort left) (mergesort right) where size = div (length l) 2 (left, right) = splitAt size l merge :: Ord a => [a] -> [a] -> [a] merge ls [] = ls merge [] vs = vs merge first@(l:ls) second@(v:vs) | l < v = l : merge ls second | otherwise = v : merge first vs quicksort :: Ord a => [a] -> [a] quicksort [] = [] quicksort [x] = [x] quicksort l = quicksort less ++ pivot:(quicksort greater) where pivotIndex = div (length l) 2 pivot = l !! pivotIndex [less, greater] = foldl addElem [[], []] $ enumerate l addElem [less, greater] (index, elem) | index == pivotIndex = [less, greater] | elem < pivot = [elem:less, greater] | otherwise = [less, elem:greater] enumerate :: [a] -> [(Int, a)] enumerate = zip [0..]
Edit 2 3: I was asked to provide timings for my implementations versus the sort in Data.List
. Following @Will Ness' suggestions, I compiled this gist with the -O2
flag, changing the supplied sort in main
each time, and executed it with +RTS -s
. The sorted list was a cheaply-created, pseudorandom [Int]
list with 2^20 elements. The results were as follows:
Data.List.sort
: 0.171smergesort
: 1.092s (~6x slower than Data.List.sort
)quicksort
: 1.152s (~7x slower than Data.List.sort
)If we want the relative order of equal elements after sorting the data to be preserved, merge sort would be the preferred choice since merge sort is a stable sorting algorithm while quicksort isn't. Although quicksort can be modified to be stable, it is hard to implement and reduces the algorithm's efficiency.
Merge sort requires more space as it creates an extra array for storing, and no matter what it will compare every item. Quick sort on the other hand does not require extra space, and doesn't swap or compare more than necessary.
The main difference between quicksort and merge sort is that the quicksort sorts the elements by comparing each element with an element called a pivot while merge sort divides the array into two subarrays again and again until one element is left.
Quick sort is an in-place sorting algorithm. In-place sorting means no additional storage space is needed to perform sorting. Merge sort requires a temporary array to merge the sorted arrays and hence it is not in-place giving Quick sort the advantage of space.
In imperative languages, Quicksort is performed in-place by mutating an array. As you demonstrate in your code sample, you can adapt Quicksort to a pure functional language like Haskell by building singly-linked lists instead, but this is not as fast.
On the other hand, Mergesort is not an in-place algorithm: a straightforward imperative implementation copies the merged data to a different allocation. This is a better fit for Haskell, which by its nature must copy the data anyway.
Let's step back a bit: Quicksort's performance edge is "lore" -- a reputation built up decades ago on machines much different from the ones we use today. Even if you use the same language, this kind of lore needs rechecking from time to time, as the facts on the ground can change. The last benchmarking paper I read on this topic had Quicksort still on top, but its lead over Mergesort was slim, even in C/C++.
Mergesort has other advantages: it doesn't need to be tweaked to avoid Quicksort's O(n^2) worst case, and it is naturally stable. So, if you lose the narrow performance difference due to other factors, Mergesort is an obvious choice.
I think @comingstorm's answer is pretty much on the nose, but here's some more info on the history of GHC's sort function.
In the source code for Data.OldList
, you can find the implementation of sort
and verify for yourself that it's a merge sort. Just below the definition in that file is the following comment:
Quicksort replaced by mergesort, 14/5/2002. From: Ian Lynagh <[email protected]> I am curious as to why the List.sort implementation in GHC is a quicksort algorithm rather than an algorithm that guarantees n log n time in the worst case? I have attached a mergesort implementation along with a few scripts to time it's performance...
So, originally a functional quicksort was used (and the function qsort
is still there, but commented out). Ian's benchmarks showed that his mergesort was competitive with quicksort in the "random list" case and massively outperformed it in the case of already sorted data. Later, Ian's version was replaced by another implementation that was about twice as fast, according to additional comments in that file.
The main issue with the original qsort
was that it didn't use a random pivot. Instead it pivoted on the first value in the list. This is obviously pretty bad because it implies performance will be worst case (or close) for sorted (or nearly sorted) input. Unfortunately, there are a couple of challenges in switching from "pivot on first" to an alternative (either random, or -- as in your implementation -- somewhere in "the middle"). In a functional language without side effects, managing a pseudorandom input is a bit of a problem, but let's say you solve that (maybe by building a random number generator into your sort function). You still have the problem that, when sorting an immutable linked list, locating an arbitrary pivot and then partitioning based on it will involve multiple list traversals and sublist copies.
I think the only way to realize the supposed benefits of quicksort would be to write the list out to a vector, sort it in place (and sacrifice sort stability), and write it back out to a list. I don't see that that could ever be an overall win. On the other hand, if you already have data in a vector, then an in-place quicksort would definitely be a reasonable option.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With