2

I'm working on an extra assignment where the partition function of a quicksort in SML may only be done with foldr, and no library functions may be used. I've gotten the partitioning down just fine, and have the fundamentals of quicksort just fine, but am running into problems where it seems to merge the wrong lists together/merge in the wrong order.

(*comp is a placeholder, I will be given a comparator function and list as input*)
fun comp a b = a > b;

(*Both partition functions are working as intended.*)
fun getGreater (a::b) c = foldr (fn (a, lst) => if (comp a c) then a::lst else lst) [] (a::b);
fun getLesserOrEqual (a::b) c = foldr (fn (a, lst) => if not (comp a c) then a::lst else lst) [] (a::b);

fun merge (a::b) (c::d) = foldr (op::) (c::d) (a::b);

fun tripleMerge (a::b) c (d::e) = merge (a::b) (c::(d::e));

(*Removes head, uses head as pivot. Recursive calls on results on getLesserOrEqual and getGreater*)
fun sort [] = [] | sort (a::b) = if ([a] = (a::b)) then [a] else
    tripleMerge (sort(getLesserOrEqual b a)) a (sort(getGreater b a));

As an example, here are some of the tests I'm running. When I follow my logic on paper, I don't get the same answers as the incorrect items:

sort [2, 6, 3, 6, 4, 100, 0];
val it = [0,2,3,6,4,6,100] : int list (*Incorrect*)

sort [1, 2, 3, 4, 5];
val it = [1,2,3,4,5] : int list

sort [5, 4, 3, 2, 1];
val it = [5,4,3,2,1] : int list (*incorrect*)

sort [1, 100, 10, 1000, 0];
val it = [0,1,10,100,1000] : int list

sort [1, 2, 1, 2, 1, 2, 5, 1];
val it = [1,1,1,1,2,2,2,5] : int list

Is my mistake obvious to anyone?

Gigaflop
  • 390
  • 1
  • 13
  • With the code that you have shown, when I run `sort [2, 6, 3, 6, 4, 100, 0];` I get the error message `uncaught exception Match [nonexhaustive match failure]` rather than an incorrectly sorted list. Are you sure that you posted the correct code? – John Coleman Feb 06 '17 at 21:41

2 Answers2

2

Here is some feedback:

  1. This isn't Quicksort.

  2. Instead of writing fun comp a b = a > b, you may refer to op > (a, b) (i.e. turn an operator into a function that takes a pair).

    I understand that your actual comparison function is given as input. A minor point of confusion might be that in the standard library, functions named compare return an order type, i.e. LESS, EQUAL or GREATER, rather than a bool. But never mind.

  3. As John says, there is no real reason to use the pattern a :: b when the function is well-defined for empty lists.

  4. Prefixing function names with get is somewhat redundant, since all (pure) functions get something as their main purpose.

  5. You could combine your two filtering functions by making a higher order variant:

    fun flip f x y = f y x
    fun filter p xs = foldr (fn (x, ys) => if p x then x::ys else ys) [] xs
    fun greaterThan x xs = filter (flip comp x) xs
    fun lessThanOrEqual x xs = filter (comp x) xs
    
  6. It seems that sort has two base cases where one is handled with a pattern and the other with an if-then-else. Why?

    fun sort [] = []
      | sort [x] = [x]
      | sort (x::xs) = sort (lessThanOrEqual x xs) @ x :: sort (greaterThan x xs)
    

    Or, if, for some reason, you wanted the base cases listed last:

    fun sort (x::(xs as (_::_))) = sort (lessThanOrEqual x xs) @
                                   x :: sort (greaterThan x xs)
      | sort xs = xs
    
  7. Even though this isn't Quicksort, and so isn't nowhere near as efficient, one thing you could do to improve it, is to only pass through xs once, rather than twice. Since we're only allowed to foldr, we have to produce a pair of lists:

    fun partition p xs = foldr (fn (x, (ys, zs)) =>
        if p x then (x::ys, zs) else (ys, x::zs)) ([], []) xs
    
    fun sort [] = []
      | sort [x] = [x]
      | sort (x::xs) =
        let val (lesser, greater) = partition (comp x) xs
        in sort lesser @ x :: sort greater end
    
Community
  • 1
  • 1
sshine
  • 15,635
  • 1
  • 41
  • 66
  • 1
    Good post, though I tend to agree with this answer (to the question you link to) which says that it is better to view this as an inefficient quick-sort rather than something which isn't a quicksort: http://stackoverflow.com/a/7718127/4996248 – John Coleman Feb 07 '17 at 21:57
  • @JohnColeman: I tend to agree with Jon Harrop's comment in the same thread: »A "valid" implementation of any algorithm should have the same asymptotic bounds, don't you think? The bastardised Haskell quicksort doesn't preserve any of the memory complexity of the original algorithm. Not even close. That's why it is over 1,000x slower than Sedgewick's genuine Quicksort in C.« – sshine Feb 07 '17 at 22:02
  • 1
    I look it like this: if a student came up to me and excitedly told me that they had just discovered their own sorting algorithm, and it was equivalent to this Haskell version, I would be remiss to not point out that they had essentially rediscovered a version of quicksort. I would also be remiss if I didn't point out that the implementation wasn't very good for the reasons that you outline. Ultimately it is a question of definitions as to whether this counts as a bad quicksort or not a quicksort at all. I don't have very strong opinions on the matter. – John Coleman Feb 07 '17 at 23:45
  • @JohnColeman: I only recently obtained a strong opinion of this from having read too much of [Jon Harrop's blog](http://flyingfrogblog.blogspot.dk/) and found his criticism of over-simplified, but inefficient FP examples compelling. Jon seems to be a [proponent of FP](http://flyingfrogblog.blogspot.dk/2010/05/why-is-haskell-used-so-little-in.html), but perhaps because of attitude tends to get deleted from StackOverflow. His point seems valid to me: Bad examples give FP a bad reputation. And you're right, that there is a partition-exchange sort, just not a very quick one. ;-) – sshine Feb 08 '17 at 00:29
  • I am familiar with Jon Harrop from the Usenet days of `comp.lang.functional` and definitely respect his views, although he does seem to be a tad dogmatic at times. – John Coleman Feb 08 '17 at 00:33
1

The only mistake I see is in your merge and tripleMerge functions. By using patterns like a::b you are disallowing empty lists. But -- when the pivot is either the smallest or the greatest element, than one of your partitioning functions will be returning empty lists. If you tweak the code of those two functions like this:

fun merge xs ys = folder (op::) ys xs;

fun tripleMerge xs y ys = merge xs (y::ys);

and leave the rest of the code alone, it will work as expected.

John Coleman
  • 51,337
  • 7
  • 54
  • 119
  • Thanks for the insight. Rewriting merge to be as you gave it, and replacing the call to tripleMerge with `merge (getLEQ ...) (pivot::(getGreater ...))` fixed my problem. – Gigaflop Feb 06 '17 at 22:25