A few days ago, I called the following Haskell function, reminiscent of
Quicksort and considered the epitome of beautiful code by many, unusable for
taking always " space and time":
Moreover, I referred to this version (using a C-like DSEL in Haskell)
due to Lennart Augustsson
as a usable one (as opposed to a algorithm):
qsortM :: forall i a m r arr .
(MonadRef m r, Num i, Ix i, MArray arr a m, Ord a Bool) =>
arr i a -> m (arr i a)
qsortM ma = runE $ do
(lb, ub) <- embed $ getBounds ma
let mlb = pure0 lb
mub = pure0 ub
a <- liftArray ma
let qsort' l r =
if1 (r > l) $ do
i <- auto l
j <- auto (r+1)
let v = a[l] :: E m a
iLTj = i < (j :: E m i)
while iLTj $ do
while ((i += 1) < mub && a[i] < v)
skip
while (a[j -= 1] > v)
skip
if1 iLTj $ do
a[i] =:= a[j]
a[l] =:= a[j]
qsort' l (j-1)
qsort' i r
qsort' mlb mub
return ma
I was wrong. Instead of analyzing it with some care, I believed the
claims I read long ago in some now forgotten blog. I just saw the list
concatenations and reacted without thinking.
Finding an upper bound for the number of comparisons in the best case is easy.
In fact, while I'am at it, I'll find a bound for the number of cons
operations, which is clearly greater than that of comparisons.
Noting the number of conses C(n) for a list of size n, for n > 3 (there are a
few irregularities for n = 1 to 3 because no cons is needed when appending or
prepending [] to another list),
, where the problem for a list
of length n has been divided into two halves (lists of length p and q) plus a
lone pivot element. p (resp. q) conses are needed to build the list passed to the
recursive call to qsort, C(p) (C(q)) conses are needed for the subproblem, and
finally p + 1 conses are needed to concatenate the resulting sorted sublists.
The problem is how to find p and q. For the best case analysis,
. The n list can be divided into
the desired three parts as follows:
Therefore,
(There are two choices for the number of conses in the final list
concatenation, differing by 1. The above is a conservative one, which will
give an upper bound.)
Even after referring to Graham & Knuth & Patashnik,
I saw no immediate way to solve this recurrence (please drop a comment if you
have a closed-form solution). Fortunately, I just need an upper bound so I can
use
which is easily proved by induction, since all I've done is to remove the
floor operations.
The above program thus uses at most space and
time. The upper bound is satisfactory enough; in fact, it's the same as for
the real quicksort, and, furthermore, the lowest achievable bound for a
comparison sort.
So far, I've assumed that the program evaluates everything eagerly, which
might not be the case in Haskell, as it has a non-strict semantics (which is
not exactly the same as being lazy).
Let's see if GHC is really smart and can recognize that qsort(l) is just a
permutation of l to skip the sort altogether when some operation that doesn't
depend on the order is performed (this is quite close to proving that the
qsort function actually sorts the list, and would be an amazing exploit
indeed).
To that end, I first rewrite the function in OCaml; if GHC is able to do less
work thanks to its non-strict semantics, the solution will scale better than
with OCaml:
It's very similar to the Haskell one; the main difference is in the signs used
with List.filter: ((>) x) means "x is greater than _", as opposed to "_ is
greater than x" in Haskell.
The main function just creates a list of random integers, sums them up, and
finally adds the numbers from the sorted list --- obtaining the same number, I
hope (the sums are printed to stdout to make sure that computation is not
omitted).
Here are the times I get:
The GHC binary takes nearly 800MB to sort a 3 million list, which explains why it
suffers as n increases.
Well, this isn't... what was it, GHC 11? Being lazy does pay off if you try to
get the nth (and in particular the first) element, though; using