The TAO of Java: Download Zip With Sources and Everything
The TAO of Java: Download Zip With Sources and Everything
Introduction...
Welcome to Java Data Structures (2nd edition). This document was created with an
intent to show people how easy Java really is, and to clear up a few things I've missed in
the previous release of the document.
This is a growing document; as new features are added to the language, new techniques
are discovered or realized, this document shall be updated to try to accommodate them all.
If you have suggestions, or requests, (or spelling/grammar errors) just e-mail them, and I'll
try to add the suggested topics into the subsequent release. Because this document is
changing so much, I've decided to implement a version number. This release is: v2.2.11,
updated: May 7th, 2002.
Current release of the document, including all the sources, can be downloaded here:
Of course, this document is free, and I intend to keep it that way. Selling of this
document is NOT permitted. You WILL go to hell if you do (trust me). (not that anybody
would want to buy it...) You may distribute this document (in ANY form), provided you don't
change it. (yes, you CAN include it in a book provided you notify me and give me credit
<and give me one free copy of the book>) To my knowledge, this document has already
been reproduced and distributed within some corporations, schools and colleges, but has
yet to be formally published in a book.
I take no responsibility for ANYTHING. I am only responsible for all the good things you
like about the article. So, remember, if it's bad, don't blame me, if it's good, thank me (give
me credit).
All the source has been compiled and tested using JDK v1.2. Although most things should
work flawlessly with previous versions, there are things where JDK 1.2 is more appropriate.
If you find problems and/or errors, please let me know.
Although this document should be read in sequence, it is divided into several major
sections, here they are:
Variables
Arrays
Array Stack
Array Queue
Array List
The Vector
Nodes
Linked Lists
Reusing Tricks
Trees
Generic Tree
Comparing Objects
Binary Search Trees
Tree Traversals
Node Pools
Node Pool Nodes
Node Pool Generic Trees
Node Pool Sort Trees
Priority Vectors
Sorting
Sorting JDK 1.2 Style
Sorting using Quicksort
Optimizing Quicksort
Radix Sort
Improving Radix Sort
Bibliography
Special Thanks
Contact Info
In contrast to what most people think about Java, it being a language with no pointers,
data structures are quite easy to implement. In this section, I'll demonstrate few basic data
structures. By learning how easy they are to implement in Java, you'll be able to write any
implementation yourself.
I also think that this document is a pretty good introduction to Data Structures in
general. All these concepts can be applied in any programming language. Incidentally, most
of these programs are ported from their C++ counterparts. So, if you want to learn Data
Structures in C/C++, you'll still find this document useful! Java is an Object Oriented
language, but more so than C++, so, most data structure concepts are expressed and
illustrated "more naturally" in Java! (try not to raise your blood pressure from all the
caffeine)
I suggest that you be familiar with Java format, and know some other programming
language in advance. Coincidentally, I and a couple of my friends are in the process of
writing a C language book, which deals with all that "start up" stuff.
The way most examples are executed is through the JDK's command line Java
interpreter. (at the prompt, you just type "java" and the name of the class to run.)
Linear Structures
Summary: We consider the essential features of a class of abstract data types known as linear
structures. Linear structures permit you to collect information and retrieve one piece of
information at a time.
Contents:
Introduction: Collections
Linear Structures
Additional Operations
A Linear Structure Interface
Introduction: Collections
As you may have learned earlier, computer scientists regularly design, analyze, and implement
mechanisms for collecting information. Why do we have more than one general Collection data
type? Because it turns out that we can often make the implementation more efficient if we focus
on certain operations. (More precisely, we can make the implementation of those operations
more efficient if we focus on those operations.)
You have probably already encountered a variety of collections in your computer science,
mathematics, or even real world education. For example, a set collects values and provides one
central operation of membership. In contrast, a list, such as a list of students in a class, collects
values and provides a central operation of visiting each value in the list in turn. Over the next
few readings and labs, we will consider a variety of collections and their implementations.
Linear Structures
Among the simplest collections we encounter are the so-called linear structures. Linear
structures are collections that let you add elements and will return elements to you one-by-one
(removing the elements as they are returned). You can think of a to-do list or a job jar as kinds of
linear structures: As you come up with new tasks, you add them to the list or jar. When you have
time to undertake a new task, you grab one from the list or jar.
You may be asking yourself (or the page, or the computer screen, or your professor) what
determines which object get() returns. In the most generic form of linear structure, the
particular object returned by get is not specified. However, we do build a variety of kinds of
linear structures, each with its own policy for specifying which object get returns.
Stacks return objects using a last-in, first-out policy. That is, get
removes and returns the object that has been most recently added to the
stack.
Queues return objects using a first-in, first-out policy. That is, get
removes and returns the object that has been least recently added to the
stack.
Priority Queues return objects based on some ordering. In this case, the
get operation removes and returns the object of highest priority. In
Java, we typically specify the priority using the compareTo operation or
a separate object that implements Comparator.
Randomized Linear Structures return objects randomly. In this case, it
should be difficult to predict which value the structure will return.
We will visit each of these structures over the next few readings and labs.
Additional Operations
So far, we've only specified two operations for linear structures. Are there others we might want
or expect them to provide? Certainly.
One principle many designers employ is that any precondition that a client must meet must also
have a mechanism for checking that precondition. Since it is a bad idea to try to get a value from
an empty structure, clients probably need access to a isEmpty predicate.
Experience also suggests that there are times in which it is useful to check what value get will
return, but not to remove that value. (Note that while we can remove and re-add the value in a
stack or priority queue, if we remove an element and re-add it in a queue or a randomized linear
structure, it moves to the end.) For such situations, many designers of linear structures include a
peek operation.
Some designers prefer as much symmatry in their structures as they can provide. Others worry
about implementation and note that we will eventually run out of room as we add values to a
collection. Both classes of designers provide an isFull method. We will take the perspective
that when we run out memory, something is seriously wrong with our program, and will not
provide an explicit isFull.
Some designers like to add functionality by permitting clients to determine how many values are
in the structure. Others note that determining the size of a linear structure is not central to the
mission of linear stuctures and do without it. We follow the latter strategy.
You may also want to consider which of the standard object methods a linear structure should
implement. Should we be able to convert the structure to a string and see all of the values? (If so,
in what order should they appear?) What does it mean for two linear structures to be equals? Can
we consider one linear structure naturally smaller than another?
/**
* A simple collection of values that permits clients to add elements
* and to extract them one-by-one.
*
* @author Samuel A. Rebelsky
* @version 1.1 of March 2006
*/
public interface LinearStructure<T>
{
/**
* Add an element to the structure. We assume that linear structures
* never fill, and issue a significant error if the structure fills.
* (The error is significant and rare enough that we do not list it
* as an exception, which means that programmers are not forced to
* catch it.)
*
* @param val
* The object that is to be added.
* @post
* The collection now contains an additional copy of val.
*/
public void put(T val);
/**
* Remove an element from the structure. The particular policy
* for removal is determined by the implementation or variant.
*
* @return val
* An object in the structure.
* @pre
* The structure is not empty.
* @post
* The structure contains one fewer copy of val.
*/
public T get();
/**
* Determine which object will next be returned by get.
*
* @return val
* An object in the structure.
* @pre
* The structure is not empty.
* @post
* val is the same value that get() would return.
* The structure is unchanged.
*/
public T peek();
/**
* Determine if the structure is empty.
*/
public boolean isEmpty();
} // interface LinearStructure
Trees...
The next major set of data structures belongs to what's called Trees. They are called
that, because if you try to visualize the structure, it kind of looks like a tree (root, branches,
and leafs). Trees are node based data structures, meaning that they're made out of small
parts called nodes. You already know what a node is, and used one to build a linked list.
Tree Nodes have two or more child nodes; unlike our list node, which only had one child.
Trees are named by the number of children their nodes have. For example, if a tree node
has two children, it is called a binary tree. If it has three children, it is called tertiary tree. If
it has four children, it is called a quad tree, and so on. Fortunately, to simplify things, we
only need binary trees. With binary trees, we can simulate any tree; so the need for other
types of trees only becomes a matter of simplicity for visualization.
Since we'll be working with binary trees, lets write a binary tree node. It's not going to be
much different from our pOneChildNode class; actually, it's going to be quite the same, only
added support for one more pointer. The source for the follows:
public pTwoChildNode(){
data = null;
left = right = null;
}
public pTwoChildNode(Object d){
data = d;
left = right = null;
}
public void setLeft(pTwoChildNode l){
left = l;
}
public void setRight(pTwoChildNode r){
right = r;
}
public void setData(Object d){
data = d;
}
public pTwoChildNode getLeft(){
return left;
}
public pTwoChildNode getRight(){
return right;
}
public Object getData(){
return data;
}
public String toString(){
return ""+data;
}
}
This node is so much similar to the previous node we did, that I'm not even going to
cover this one. (I assume you'll figure it out by simply looking at the source) The children of
the node are named left and right; these will be the left branch of the node and a right
branch. If a node has no children, it is called a leaf node. If a node has no parent (it's the
father of every node), it is called the root of the tree. This weird terminology comes from
the tree analogy, and from the family tree analogy.
Some implementations of the tree node, might also have a back pointer to the parent
node, but for what we'll be doing with the nodes, it's not necessary. The next section will
talk about a generic binary tree which will be later used to create something cool.
Generic Tree...
Binary Trees are quite complex, and most of the time, we'd be writing a unique
implementation for every specific program. One thing that almost never changes though is
the general layout of a binary tree. We will first implement that general layout as an
abstract class (a class that can't be used directly), and later write another class which
extends our layout class.
Trees have many different algorithms associated with them. The most basic ones are the
traversal algorithms. Traversals algorithms are different ways of going through the tree (the
order in which you look at it's values). For example, an in-order traversal first looks at the
left child, then the data, and then the right child. A pre-order traversal, first looks at the
data, then the left child, and then the right; and lastly, the post-order traversal looks at the
left child, then right child, and only then data. Different traversal types mean different
things for different algorithms and different trees. For example, if it's binary search tree (I'll
show how to do one later), then the in-order traversal will print elements in a sorted order.
Well, lets not just talk about the beauties of trees, and start writing one! The code that
follows creates an abstract Generic Binary Tree.
import pTwoChildNode;
Now, lets go over it. The pGenericBinaryTree is a fairly large class, with a fair amount
of methods. Lets start with the one and only data member, the root! In this abstract
class, root is a private head of the entire tree. Actually, all we need is root to access
anything (and that's how you'd implement it in other languages). Since we'd like to have
access to root from other places though (from derived classes, but not from the "outside,"
we've also added two methods, named getRoot(), and setRoot() which get and set the
value of root respectively.
We have two constructors, one with no arguments (which only sets root to null), and
another with one argument (the first element to be inserted on to the tree). Then we have a
standard isEmpty() method to find out if the tree is empty. You'll also notice that
implementing a counter for the number of elements inside the tree is not a hard thing to do
(very similar to the way we did it in a linked list).
The getData() method returns the data of the root node. This may not be particularly
useful to us right now, but may be needed in some derived class (so, we stick it in there
just for convenience). Throughout data structures, and mostly entire programming world,
you'll find that certain things are done solely for convenience. Other "convenient" methods
are getLeft(), getRight() and setData().
The two methods we'll be using later (for something useful), are:
insertLeft(pTwoChildNode,Object), and insertRight(pTwoChildNode,Object). These
provide a nice way to quickly insert something into the left child (sub-tree) of the given
node.
The rest are just print methods. The trick about trees are that they can be traversed in
many different ways, and these print methods print out the whole tree, in different
traversals. All of these are useful, depending on what you're doing, and what type of tree
you have. Sometimes, some of them make absolutely no sense, depending on what you're
doing.
Printing methods are recursive; a lot of the tree manipulation functions are recursive,
since they're described so naturally in recursive structures. A recursive function is a function
that calls itself (kind of like pretrav(), intrav(), and postrav() does).
Go over the source, make sure you understand what each function is doing (not a hard
task). It's not important for you to understand why we need all these functions at this point
(for now, we "just" need them); you'll understand why we need some of them in the next
few sections, when we extend this class to create a really cool sorting engine.
Comparing Objects...
Comparing Objects in Java can be a daunting task, especially if you have no idea how it's
done. In Java, we can only compare variables of native type. These include all but the
objects (ex: int, float, double, etc.). To compare Objects, we have to make objects with
certain properties; properties that will allow us to compare.
We usually create an interface, and implement it inside the objects we'd like to
compare. In our case, we'll call the interface pComparable. Interfaces are easy to write,
since they're kind of like abstract classes.
As you can see, there is nothing special to simple interfaces. Now, the trick is to
implement it. You might be saying, why am I covering comparing of objects right in the
middle of a binary tree discussion... well, we can't have a binary search tree without being
able to compare objects. For example, if we'd like to use integers in our binary search tree,
we'll have to design our own integer, and let it have a pComparable interface.
Next follows our implementation of pInteger, a number with a pComparable interface.
I couldn't just extend the java.lang.Integer, since it's final (cannot be extended) (those
geniuses!).
I believe most of the interface is self explanatory, except maybe for the
compareTo(Object) method. In the method, we first make sure that the parameter is of
type pInteger, and later using casting, and calling methods, we compare the underlying
native members of pInteger and return an appropriate result.
A note on JDK 1.2: In the new versions of the JDK, you won't need to implement your
own pComparable, or your own pInteger; since it's built in! There is a Comparable
interface, and it's already implemented by the built in java.lang.Integer,
java.lang.String, and other classes where you might need comparisons. I'm doing it this
way only for compatibility with the older versions of JDK. I'll talk about JDK 1.2 features
later in this document (hopefully).
A binary search tree will extend our pGenericBinaryTree, and will add on a few
methods. One that we definitely need is the insert() method; to insert objects into a tree
with binary search in mind. Well, instead of just talking about it, lets write the source!
import pComparable;
public pBinarySearchTree(){
super();
}
As you can obviously see, the insert(pComparable) method is definitely the key to the
whole thing. The method starts out by declaring two variables, 't', and 'q'. It then falls
into a for loop. The condition inside the for loop is that 't' does not equal to null (since it
was initially set to getRoot(), which effectively returns the value of root), and while the
object we're trying to insert does not equal to the object already inside the tree.
Usually, a binary search tree does not allow duplicate insertions, since they're kind of
useless; that's why we're attempting to catch the case where we're trying to insert a
duplicate. Inside the for loop, we set 'q' to the value of the next node to be examined. We
do this by first comparing the data we're inserting with the data in the current node, if it's
greater, we set 't' to the right node, if less, we set it to the left node (all this is cleverly
disguised inside that for statement).
We later check the value of 't' to make sure we've gotten to the end (or leaf) of the
tree. If 't' is not null, that means we've encountered a duplicate, and we simply return.
We then check to see if the tree is empty (didn't have a root), if it didn't, we create a new
root by calling setRoot() with a newly created node holding the inserted data.
If all else fails, simply insert the object into the left or the right child of the leaf node
depending on the value of the data. And that's that!
Understanding binary search trees is not easy, but it is the key to some very interesting
algorithms. So, if you miss out on the main point here, I suggest you read it again, or get a
more formal reference (where I doubt you'll learn more).
Anyway, as it was with our stacks and queues, we always had to test everything, so, lets
test it! Below, I give you the test module for the tree.
import java.io.*;
import pInteger;
import pBinarySearchTree;
class pBinarySearchTreeTest{
public static void main(String[] args){
pBinarySearchTree tree = new pBinarySearchTree();
pInteger n;
int i;
System.out.println("Numbers inserted:");
for(i=0;i<10;i++){
tree.insert(n=new pInteger((int)(Math.random()*1000)));
System.out.print(n+" ");
}
System.out.println("\nPre-order:");
tree.print(1);
System.out.println("\nIn-order:");
tree.print();
System.out.println("\nPost-order:");
tree.print(3);
}
}
As you can see, it's pretty simple (and similar to our previous tests). It first inserts ten
pInteger (pComparable) objects in to the tree, and then traverses the tree in different
orders. These different orders print out the whole tree. Since we know it's a binary search
tree, the in-order traversal should produce an ordered output. So, lets take a look at the
output!
Numbers inserted:
500 315 219 359 259 816 304 902 681 334
Pre-order:
500 315 219 259 304 359 334 816 681 902
In-order:
219 259 304 315 334 359 500 681 816 902
Post-order:
304 259 219 334 359 315 681 902 816 500
Well, our prediction is confirmed! The in-order traversal did produce sorted results. There
is really nothing more I can say about this particular binary search tree, except that it's
worth knowing. This is definitely not the fastest (nor was speed an issue), and not
necessarily the most useful class, but it sure may proof useful in teaching you how to use
trees.
And now, onto something completely different! NOT! We're going to be doing trees for a
while... I want to make sure you really understand what they are, and how to use them.
(and to show you several tricks other books try to avoid <especially Java books>)
Tree Traversals...
I've talked about tree traversals before in this document, but lets review what I've said.
Tree's are created for the sole purpose of traversing them. There are two major traversal
algorithms, the depth-first, and breadth-first.
So far, we've only looked at depth-first. Pre-traversal, in-traversal, and post-traversal are
subsets of depth-first traversals. The reason it's named depth-first, is because we
eventually end up going to the deepest node inside the tree, while still having unseen nodes
closer to the root (it's hard to explain, and even harder to understand). Tracing a traversal
surely helps; and you can trace that traversal from the previous section (it's only ten
numbers!).
The other type of traversal is more intuitive; more "human like." Breadth-first traversal
goes through the tree top to bottom, left to right. Lets say you were given a tree to read
(sorry, don't have a non-copyrighted picture I can include), you'd surely read it top to
bottom, left to right (just like a page of text, or something).
Think of a way you visualize a tree... With the root node on top, and all the rest
extending downward. What Breadth-First allows us to do is to trace the tree from top to
bottom as you see it. It will visit each node at a given tree depth, before moving onto the
the next depth.
A lot of the algorithms are centered around Breadth-First method. Like the search tree
for a Chess game. In chess, the tree can be very deep, so, doing a Depth-First traversal
(search) would be costly, if not impossible. With Breadth-First as applied in Chess, the
program only looks at several moves ahead, without looking too many moves ahead.
The Breadth-First traversal is usually from left to right, but that's usually personal
preference. Because the standard consul does not allow graphics, the output may be hard to
correlate to the actual tree, but I will show how it's done.
As with previous examples, I will provide some modified source that will show you how
it's done. An extended pBinarySearchTree is shown below:
import pTwoChildNode;
import pBinarySearchTree;
import pEasyQueue;
As you can see, the class is pretty simple (only one function). In this demo, we're also
using pEasyQueue, developed earlier in this document. Since breadth first traversal is not
like depth first, we can't use recursion, or stack based methods, we need a queue. Any
recursive method can be easily simulated using a stack, not so with breadth first, here, we
definitely need a queue.
As you can see, we start by first inserting the root node on to the queue, and loop while
the queue is not isEmpty(). If we have a left node in the node being examined, we insert it
in to the queue, etc. (same goes for the right node). Eventually, the nodes inserted in to the
queue, get removed, and subsequently, have their left children examined. The process
continues until we've traversed the entire tree, from top to bottom, left to right order.
Now, lets test it. The code below is pretty much the same code used to test the tree, with
one minor addition; the one to test the breadth-first traversal!
import java.io.*;
import pInteger;
import pBinarySearchTree;
class pBreadthFirstTraversalTest{
public static void main(String[] args){
pBreadthFirstTraversal tree = new pBreadthFirstTraversal();
pInteger n;
int i;
System.out.println("Numbers inserted:");
for(i=0;i<10;i++){
tree.insert(n=new pInteger((int)(Math.random()*1000)));
System.out.print(n+" ");
}
System.out.println("\nPre-order:");
tree.print(1);
System.out.println("\nIn-order:");
tree.print();
System.out.println("\nPost-order:");
tree.print(3);
System.out.println("\nBreadth-First:");
tree.breadth_first();
}
}
As you can see, nothing too hard. Next, goes the output of the above program, and you
and I will have to spend some time on the output.
Numbers inserted:
890 474 296 425 433 555 42 286 724 88
Pre-order:
890 474 296 42 286 88 425 433 555 724
In-order:
42 88 286 296 425 433 474 555 724 890
Post-order:
88 286 42 433 425 296 724 555 474 890
Breadth-First:
890 474 296 555 42 425 724 286 433 88
Looking at the output in this format is very abstract and is not very intuitive. Lets just
say we have some sort of a tree, containing these numbers above. We were looking at the
root node. Now, looking at the output of this program, can you guess what the root node is?
Well, it's the first number in breadth-first: 890. The left child of the root is: 474. Basically,
the tree looks like:
|--[890]
|
|--[474]--|
| |
|--[296]--| [555]--|
| | |
[ 42]--| [425]--| [724]
| |
|--[286] [433]
|
[ 88]
As is evident from this picture, I'm a terrible ASCII artist! (If it's screwed up, sorry).
What you can also see is that if you read the tree form left to right, top to bottom, you
end up with the breadth first traversal. Actually, this is a good opportunity for you to go
over all traversals, and see how they do it, and if they make sense.
There are many variations to these traversals (and I'll show a few in subsequent
sections), for now, however, try to understand these four basic ones.
Well, see you again in the next section, where we examine some ways you can speed up
your code and take a look at JDK 1.2 features, which will simplify our life a LOT!
Sorting...
Sorting, in general, means arranging something in some meaningful manner. There are
TONS of ways of sorting things, and we will only look at the most popular ones. The one we
won't cover here (but one you should know) is Quick Sort. You can find a really good Quick
Sort example in the JDK's sample applets. We have also learned that we can sort using
binary trees, thus, in this section, we won't cover that approach.
We will be using JDK 1.2's java.lang.Comparable interface in this section, thus, the
examples will not work with earlier versions of the JDK. The sorting algorithms we'll look at
in this section are: Bubble Sort, Selection Sort, Insertion Sort, Heap Sort, and lastly, Merge
Sort. If you think that's not enough, you're welcome to find other algorithms from other
sources. One source I'd recommend is the JDK's sources. The JDK has lots of code, and lots
of algorithms that may well be very useful.
We won't exactly cover the algorithms; I will simply illustrate them, and you'll have to
figure out how they work from the source. (By tracing them.) I don't have the time to go
over each and every algorithm; I'm just taking my old C source, converting it to Java, and
inserting it into this document ;-)
import java.io.*;
import java.util.*;
import java.lang.*;
The above program both illustrates the algorithms, and tests them. Some of these may
look fairly complicated; usually, the more complicated a sorting algorithm is, the faster
and/or more efficient it is. That's the reason I'm not covering Quick Sort; I'm too lazy to
convert that huge thing! Some sample output from the above program follows:
starting...
adding: 22 63 33 19 82 59 70 58 98 74
printing: 19 22 33 58 59 63 70 74 82 98
Done ;-)
I think you get the idea bout sorting. You can always optimize the above sorting
methods, use native types, etc. You can also use these in derived classes from
java.util.Vector, using the java.util.Vector data directly. And just when you though
we were done with sorting, here we go again...
import java.io.*;
import java.util.*;
starting...
adding: 91 37 16 53 11 15 89 44 90 58
printing: 11 15 16 37 44 53 58 89 90 91
Done ;-)
All this is nice and useful, but what if you need descending order, instead of the "default"
accenting one? Guess what?, the implementers of the language have though about that as
well! We have a java.util.Comparator interface. With this Comparator, we can specify our
own compare function, making it possible to sort in descending order (or any other way we
want... i.e.: sorting absolute values, etc.).
import java.io.*;
import java.util.*;
As you can see, this time, we're sending a Comparator object to the
Collections.sort() method. This technique is really cool and useful. The output from the
above program follows:
starting...
adding: 9 96 58 64 13 99 91 55 51 95
printing: 99 96 95 91 64 58 55 51 13 9
Done ;-)
I suggest you go over all those JDK classes and their methods. There is a LOT of useful
stuff there. Now, say good bye to sorting for a while; we're moving into something totally
different; NOT!
Bubble sort tends to be O(n^2); in other words, pretty slow. Now, quicksort can perform
quite fast, on average about O(n log n), but it's worst case, is a humiliating O(n^2). For
quicksort, the worst case is usually when the data is already sorted. There are many
algorithms to make this "less likely to occur," but it does happen. (the O notation is
paraphrased "on the order of")
If you need a no-worst-case fast sorting algorithm, then by all means, use heap sort
(covered earlier). In my opinion, heap sort is more elegant than any other sort <it is in
place O(n log n) sort>. However, on average, it does tend to perform twice as slow as
quicksort.
UPDATE: I've recently attended a conference at the [insert fancy name here] Science
Society, and heard a nice talk by a professor from Princeton. The talk was totally dedicated
to Quicksort. Apparently, if you modify the logic a bit, you can make Quicksort optimal! This
is a very grand discovery! (unfortunately, I didn't write down the name of anybody; not
even the place!) The idea (among other things) involves moving duplicate elements to one
end of the list, and at the end of the run, move all of them to the middle, then sort the left
and right sides of the list. Since now Quicksort performs well with lots of duplicate values
(really well actually ;-), you can create a radix-like sort that uses embedded Quicksort to
quickly (very quickly) sort things. There's also been a Dr.Dobbs article (written by that
same professor ;-) on this same idea, and I'm sorry for not having any more references
than I should. I still think this is something that deserved to be mentioned.
We will start out by writing a general (simplest) implementation of quicksort. After we
thoroughly understand the algorithm we will re-write it implementing all kinds of tricks to
make it faster.
Quicksort is naturally recursive. We partition the array into two sub-arrays, and then re-
start the algorithm on each of these sub-arrays. The partition procedure involves choosing
some object (usually, already in the array); If some other object is greater than the chosen
object, it is added to one of the sub-arrays, if it's less than the chosen object, it's added to
another sub-array. Thus, the entire array is partitioned into two sub-arrays, with one sub-
array having everything that's larger than the chosen object, and the other sub-array
having everything that's smaller.
The algorithm is fairly simple, and it's not hard to implement. The tough part is making it
fast. The implementation that follows is recursive and is not optimized, which makes this
function inherently slow (most sorting functions try to avoid recursion and try to be as fast
as possible). If you need raw speed, you should consider writing a native version of this
algorithm.
As you can see, the qsort() functions itself is fairly short. One of them is just a wrapper
for the other one. The idea is that qsort() receives the array, and then the start, and end
pointer between which you want everything sorted. So, the starting call starts at array
position zero, and ends at the last valid position in the array. Some sample output follows:
inserting:
58 52 82 27 23 67 37 63 68 18 95 41 87 6 53 85 65 30 10 3
sorted:
3 6 10 18 23 27 30 37 41 52 53 58 63 65 67 68 82 85 87 95
Done ;-)
The sorts starts out by first checking to see if the end pointer is less then or equal to the
start pointer. It if is less, then there is nothing to sort, and we return. If they are equal, we
have only one element to sort, and an element by itself is always already sorted, so we
return.
If we did not return, we make a pick. In our example, we simply choose the first element
in the array to use as our partition element (some call it pivot element). To some people,
this is the most critical point of the sort, since depending on what element you choose,
you'll get either good performance, or bad. A lot of the times, instead of choosing the first,
people choose the last, or take a median of the first, last and middle elements. Some even
have a random number generator to pick a random element. However, all these techniques
are useless against random data, in which case, all those tricky approaches can actually
prove to worsen results. Then again, most of the times, they do work quite nicely... (it
really depends on the type of data you are working with)
After we have picked the element, we setup i and j, and fall into the a for loop. Inside
that loop, we have two inner loops. Inside the first inner loop, we scan the array looking for
the first element which is larger than our picked element, moving from left to right of the
array. Inside the second inner loop, we scan to find an element smaller than the picked,
moving from right to left in the array. Once we've found them (fallen out of both loops), we
check to see if the pointers haven't crossed (since if they did, we are done). We then swap
the elements, and continue on to the next iteration of the loop.
You should notice that in all these loops, we are doing one simple thing. We are making
sure that only one element gets to it's correct position. We deal with one element at a time.
After we find that correct position of our chosen element, all we need to do is sort the
elements on it's right, and left sides, and we're done. You should also notice that the
algorithm above is lacking in optimization. The inner loops, where most of the time takes
place are not as optimized as they should be. However, for the time being, try to
understand the algorithm; and we will get back to optimizing a bit later.
When we fall out of the outer loop, we put our chosen element into it's correct position,
calculate the upper and lower bounds of the left and right arrays (from that chosen
elements), and recursively call the method on these new computed arrays.
That's basically it for the general algorithm. In the next section, you'll be amazed at how
much faster we can make this work. Basically, in the next section, we will implement a sort
function to use everywhere (it will be faster than most other approaches).
Optimizing Quicksort...
Optimizing Quicksort written in Java is not such a great task. Whatever tricks we use, we
will still be restrained by the speed of the Java Virtual Machine (JVM). In this section, we will
talk about algorithmic improvements, rather than quirky tricks.
The first thing what we should do is look at the above code of the un-optimized sort, and
see what can be improved. One thing that should be obvious is that it's way too much work
if the array is very small. There are a LOT simpler sorts available for small arrays. For
example, simple insertion sort has almost no overhead, and on small arrays, is actually
faster than quicksort! This can be fixed rather easily, by including an if statement to see if
the size of the array is smaller than some particular value, and doing a simple insertion sort
if it is. This threshold value can only be determined from doing actual experiments with
sample data. However, usually any value less than 15 is pretty good; and that is why we
will choose 7 for our upcoming example.
What makes quicksort so fast is the simplicity of it's inner loops. In our previous
example, we were doing extra work in those inner loops! We were checking for the array
bounds; we should have made some swaps beforehand to make sure that the inner loops
are as simple as possible. Basically, the idea is to make sure that the first element in the
array is less than or equal to the second, and that the second is less than or equal to the
last. Knowing that, we can remove those array bounds checking from the inner loops (in
some cases speeding up the algorithm by about two times).
Another thing that should be obvious is recursion. Because sort methods are usually very
performance hungry, we would like to remove as much function calls as possible. This
includes getting rid of recursion. The way we can get rid of recursion is using a stack to
store the upper and lower bounds of arrays to be sorted, and using a loop to control the
sort.
A question automatically arises: How big should that stack be? A simple answer is: It
should be big enough. (you'll be surprised how many books avoid this topic!) We cannot
just use java.util.Vector object for it's dynamic growing ability, since that would be way
too much overhead for simple stack operations that should be as quick as possible. Now,
what would be a big enough stack? Well, a formula below calculates the size:
size = 2 * ln N / ln 2
Where ln is natural logarithm, and N is the number of elements in the array. Which all
translates to:
size = (int)(Math.ceil(2.0*Math.log(array.length)/Math.log(2.0));
Believe it or not, but it's actually not worth using this equation. If you think about it, you
will notice that the size of the stack grows VERY slowly. For example, if the array.length is
0xFFFFFFFF in length (highest 32 bit integer value), then size will be 64! We will make it
128 just in case we will ever need it for 64 bit integers. (If you don't like magic values
inside your program, then by all means, use the equation.)
Having gotten to this point, we are almost ready to implement our optimized version of
quicksort. I say almost because it is still not optimized to it's fullest. If we were using native
types instead of Comparable objects, then the whole thing would be faster. If we
implementing it as native code, it would be even faster. Basically, most other speed-ups are
left up to these system or program specific quirky optimizations. And now, here's our
optimized version:
import java.lang.*;
import java.io.*;
inserting:
17 52 88 79 91 41 31 57 0 29 87 66 94 22 19 30 76 85 61 16
sorted:
0 16 17 19 22 29 30 31 41 52 57 61 66 76 79 85 87 88 91 94
Done ;-)
This new sort function is a bit larger than most other sorts. It starts out by setting left
and right pointers, and the stack. Stack allocation could be moved outside the function,
but making it static is not a good choice since that introduces all kinds of multithreading
problems.
We then fall into an infinite loop in which we have an if() and an else statement. The
if() statement finds out if we should do a simple insertion sort, else, it will do quicksort. I
will not explain insertion sort here (since you should already be familiar with it). So, after
insertion sort, we see if the stack is not empty, if it is, we break out of the infinite loop,
and return from the function. If we don't return, we pop the stack, set new left and
right pointers (from the stack), and continue on with the next iteration of the loop.
Now, if threshold value passed, and we ended up doing quicksort we first find the
median. This median is used here to make the case with bad performance less likely to
occur. It is useless against random data, but does help if the data is in somewhat sorted
order.
We swap this median with the second value in the array, and make sure that the first
value is less than or equal than the second, and the second is less than or equal than the
last. After that, we pick our partition element (or pivot), and fall into an infinite loop of
finding that pivot's correct place.
Notice that the most inner loops are fairly simple. Only one increment or decrement
operation, and a compare. The compare could be improved quite a bit by using native
types; instead of calling a function. The rest of this part of the sort is almost exactly like in
the un-optimized version.
After we find the correct position of the pivot variable, we are ready to continue the sort
on the right sub-array, and on the left sub-array. What we do next is check to see which
one of these sub-arrays is bigger. We then insert the bigger sub-array bounds on to the
stack, and setup new left and right pointers so that the smaller sub-array gets processed
next.
I guess that's it for quicksort. If you still don't understand it, I doubt any other reference
would be of any help. Basically go through the algorithm; tracing it a few times, and
eventually, it will seem like second nature to you. The above function is good enough for
most purposes. I've sorted HUGE arrays (~100k) of random data, and it performed quite
well.
Radix Sort...
Radix sort is one of the nastiest sorts that I know. This sort can be quite fast when used
in appropriate context, however, to me, it seems that the context is never appropriate for
radix sort.
The idea behind the sort is that we sort numbers according to their base (sort of). For
example, lets say we had a number 1024, and we break it down into it's basic components.
The 1 is in the thousands, the 0 is in the hundreds, the 2 is in the tens, and 4 is in some
units. Anyway, given two numbers, we can sort them according to these bases (i.e.: 100 is
greater than 10 because first one has more 'hundreds').
In our example, we will start by sorting numbers according to their least significant bits,
and then move onto more significant ones, until we reach the end (at which point, the array
will be sorted). This can work the other way as well, and in some cases, it's even more
preferable to do it 'backwards'.
Sort consists of several passes though the data, with each pass, making it more and
more sorted. In our example, we won't be overly concerned with the actual decimal digits;
we will be using base 256! The workings of the sort are shown below:
Numbers to sort: 23 45 21 56 94 75 52 we create ten queues (one queue for each
digit): queue[0 .. 9]
We start going thought the passes of sorting it, starting with least significant digits.
queue[0] = { }
queue[1] = {21}
queue[2] = {52}
queue[3] = {23}
queue[4] = {94}
queue[5] = {45,75}
queue[6] = {56}
queue[7] = { }
queue[8] = { }
queue[9] = { }
Notice that the queue number corresponds to the least significant digit (i.e.: queue 1
holding 21, and queue 6 holding 56). We copy this queue into our array (top to bottom, left
to right) Now, numbers to be sorted: 21 52 23 94 45 75 56 We now continue with
another pass:
queue[0] = { }
queue[1] = { }
queue[2] = {21,23}
queue[3] = { }
queue[4] = {45}
queue[5] = {52,56}
queue[6] = { }
queue[7] = {75}
queue[8] = { }
queue[9] = {94}
Notice that the queue number corresponds to the most significant digit (i.e.: queue 4
holding 45, and queue 7 holding 75). We copy this queue into our array (top to bottom, left
to right) and the numbers are sorted: 21 23 45 52 56 75 94
Hmm... Isn't that interesting? Anyway, you're probably wondering how it will all work
within a program (or more precisely, how much book keeping we will have to do to make it
work). Well, we won't be working with 10 queues in our little program, we'll be working with
256 queues! We won't just have least significant and most significant bits, we'll have a
whole range (from 0xFF to 0xFF000000).
Now, using arrays to represent Queues is definitely out of the question (most of the
times) since that would be wasting tons of space (think about it if it's not obvious). Using
dynamic allocation is also out of the question, since that would be extremely slow (since we
will be releasing and allocating nodes throughout the entire sort). This leaves us with little
choice but to use node pools. The node pools we will be working with will be really slim,
without much code to them. All we need are nodes with two integers (one for the data, the
other for the link to next node). We will represent the entire node pool as a two dimensional
array, where the height of the array is the number of elements to sort, and the width is two.
import java.lang.*;
import java.io.*;
Yeah, not exactly the most friendliest code I've written. A few things to mention about
the code. One: it's NOT fast (far from it!). Two: It only works with positive integers. Sample
output:
original: 1023 1007 583 154 518 671 83 98 213 564 572 989 241 150 64
sorted: 64 83 98 150 154 213 241 518 564 572 583 671 989 1007 1023
Done ;-)
I don't like this sort much, so, I won't talk much about it. However, a few words before
we go on. This sort can be rather efficient in conjunction with other sorts. For example, you
can use this sort to pre-sort the most significant bits, and then use insertion sort for the
rest. Another very crucial thing to do to speed it up is to make the node pool and queues
statically allocated (don't allocate them every time you call the function). And if you're
sorting small numbers (i.e.: 1-256), you can make it 4 times as fast by simply removing the
outer loop.
This sort is not very expandable. You'd have problems making it work with anything
other than numbers. Even adding negative number support isn't very straight forward.
Anyway, I'm going to go and think up more reasons not to use this sort... and use
something like quick-sort instead.
After I got the code down to a mere 14 lines, I still wasn't satisfied with the inner loop
that searched for the last node on the queue. So, created another array, just to hold the
backs of queues. That eliminated lots of useless operations, and increased speed quite a bit,
especially for large arrays.
After all that, I've moved the static arrays out of the function (just for the fun of it), since
I didn't want to allocate exactly the same arrays every single time.
With each and every improvement, I was getting code which sucked less and less. Later,
I just reset the 32 bit size to 16 bit integer size (to make it twice as fast, since the test
program only throws stuff as large as 1024). I didn't go as far as unrolling the loops, but for
a performance needy job, that could provide a few extra cycles.
Anyway, I won't bore you any longer, and just give you the new and improved code
(which should have been the first one you've saw, but... I was lazy, and didn't really feel
like putting in lots of effort into radix sort).
import java.lang.*;
import java.io.*;
This code is so much better than the previous one, that I'd consider it as fast as quicksort
(in some cases). Counting the looping, we can get an approximate idea of how fast is it...
First, we have a loop which repeats 2 times (for 16 bit numbers), inside of it, we have 2
loops, both of which repeat on the order of N. So, on average, I'd say this code should be
about 2*2*N, which is 4N... (not bad... <if I did it correctly>), imagine, if you have 1000
elements in the array, the sort will only go through about 4000 loops (for bubble sort, it
would have to loop for 1,000,000 times!). True, these loops are a bit complicated compared
to the simple bubble sort loop, but the magnitude is still huge.
Anyway, I should really get some sleep right about now (didn't get much sleep in the
past few days due to this little 'thought' which kept me awake). Never settle for code which
you truly believe sucks (go do something about it!).