Merge sort (Haskell)

From LiteratePrograms

Jump to: navigation, search
Other implementations: ACL2 | C | C++ | dc | Eiffel | Erlang | Haskell | Java | JavaScript | Lisp | OCaml | Oz | Perl | Prolog | Python | Ruby | Scheme

This is an implementation of the merge sort algorithm in Haskell. The sorting predicate is user-specified; use <= to provide the usual stable sorting of numbers. For pedagogical reasons, this implementation is fairly verbose. More concise versions, which make better use of Haskell's capabilities, are also possible.

Contents

Implementation

mergesort

The mergesort function applies a predicate to a list of items that can be compared using that predicate. For a simple list (one element or empty), we just return a duplicate of the current list. For longer lists, we split the list into two halves, recurse on each half, then merge the two halves according to the predicate:

<<mergesort.hs>>=
split
merge
mergesort :: (a -> a -> Bool) -> [a] -> [a]
mergesort pred []   = []
mergesort pred [x]  = [x]
mergesort pred xs = merge pred (mergesort pred xs1) (mergesort pred xs2)
  where
    (xs1,xs2) = split xs

split

To break the list into two halves without having to first measure its length (an extra traversal) we count in twos over it, and use another pointer into the list to advance in steps of one to get the two halves, keeping the original order to ensure a stable sort:

<<split>>=
split :: [a] -> ([a],[a])
split xs = go xs xs where
  go (x:xs) (_:_:zs) = (x:us,vs) where (us,vs)=go xs zs
  go    xs   _       = ([],xs)

Another way of splitting the list might be to put all odd-positioned elements in one sublist, and the rest in another. For example, we might have written split as

split :: [a] -> ([a],[a])   
split (x:y:zs) = (x:xs,y:ys) where (xs,ys) = split zs
split xs       = (xs,[]) 

This, however, is not stable: a list that is already partially-ordered according to the predicate might get rearranged. For example, given a binary predicate string-length<= that returns True iff the first argument is not longer than the second, and the list ["aaa", "bbb", "ccc"], you get ["aaa", "ccc", "bbb"] back. The first function avoids this problem. A somewhat simpler alternative would have been to use the standard Haskell Prelude function splitAt to break the list in two (but then we would have had to explain how splitAt works – and more importantly, to traverse the list in full, in order to find out its length).

merge

Merge is the heart of the algorithm and operates by interleaving the elements of two ordered lists in such a way that the combined list is ordered. This process takes only linear (O(n)) time. The merge function takes two lists. If either list is empty, we return a duplicate of the other as the merged result. Otherwise, the lead elements of each list are compared. A true result from the predicate indicates that the first element should precede the second in sorting order (assuming it works like ≤ predicate; the more usual way of defining this would be to assume it were like < and compare pred y x instead – the extra caution is to preserve the original order of elements where possible). The "winning" element is then removed from its list and prepended to the rest of the result, which we get from merging the remainder of the lists. This is your regular tail recursion modulo cons right there, especially that Haskell is lazy and the result is consumed on demand – head first, tail later – triggering the actual recursive call in truly a tail position, after the lazy cons (:) data constructor has been consumed / destructed / traversed over.

<<merge>>=
merge :: (a -> a -> Bool) -> [a] -> [a] -> [a]
merge pred xs []         = xs
merge pred [] ys         = ys
merge pred (x:xs) (y:ys) =
  case pred x y of
    True  -> x: merge pred xs (y:ys)
    False -> y: merge pred (x:xs) ys

Testing

The mergesort implementation can easily be tested in an interactive Haskell environment such as Hugs or the Glasgow Haskell Compiler's GHCi. For example, after loading mergesort.hs into GHCi, typing

mergesort (<=) [1, 5, 6, 4, 3]

at the prompt produces the sorted list

[1,3,4,5,6]

Iterative implementation

As written, mergesort is doubly-recursive. Another way it can be implemented is in the bottom-up, iterative way,

mergesort pred [] = []
mergesort pred xs = go [[x] | x <- xs]
  where
    go xs@(_:_:_)  = go (pairs xs)
    go [xs]        = xs
    pairs (x:y:xs) = merge pred x y : pairs xs
    pairs xs       = xs

using the same merge function as above, turning each element of the argument list initially into a singleton list using a list-comprehension expression, and then combining these lists in a pair-wise manner with the merge function, resulting in fewer and fewer lists until only one is left, which is the sorted list – the result we seek.

Download code
Views