Skip to main content

C'est la Z

From selection to sorting

When I first saw the quicksort it was in an algorithms class back in the day. We first learned the quicksort, then choosing a good pivot element and then as an afterthought we did quickselect.

Fast forward to teaching. I was never really happy teaching quicksort. Mergesort is easy to motivate and it's pretty easy to write. Quicksort always felt a little forced.

I thought I'd try switching things up this time and doing quickselect first.

The motivating problem: find the Kth smallest item in a list - in our case the list is an array of ints.

I want to start with the least efficient algorithm so I stack the deck. I remind them that we've been finding the smallest item in a list for two years now.

They don't disappoint and suggest something like this:

L = [10,3,28,82,14,42,66,74,81]

def findKth(L,k):
omits=[]
for i in range(k):
ans=max(L)
for item in L:
if item < ans and item not in omits:
ans=item
omits.append(ans)
return ans

print findKth(L,3)

Clearly an \(O(n^2)\) algorithm.

Can we do better?

Certainly.

The students then suggest sorting the data set first. If we use mergesort, we can sort in \(O(nLg (n))\) time. This lead to a great conversation about sorting being so fast it's practically free and that you don't have to hard code everything from scratch. Not only is sorting the data set then plucking the kth item out much faster, if you already have a sort written or if you use your language's library's sort, it's much easier as well:

def findKth(L,k):
tmp = sorted(L)
return tmp[k]

But we can do even better. So now we talk about quickselect

We pick a random pivot, partition the list a la quicksort (reorder the list such that all items less than the pivot are to its left, and all items greater than the pivot are to its right).

We now know that after partitioning. the pivot is in it's exact location. If its index is k then we're done. If not, we can recursively quickselect on either the left or right side.

Pretty cool, but is it faster?

It's easy to see that if we keep choosing a bad pivot (the smallest or largest in the list), each iteration takes \(n\) time to partition and each iteration takes one item out of contention. This takes us back to \(O(n^2)\).

However…

If we choose a good partition – at the middle of the list, each partition takes less and less time. We get a run time of:

\(n+\frac{n}{2} +\frac{n}{4}+\frac{n}{8}+\dots\) and since \(\frac{n}{2} +\frac{n}{4}+\frac{n}{8}\dots=n\) this becomes an \(O(2n)\), or \(O(n)\) algorithm.

That's really cool.

Homework was the actual implementation.

I think this might be a better way to approach quicksort. It seems less forced, plus the class gets to go through the exercise of taking an algorithm form \(O(n^2)\) to \(O(nlg(n))\) to \(O(n)\).

Next, moving to the quicksort and also showing that we can indeed avoid those really bad pivots.

Addendum

We moved to quicksort today and overall I'm happy with this approach. The only thing I think needs tweaking is going from the idea of partitioning to Java code. Java makes it somewhat of a bear.
comments powered by Disqus