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 ef
Many arguments on why Quicksort is not used in Haskell seem plausible. However, at least Quicksort is not slower than Mergesort for the random case. Based on the implementation given in Richard Bird's book, Thinking Functionally in Haskell, I made a 3-way Quicksort:
tqsort [] = []
tqsort (x:xs) = sortp xs [] [x] []
where
sortp [] us ws vs = tqsort us ++ ws ++ tqsort vs
sortp (y:ys) us ws vs =
case compare y x of
LT -> sortp ys (y:us) ws vs
GT -> sortp ys us ws (y:vs)
_ -> sortp ys us (y:ws) vs
I benchmarked a few cases, e.g., lists of size 10^4 containing Int between 0 and 10^3 or 10^4, and so on. The result is the 3-way Quicksort or even Bird's version are better than GHC's Mergesort, something like 1.x~3.x faster than ghc's Mergesort, depending on the type of data (many repetitions? very sparse?). The following stats is generated by criterion:
benchmarking Data.List.sort/Diverse/10^5
time 223.0 ms (217.0 ms .. 228.8 ms)
1.000 R² (1.000 R² .. 1.000 R²)
mean 226.4 ms (224.5 ms .. 228.3 ms)
std dev 2.591 ms (1.824 ms .. 3.354 ms)
variance introduced by outliers: 14% (moderately inflated)
benchmarking 3-way Quicksort/Diverse/10^5
time 91.45 ms (86.13 ms .. 98.14 ms)
0.996 R² (0.993 R² .. 0.999 R²)
mean 96.65 ms (94.48 ms .. 98.91 ms)
std dev 3.665 ms (2.775 ms .. 4.554 ms)
However, there is another requirement of sort
stated in Haskell 98/2010: it needs to be stable. The typical Quicksort implementation using Data.List.partition
is stable, but the above one isn't.
Later addition: A stable 3-way Quicksort mentioned in the comment seems as fast as tqsort
here.
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 <igloo@earth.li>
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.
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.
Short answer:
Quicksort is advantageous for arrays (in-place, fast, but not worst-case optimal). Mergesort for linked lists (fast, worst-case optimal, stable, simple).
Quicksort is slow for lists, Mergesort is not in-place for arrays.
I am not sure, but looking at the code i don't think Data.List.sort
is Mergesort as we know it. It just makes a single pass starting with the sequences
function in a beautiful triangular mutual recursive fashion with ascending
and descending
functions to result in a list of already ascending or descending ordered chunks in the required order. Only then it starts merging.
It's a manifestation of poetry in coding. Unlike Quicksort, its worst case (total random input) has O(nlogn) time complexity, and best case (already sorted ascending or descending) is O(n).
I don't think any other sorting algorithm can beat it.
On a singly-linked list, mergesort can be done in place. What's more, naive implementations scan over half the list in order to get the start of the second sublist, but the start of the second sublist falls out as a side effect of sorting the first sublist and does not need extra scanning. The one thing quicksort has going over mergesort is cache coherency. Quicksort works with elements close to each other in memory. As soon as an element of indirection enters into it, like when you are sorting pointer arrays instead of the data itself, that advantage becomes less.
Mergesort has hard guarantees for worst-case behavior, and it's easy to do stable sorting with it.