2

I could not find my code anywhere on the net, so can you please tell me why or why not the function myMergeSort is a mergesort? I know my function myMergeSort sorts, but am not sure if it really sorts using the mergesort algorithm or if it is a different algorithm. I just began with Haskell a few days ago.

merge xs [] = xs
merge [] ys = ys
merge (x : xs) (y : ys)
    | x <= y = x : merge xs (y : ys)
    | otherwise = y : merge (x : xs) ys

myMergeSort :: [Int] -> [Int]
myMergeSort [] = []
myMergeSort (x:[]) = [x]
myMergeSort (x:xs) = foldl merge [] (map (\x -> [x]) (x:xs))

I have no questions about the merge function.

The following function mergeSortOfficial was the solution presented to us, I understand it but am not sure if I am implementing the mergesort algorithm in my function myMergeSort correctly or not.

Official solution - implemenation:

mergeSortOfficial [] = []
mergeSortOfficial (x : []) = [x]
mergeSortOfficial xs = merge
    (mergeSortOfficial (take ((length xs) ‘div‘ 2) xs))
    (mergeSortOfficial (drop ((length xs) ‘div‘ 2) xs))
user3069376
  • 1,023
  • 6
  • 8
  • 1
    Irrespective of whether `mergeSortOfficial` indeed implements MergeSort or not, what strikes me is that it recomputes the length of the list (twice!) at each call. That's very inefficient; the length should be computed once and for all at the beginning. – jub0bs Mar 11 '15 at 16:46
  • 1
    Doesn't this belongs on codereview? – Squidly Mar 11 '15 at 16:47
  • 5
    A merge sort divides the input in two *equal* (or almost equal) parts. This is important. You always "merge" with a one-element list. This doesn't have the required asymptotic complexity. – n. m. could be an AI Mar 11 '15 at 16:53
  • 2
    @MrBones not really. This code isn't working, for some definition of "working", so the question belongs here. – n. m. could be an AI Mar 11 '15 at 16:57
  • If you want to work bottom-up from a list of one-element lists, you can. You have a list of lists. Merge each even-numbered element with its odd-numbered neighbour. Repeat until you have just one list left. – n. m. could be an AI Mar 11 '15 at 17:07
  • Bottom-up is definitely the way to go. Then you never need to count elements at all. – Carl Mar 11 '15 at 17:30
  • 2
    I believe this is insertion sort in disguise: merges are done in the form `merge list [x]` which amounts to inserting `x` into an ordered `list`. The current code looks O(n^2). – chi Mar 11 '15 at 17:53
  • 1
    stable bottom-up mergesort: https://en.wikipedia.org/w/index.php?title=Haskell_98_features#Mergesort – Will Ness Mar 11 '15 at 19:20

2 Answers2

10

No, that's not mergeSort. That's insertionSort, which is essentially the same algorithm as bubbleSort, depending on how you stare at it. At each step, a singleton list is merged with the accumulated ordered-list-so-far, so, effectively, the element of that singleton is inserted.

As other commenters have already observed, to get mergeSort (and in particular, its efficiency), it's necessary to divide the problem repeatedly into roughly equal parts (rather than "one element" and "the rest"). The "official" solution gives a rather clunky way to do that. I quite like

foldr (\ x (ys, zs) -> (x : zs, ys)) ([], [])

as a way to split a list in two, not in the middle, but into elements in even and odd positions.

If, like me, you like to have structure up front where you can see it, you can make ordered lists a Monoid.

import Data.Monoid
import Data.Foldable
import Control.Newtype

newtype Merge x = Merge {merged :: [x]}
instance Newtype (Merge x) [x] where
  pack = Merge
  unpack = merged

instance Ord x => Monoid (Merge x) where
  mempty = Merge []
  mappend (Merge xs) (Merge ys) = Merge (merge xs ys) where
    -- merge is as you defined it

And now you have insertion sort just by

ala' Merge foldMap (:[]) :: [x] -> [x]

One way to get the divide-and-conquer structure of mergeSort is to make it a data structure: binary trees.

data Tree x = None | One x | Node (Tree x) (Tree x) deriving Foldable

I haven't enforced a balancing invariant here, but I could. The point is that the same operation as before has another type

ala' Merge foldMap (:[]) :: Tree x -> [x]

which merges lists collected from a treelike arrangement of elements. To obtain said arrangements, think "what's cons for Tree?" and make sure you keep your balance, by the same kind of twistiness I used in the above "dividing" operation.

twistin :: x -> Tree x -> Tree x   -- a very cons-like type
twistin x None        = One x
twistin x (One y)     = Node (One x) (One y)
twistin x (Node l r)  = Node (twistin x r) l

Now you have mergeSort by building a binary tree, then merging it.

mergeSort :: Ord x => [x] -> [x]
mergeSort = ala' Merge foldMap (:[]) . foldr twistin None

Of course, introducing the intermediate data structure has curiosity value, but you can easily cut it out and get something like

mergeSort :: Ord x => [x] -> [x]
mergeSort []   = []
mergeSort [x]  = [x]
mergeSort xs   = merge (mergeSort ys) (mergeSort zs) where
  (ys, zs) = foldr (\ x (ys, zs) -> (x : zs, ys)) ([], []) xs

where the tree has become the recursion structure of the program.

pigworker
  • 43,025
  • 18
  • 121
  • 214
  • 3
    *That's insertionSort, which is essentially the same algorithm as bubbleSort, depending on how you stare at it.* How do *you* stare at it? Bubble Sort and Insertion Sort may both have worst-case quadratic time complexity, but they're not the same algorithm. – jub0bs Mar 11 '15 at 18:15
  • Note: an advantage of sorting via `Tree` is that it's quite obviously terminating (all recursive calls are structural). That might not be a big deal in Haskell, but it's nice in total langauges like Coq and Agda. – András Kovács Mar 11 '15 at 18:23
  • Is that `mappend` associative? I could be convinced, but it's not obvious. – dfeuer Mar 11 '15 at 19:03
  • Unrelatedly, this is making me wonder if it would be possible to write an interesting incremental quicksort for `Data.Sequence` using median-of-medians to choose pivots and getting just precise enough to build 2-3 trees. – dfeuer Mar 11 '15 at 19:47
  • 1
    @Jubobs They have the same structure of comparisons and the same dataflow. See [this related answer of mine](http://stackoverflow.com/a/21879087/828361). We call it insertion sort when we focus on saying where each input goes, and we call it bubble sort when we focus on saying where each output comes from, but it's the same process. – pigworker Mar 11 '15 at 20:59
7

myMergeSort is not a correct merge sort. It is a correct insertion sort though. We start with an empty list, then insert the elements one-by-one into the correct position:

myMergeSort [2, 1, 4, 3] == 
foldl merge [] [[2], [1], [4], [3]] ==
((([] `merge` [2]) `merge` [1]) `merge` [4]) `merge` [3] == 
(([2] `merge` [1]) `merge` [4]) `merge` [3]
([1, 2] `merge` [4]) `merge` [3] == 
[1, 2, 4] `merge` [3] == 
[1, 2, 3, 4]

Since each insertion takes linear time, the whole sort is quadratic.

mergeSortOfficial is technically right, but it's inefficient. length takes linear time, and it's called at each level of recursion for the total length of the list. take and drop are also linear. The overall complexity remains the optimal n * log n, but we run a couple of unnecessary circles.

If we stick to top-down merging, we could do better with splitting the list to a list of elements with even indices and another with odd indices. Splitting is still linear, but it's only a single traversal instead of two (length and then take / drop in the official sort).

split :: [a] -> ([a], [a])
split = go [] [] where
  go as bs []     = (as, bs)
  go as bs (x:xs) = go (x:bs) as xs

mergeSortOfficial :: [Int] -> [Int]
mergeSortOfficial [] = []
mergeSortOfficial (x : []) = [x]
mergeSortOfficial xs = 
  let (as, bs) = split xs in
    merge (mergeSortOfficial as) (mergeSortOfficial bs)

As WillNess noted in the comments, the above split yields an unstable sort. We can use a stable alternative:

import Control.Arrow

stableSplit :: [a] -> ([a], [a])
stableSplit xs = go xs xs where
    go (x:xs) (_:_:ys) = first (x:) (go xs ys)
    go xs     ys       = ([], xs)

The best way is probably doing a bottom-up merge. It's the approach the sort in Data.List takes. Here we merge consecutive pairs of lists until there is only a single list left:

mergeSort :: Ord a => [a] -> [a]
mergeSort [] = []
mergeSort xs = mergeAll (map (:[]) xs) where
    mergePairs (x:y:ys) = merge x y : mergePairs ys
    mergePairs xs       = xs

    mergeAll [xs] = xs
    mergeAll xs   = mergeAll (mergePairs xs)

Data.List.sort works largely the same as above, except it starts with finding descending and ascending runs in the input instead of just creating singleton lists from the elements.

András Kovács
  • 29,931
  • 3
  • 53
  • 99
  • with this twisting top-down split, the sort is not stable. It's better to split in half without measuring the length at all, by advancing _two_ pointers, one at twice the speed of the other. bottom-up is best, esp. as you can separate out the "runs" in the initial pass, like is done in [data-ordlist's `nubSort`](https://hackage.haskell.org/package/data-ordlist-0.4.7.0/docs/Data-List-Ordered.html#v:nubSort). – Will Ness Mar 11 '15 at 19:12
  • Thanks for explaining. Just a small thing: Of course your second line should be: foldl merge [] [[2], [1], [4], [3]] == instead of foldl merge [] [[1], [2], [3], [4]] == – user3069376 Mar 11 '15 at 19:27
  • @WillNess that's a very nice idea! I added a split like that to the answer. – András Kovács Mar 11 '15 at 20:07
  • @AndrásKovács Where is `merge` defined, in your last code block? – jub0bs Mar 12 '15 at 19:15
  • @Jubobs it's assumed the same as in the question. – András Kovács Mar 12 '15 at 19:17