| |
QuickSort in C# by Ben Watson
Introduction
This program stems from an assignment I did for a class. In every day use, there is little reason to write our own sorting routines because there are so many other, highly-optimized implementations at our disposal. However, understanding one of the fastest and most widely used sorting algorithms out there is sure to benefit any but the most casual programmer.
General Procedure
QuickSort is designed to work efficiently on arrays, and it is based on a divide & conquer approach. The general steps are as follows:
1. Choose a "pivot" location in the array. 2. Move everything less than the pivot to the left part of the array, and everything more (or equal) than it to the right part of the array. 3. Move the pivot item to where we stopped scanning for items less than it. 4. Call quicksort recursively on the parts of the array to either side of the pivot. 5. When the part of the array we're sorting is sufficiently small, call a simpler sorting algorithm on the few places left, and return.
Walkthrough example
This is easier to see with an example:
Let's sort the following array of ten integers:
[6 24 80 4 19 84 1 10 13 7]
Let's choose our pivot to arbitrarily be the first
element, a 6. In addition, let's create two
pointer iterators to walk through the array looking
for things to swap--one from the beginning and the
other from the end. We'll call these variable p, m,
and n.
Thus:
p
[6 24 80 4 19 84 1 10 13 7]
m n
If array[m] is greater than array[p] and array[n] is
less than array[p] we'll simply swap array[m] and
array[n]. If one of the conditions matches, and
the other doesn't we'll just keep moving the pointers
until we can do a swap:
p
[6 24 80 4 19 84 1 10 13 7]
m n
Swap:
p
[6 1 80 4 19 84 24 10 13 7]
m n
Advance pointers:
p
[6 1 80 4 19 84 24 10 13 7]
m n
Swap again:
p
[6 1 4 80 19 84 24 10 13 7]
m n
Advance pointers:
p
[6 1 4 80 19 84 24 10 13 7]
n m
Now we see that our pointers have crossed each other.
At this point we can safely move our pivot to where n is.
This will put the pivot item in its final position in
the sorted array:
p
[4 1 6 80 19 84 24 10 13 7]
At this point, everything before p is less than array[p]
and everything after p is greater that array[p]. Now
we just need to sort both halves. To sort the first half,
we don't need to make a call to quicksort--that would be
a waste of time since it's only a few elements in length.
Instead, we can use a more elementary sorting algorithm,
like selection sort, which will yield the following array:
p
[1 4 6 80 19 84 24 10 13 7]
we also need to sort the array after p. For this we
will call quicksort recursively on that part. I'll rewrite
the array to show only what we're interested in and we'll
again choose the first element as the pivot:
p
[80 19 84 24 10 13 7]
m n
I've already moved m to the first element greater
than array[p], and the last element less than array[p]
is the last element in the array. We can then swap those:
p
[80 19 7 24 10 13 84]
m n
You'll notice that there are no more swaps that will occur
in this step, and our pointers will cross like this:
p
[80 19 7 24 10 13 84]
n m
We can then swap p and n, giving us:
p
[13 19 7 24 10 80 84]
We obviously do not need to sort the right hand side,
but the left hand could use a little work:
p
[13 19 7 24 10]
m n
p
[13 10 7 24 19]
m n
p
[13 10 7 24 19]
n m
p
[7 10 13 24 19]
Each side of this can be easily sorted using selection
sort (or similar), giving:
[7 10 13 19 24]
If then look at the whole array we've sorted in pieces, we'll have:
[1 4]6[7 10 13 19 24]80[84] -- perfectly sorted!
|
Make sure you understand the walk-through above, and if you're having trouble understanding what's going--work out your own array on paper--just like I did when I learned this.
Code
To implement this in C#, let's create our own Array class:
public class Array
{
int[] array;
int numItems;
public Array(int size)
{
array=new int[size];
numItems=0;
}
public void Add(int i)
{
array[numItems++]=i;
}
}
|
This is just the basic plumbing to store our values.
We need to add a function to do our pivoting, or swapping:
private int Pivot(int beg, int end)
{
int p=array[beg];
int m=beg;
int n=end+1;
do
{
m=m+1;
} while (array[m]<=p && m<end);
do
{
n=n-1;
} while (array[n] >p);
while (m<n)
{
int temp=array[m];
array[m]=array[n];
array[n]=temp;
do
{
m=m+1;
} while (array[m]<=p);
do
{
n=n-1;
} while (array[n] >p);
}
int temp2=array[n];
array[n]=array[beg];
array[beg]=temp2;
return n;
}
|
Pivot() does exactly what I described in the walkthrough. The comments should help you through the code.
We also need a function that does our "simple" sorting. I've decided to use selection sort:
private void SelectionSort(int beg, int end)
{
for (int i=beg;i<end;i++)
{
int minj=i;
int minx=array[i];
for (int j=i+1;j<=end;j++)
{
if (array[j]<minx)
{
minj=j;
minx=array[j];
}
}
array[minj]=array[i];
array[i]=minx;
}
}
|
Now that we have our basic pieces, we can create some controlling functions:
public void Sort()
{
QuickSort(0,array.numItems-1);
}
private void QuickSort(int beg, int end)
{
if (end==beg) return;
if (end-beg<9)
SelectionSort(beg,end);
else
{
int l=Pivot(beg,end);
QuickSort(beg,l-1);
QuickSort(l+1,end);
}
}
|
We make Sort() our publically accessible method, which calls QuickSort() on the entire array. QuickSort makes sure there is enough array to run on. If there are few elements, it calls SelectionSort(), otherwise, it pivots the array, and calls itself on each half.
I also created a public method to dump the entire array to the screen so that you can see the results.
public void Dump()
{
foreach(Object o in array)
System.Console.Write(o.ToString()+ " ");
System.Console.WriteLine("");
}
|
That's it! It's pretty easy once you understand it. There are likely a number of things you can do to optimize the algorithm even further, but this is the basic format.
Just to test Use the following code to demonstrate the Array class and QuickSort:
class MainClass
{
[STAThread]
static int Main(string[] args)
{
if (args.Length==0)
{
Console.WriteLine("You must enter an integer to specify array size.");
return 1;
}
int size=Convert.ToInt32(args[0]);
Array array=new Array(size);
Random r=new Random();
for (int i=0;i<size;i++)
array.Add(r.Next(size));
array.Dump();
array.Sort();
array.Dump();
return 0;
}
}
|
Considerations
QuickSort runs in O(n log n) time, which is plenty quick, however there are two cases which will absolutely kill its performance. If the array is sorted in either ascending or descending order, then pivot will need to be called on every single element (try running through an example if you can't see why that is). Therefore, QuickSort is really only optimal for arrays in mostly random order.
If it were possible to select the median as the pivot every time, we could always get optimal running time, however this turns out to be a problem that is not worth solving in most instances. Picking a random element, or always the first, etc. will usually be sufficiently fast.
|
|