| ||
| 2003-02-09 | © 2001-2003 Harry M. Hardjono ramstrong@earthlink.net | |
|
Home Page |
Ah, quicksort. I remember the first time the quicksort algorithm was explained to me. It was in high school. It was a explained as a trade-off question. Do you do use a more complicated algorithm to sort such as quicksort or do you use a simple algorithm such as bubble sort? The former is hard, but fast and the latter is slow, but easy. Or is it?
I think that all these years, people has been repeating the mantra that quicksort is hard, that they start to believe in it, and never realize how easy it is to do quick sort! Yes, that's right. Quicksort is easy. Of course, it's hard the first time you see it, but I don't think that it should still be hard after a few times you try it. In fact, I find it too easy! You don't believe me? Try it yourself! The whole function takes only four lines. Five if you include variable declarations. As far as I know, I'm the only who posted quicksort in 4 lines solution. Just type in "quicksort in four lines" and you'll get this page. I decided to put this simple concept to a little test, to see just how many people can actually do it. It's rather disheartening to realize that the odds of success is 1 in 1000. That's one in one thousand! Can you believe it? Anyway, here's the challenge. Just make the function work by filling in the missing piece and you're there. Here's a couple of hints: The array a[] is global and var p is the pivot point. Another hint: Just use var i as the loop variable.
/* QuickSort demonstration */
/* Written by Harry M. Hardjono */
/* Jan 1 2002 */
/* Assume array of integers a[] */
void quicksort(int lo, int hi) {
int i,p;
if (lo>=hi) return;
/* Fill in the missing line here... */
quicksort(lo, p-1);
quicksort(p+1, hi);
}
That's it! All you have to do is fill in that one line. Let's see what happens, shall we?
Henry Wong
for ((a[i=lo+1]<a[p=lo])?(a[i]^=a[p]^=a[i]^=a[p]):0 ;
i<hi;
(a[++i]<a[p])? (a[p++]^=a[p+1]^=a[p]^= a[p+1]^=a[i]^=a[p+1]^=a[i]):0
);
This is an interesting algorithm. Since you need to keep track the pivot point, the algorithm goes loop-conditional-swap-swap. But you have to make sure that the first item got swapped, so there's the first conditional and swap. Interesting how Henry got everything into ternary operators. It's not exactly simple, but it works.
Michael Kaltner
for(i=hi,p=lo;p<i;)
if(a[p]>a[p+1]) a[p++]=(a[p]+a[p+1])-(a[p+1]=a[p]);
else a[i--]=(a[i]+a[p+1])-(a[p+1]=a[i]);
Here is a very nice algorithm by Michael Kaltner. If you notice, he has a different swap technique. The swap works by storing the value (a+b) and substracting b (which is a) from it and assign it to a. Confusing? Shouldn't be. The partition technique works by comparing every values after the pivot, and swapping it to before the pivot point, all while maintaining the pivot value. I have to say that this one has originality written all over it. I don't like the comma there, but the algorithm works very well with it. I suppose you can work the loop by incrementing var i, but this way looks better. I'm happy to see it here.
Paul Bartrum
for (i=p=lo; i<=hi; i++) if(a[i]<a[p]) swap(p, i), swap(i, ++p);
and
#define swap(x, y) (a[x]^=a[y]^=a[x]^=a[y])
for (i=p=lo; i<=hi; i++)
if(a[i]<a[p] && swap(p, i)*0==0 && ++p!=i)
swap(p, i);
Paul is somewhat inexperienced. However, as I drop little hints here and there, he blossomed tremendously. This is a very simple implementation of the quicksort. It bears the structure loop-conditional-swap-swap. His inexperience shows in that in his final solution, he still hasn't yet put the swap checking in the proper place, thereby exhibiting some unnecessary inefficiency and complexity. However, his earlier solution is clear enough that I think it is worthwhile to showcase it here, even though you'll need a different swap than what is defined there.
I am sad and disappointed that no one actually got the right answer. The problem here is that choosing the first element as the pivot value means that you lose track of it as soon as you swap it. There are two solutions. The first solution is that you skip one step ahead, keeping the pivot element as the first element of the array. This works, except that due to the way the recursive function is called, it's not really one line, but two. The closest one to my algorithm is Da Kong. His algorithm is as follows:
Da Kong
for(i=p=lo; i<hi;)
if(a[++i]<a[lo] && i>++p)
a[p]^=a[i]^=a[p]^=a[i];
if(p>lo) a[p]^=a[lo]^=a[p]^=a[lo];
In order to compress this into one line, everyone chooses the technique of bubbling up the pivot point. This works, but as you can see from different solutions above, it takes two swaps. You can put the above algorithm into one line, but it's somewhat tricky and hackish.
The question is, is there a better way to do this? It's not the question of what is the best way to keep track the pivot point, but rather, is there a way to do this WITHOUT keeping track of the pivot point? It turns out there is. Instead of choosing the first element as the pivot point, you choose the last! This way, when the element gets swapped, it gets swapped into place and the work is done. I think it solves neatly two problems: Keeping track of pivot point, and putting it into place at the end of the loop. This is the solution I have. It goes by Loop-Conditional-Swap structure:
Harry Hardjono
for (p=(i=lo)-1; i<=hi; i++)
if (a[i]<=a[hi] && i!=++p)
a[i]^=a[p]^=a[i]^=a[p];
or this...
for (p=(i=lo)-1; i<=hi; i++) if (a[i]<=a[hi]) swap_int(i,++p);
That is all you have to do! Choose the last element as your pivot! Very simple, right? How come nobody got this right? It's not like choosing the last element is a novel idea. Instead, I get deluged by people saying "I know this isn't one line, but it's the best I can do." Sorry, you lose. A few even argued hard and strong that they deserve special treatment because they think they are right. Sorry, can't do. It wouldn't be fair to existing winners. Too many gave up too quickly, when they keep getting closer to the solution but never did finish and gave up before the contest deadline.
I'm not even that picky about that one line requirements. As long as I think you're capable of it, I'll accept your answer. The first requirement is that you know how quicksort works, instead of blindly copying algorithm from somewhere else (some entrants did). Also, make sure that it really is quicksort, not sequential sort in disguise! Once you know and understand how quicksort works, you need to come up with the idea of using the last element. It appears that the only people who is smart enough to understand quicksort, is smart enough to do it while bubbling up, and never dreamed that there is an easier way to do it although I must admit Michael's solution is close enough to mine. Of course, there is the question about the swap, but the swap isn't as thought provoking as pivot selection. You can always research the swap, or just figure it out. It's not that hard. The process goes like this: Choose the simplest method, the first element as the pivot. Hmmm... I lost track of it by the first swap. I can either bubble it up, or I can choose another element. Let's choose the last, and make sure to swap it at the end of the loop. Since p is the pivot point, and it needs to be incremented, this means that p needs to be incremented before swapping, so if i=lo, then p=lo-1. To make sure the last swap is done, then i<=hi.
for (p=(i=lo)-1; i<=hi; i++)Next comes the conditional. We'll swap the values only if a[i]<pivot. No. Make that a[i]<=pivot to let the pivot get swapped at the end. Now we need to increment p before the swap, and since the swap can't swap the same variable, we'll have to check that. Aha! Just increment p and check that i is not equal to p! It works by short-circuit logic. If your particular C compiler doesn't support it, you can use if(a) if(b) construct.
if (a[i]<=a[hi] && i!=++p)
Last, comes the swap. The easy way to swap is to use a function to do it. This is just an example. You should write a generalized swap routine and pass pointers to it, not specific to the array.
void swap_int(int x, int y) {
int t;
t = a[y];
a[y] = a[x];
a[x] = t;
}
However, that's not one line. Now we know XOR operation. x^0=x. x^x=0. Therefore, x^y^x=y^0=y. But that means y=x^y^x. Substitute that to the second line, and we have y. How about x? x=y^x^y. Put that on the third line and we have x. But we need x^y, so lets put that to the first line. Thus we have the famous XOR swap:
x=x^y; y=x^y; x=x^y;Nothing a 1 year CS student can't handle. Putting this into one line yields:
x=(y=(x=y^x)^y)^x;Simplify that and it becomes:
x=(y=(x^=y)^y)^x; x=(y^=(x^=y))^x; x^=(y^=(x^=y)); x^=y^=x^=y;And thus my code. With limited variables, I can take advantage of register variables option better. I did this in about 10 minutes. It shouldn't take you more than 3 hours to build and check all the supporting functions. All you have to do is find another solution besides bubbling up the pivot point and choose the last element as your pivot. What is so complicated about that? Can finding another solution be really that hard? The quicksort that I have is the simplest quicksort possible, I think. However, it's not the best. There are two things wrong with it. The first fault is that it is slower than bubblesort, at least on sorted or nearly sorted data. What kind of data is nearly sorted data? Plenty. For example, keeping track of item ranking sorted according to volume sold, or GPS satellites, or even nearly finished quicksort, as we shall see later. There are several different ways to fix this fault. The generally recognized best method is the median-of-three method. There was some discussion about whether or not you should swap the elements as you compare the values. I think you should. Therefore you put this routine before partitioning:
if (a[lo] > a[hi]) swap_int(lo,hi); if (a[lo] > a[(lo+hi)/2]) swap_int(lo,(lo+hi)/2); if (a[hi] > a[(lo+hi)/2]) swap_int(hi,(lo+hi)/2);Or you can do what I do and simply randomize the array before calling quicksort. :) Simple, right? The second fault is less obvious, but much more dangerous since you can't circumvent it by prepping the array. When the list is composed of many identical elements, then it becomes slower than sequential/selection sort! What kind of data has identical elements? Sorting male/female, Rep/Dem/Ind, or sparse array. This fault is usually overlooked by people, but it is much more devastating since you can't easily circumvent the fault. Robert Sedgewick mentioned this in his book, but only briefly, so many people missed it. You need to use 2 partition points in sorting, to separate elements that are less than pivot, pivots (may be more than one), and elements greater than pivot. Using my existing code, it is trivial to modify the code to fix this.
void quicksort(int lo, int hi) {
int i,p; if (lo >= hi) return;
for (p=(i=lo)-1; i<hi; i++) if (a[i]<a[hi]) swap_int(i,++p);
quicksort(lo, p);
for (i=p+1; i<=hi; i++) if (a[i]<=a[hi]) swap_int(i,++p);
quicksort(p+1, hi);
}
That takes care of our problem nicely. Now that we have it working right, it is now time to optimize the routines. All the existing routines you see everywhere else has a very optimized inner loop. I think you should fix the bugs before optimizing anything! Premature optimization is tempting, but easily a potential time waster.There are several possible improvements. One would be to optimize the inner loop. Easily done by copying from somewhere else.
The simplistic method above uses too many swaps. In order to eliminate these unnecessary swaps, you'd want to partition it low-hi. However, in order to make sure that there's no overrun, you need to do an extra check. When you finally eliminate that extra check, you'll probably end up with something similar to what you see everywhere else. It would be interesting to see if you can really optimize that routine and come up with optimized version. Who knows? Maybe you can come up with an efficient way to set up a three way partition! Another way to optimize would be to not use quicksort all the way through. The actual limit where we want to stop quicksorting and use another sort must be discovered through trial and error, as it heavily depends upon hardware and sorting algorithm used. In this example, we'll use bubblesort to sort nearly sorted array, so we use this code:
if (hi-lo < 5) return;However, it is inconvenient if we ask the user to first randomize the array before sorting, then sort it again with bubblesort after quicksorting it. So, we'll use another function to wrap all the elements together:
void qsort(int lo, int hi) {
RandomizeArray(); // for the whole a[]
quicksort(lo,hi);
bubblesort(lo,hi); // or use insertion sort
}
Some would argue that removing recursion would be another viable optimization. I personally stop when the compiler can handle it, and a good compiler will automatically remove tail recursion. Let the hardware/assembler guy handle it. Optimizing down to that level can be very time consuming. I'd rather reorganize the program by calling the sort routine less often. This is the difference between a hard-core computer programmer and a program designer such as I. I stop when the solution is platform dependent. :)
Of course, if you're in a stack tight environment, then it is prudent to remove recursion. However, I'd rather keep track of recursion depth, and call on Shellsort upon reaching a certain depth. It is almost guaranteed that the number of elements by that depth can be easily handled by shellsort. This you can do easily by using static variable or passing in an extra variable. Speaking of which, a couple entries submitted sequential sort, otherwise known as selection sort. The algorithm works like this: Find the smallest element in the list, and put it left most. Repeat for the next element, each time shifting one. That is my very first sort routine and I must admit that it is slow. However, a simple enhancement makes it much better. Looking at the partitioned routine, it is trivial to create a sequential sort that put the smallest elements (plural) and put it at the end of the list. Increase pointer, and repeat. That means sorting Male/Female will only take two passes, regardless of how many there are. I believe that this similarity with partitioning process is what confuses people to submit a different sort algorithm for this contest. So close to the solution, yet unattainable for some. Phew! That's a lot of lines to what amounts to quicksort in four lines.
Grand Prize Winner:
Honorary Winner List:
|