This page and the provided source files are based on the 3nd edition of the textbook. Use this page if possible (it's definitely better), but you may instead continue to use the old 2nd-edition version if you've already started working on that. The two versions differ somewhat in the terminology, the Java interfaces, and the provided source code.
Goal of this homework: This is the first part of a longer assignment on election systems. You will implement a heap-based adaptable priority queue (with comparators and location-aware entries), more or less as described in the book. You will then apply it to real voting data that you read from a file.
The next two assignments will extend this work. By the end, you will have built a complicated system with several interacting data structures.
Collaboration policy for this homework:
You must follow the CS
Department Academic Integrity Code, except that for this
homework, you may optionally work with one other student in the
class. If you do so, you must say who your partner was at the top of
your Readme
file, and both of you will receive the same grade
for the assignment.
How to hand in your work: instructions, upload page
What to hand in (all packaged up in a zip or tar file):
Your commented code:
SimpleElection.java
HeapPriorityQueue.java
(as improved by you)
VectorCompleteBinaryTree.java
(as improved by you)
HeapAdaptablePriorityQueue.java
(as completed by you)
ReportedSimpleElection
(an improved version of SimpleElection
)
.java
files, written either by us or by
you, that are necessary to make your program run correctly. Please
make sure that you include everything that is needed to make
your code compile!
A doc
subdirectory containing HTML documentation
that you have generated with the command javadoc -private
-linksource -d doc *.java
. The graders will look at this
documentation, so you should provide javadoc comments for all of
your methods, including private ones.
A Readme
file that contains your
answers to miscellaneous questions on the homework (i.e., answers that
don't go in any other file). Usually these are English paragraphs or small
fragments of code. The top of your Readme
should
state the name of your partner (if any).
Acknowledgments: Thanks to Usman Zaheer and Yuan Chen for their help in preparing this assignment.
A task queue. New tasks are inserted on a person or computer's priority queue, keyed by their importance. Whenever the person or computer has some free time, he/she/it removes the most important task from the queue and takes care of it.
A politician queue. In a city council election, 24 candidates are running for 9 seats. The 24 candidates are stored on a priority queue, keyed by the number of votes they have. The Board of Elections repeatedly removes the most popular candidate and places him/her on the council. So the priority queue tells the Board whom to elect next. (The order of election is important since the first candidates to get on the council always grab the best desks!)
An appointment reminder queue. Your personal digital assistant (e.g., a Palm Pilot) remembers all your appointments stretching far into the future. It is set to beep and flash a message ten minutes before each appointment. The appointments that it hasn't reminded you about yet are stored on a "reminder queue." At least once per second, the PDA checks the reminder queue to see whether you have an appointment in 10 minutes (or less). For example, at 7:54:03 it will check to see whether you have an appointment scheduled at the unlikely time of 8:04:03. If you do, it reminds you about this appointment and deletes it from the reminder queue.
Answer the following in your Readme
file:
In the Palm Pilot example, the "reminder queue" must support three operations: CHECK whether there is an appointment 10 minutes from now or less, ADD a new appointment to the queue, DELETE an appointment from the queue once the reminder has been delivered.
This assignment will deal with the city council election example. We will indeed construct a priority queue of all candidates, as described above, for reasons that will become clear later. But if we had 1,000,000 candidates for the 9 seats, this priority queue would be quite big and slightly slow. Briefly sketch a more efficient method that does exactly the same thing - prints out the top 9 candidates from best-to-worst order - using only a small priority queue and a small stack. (We developed most but not all of the answer in class.)
Download all the source code we are giving you. You can download the files one at a time, or get it all at once in a zip file.
Your first job is simply to try out a heap-based priority queue, as discussed in the book and in class. The priority queue will implement the following interface:
public interface PriorityQueue { public int size(); public boolean isEmpty(); public Entry insert(Object key, Object value) throws InvalidKeyException; public Entry min() throws EmptyPriorityQueueException; public Entry removeMin() throws EmptyPriorityQueueException; } |
We are providing you with the full HeapPriorityQueue
class that is defined in the book, as well as the class that it is
based on, VectorCompleteBinaryTree
(which implements CompleteBinaryTree
).
You will have to make a couple of changes to the code at this
point. The VectorCompleteBinaryTree
class won't compile
as given, because it is written to use the Vector
interface developed in the textbook. You should trivially fix it to
use Java's standard java.util.Vector
class (you'll want to click that link to read the class
documentation). Once you've done that, it should compile.
Read the code for these classes and interfaces. You are expected to understand it for the midterm. Moreover, later in the assignment you will be required to modify this textbook code to make it more efficient! The interface won't change, just the implementation.
A few remarks:
HeapPriorityQueue
has a constructor that takes a
comparator as its argument. There's also a constructor with no
arguments, which makes a priority queue that compares keys using their
compare()
method.
In class, we've talked about storing a heap tree in an array.
But then you'd have to double the array capacity yourself whenever it
got full. Why duplicate a solution that's already been written
elsewhere? Instead of an array, it makes more sense to use a vector
class that will automatically double as necessary. That's what
VectorCompleteBinaryTree
class does.
The CompleteBinaryTree
interface can support an
empty tree with no nodes at all (to represent an empty priority
queue). This is a change from homework 5. Remember that a tree with
n nodes is represented by a vector of length n+1,
because of the empty element 0. So an empty tree is a vector of
length 1. An empty tree has no root, so the root()
method should throw TreeEmptyException
.
The HeapPriorityQueue
's min()
method
returns the root of the heap tree, so it can't work if the heap is
empty. But it does
not throw an EmptyTreeException
in this case.
Instead, it throws an EmptyPriorityQueueException
. Why
the heck do we need so many different exceptions?
Well, the appropriate exception to throw is specified by the
interface. And the PriorityQueue
interface certainly
isn't going to require throwing an EmptyTreeException
,
because some implementations of priority queues might not use trees at
all. Make sense?
(If we wanted to cut down on the number of
different exceptions, we could just use some kind of generic
EmptyCollectionException
whose name doesn't specify
trees, lists, queues, or anything.)
Answer the following in your Readme
file:
The BinaryTree
interface and its extension
CompleteBinaryTree
do not require you to support
createExternal()
. Remember that your
LinkedBinaryTree
class in homework 5
did support that method.
You could find a way to implement
createExternal()
in
VectorCompleteBinaryTree
, too ... but it's probably a bad
idea. An application like arithmetic expression parsing
(homework 5), which needs to make arbitrary calls to
createExternal()
, should probably not be using
VectorCompleteBinaryTree
. Why not? (Hint: Think
about space efficiency as well as time efficiency.)
Write a test method to check that HeapPriorityQueue
class works correctly. The Election
class that you will
write below will help you test it, too, but it may not provide a
complete test (unless you write your own ballots file). I recommend
writing a short main(String[] args)
method that sorts
args
(i.e., the strings specified on the command line).
As it inserts each string, it can print out the minimum string so far.
Then it can remove them all from the priority queue to print them in
sorted order. When you make the code more efficient, below, you can
use the same test method to make sure it still works correctly. The
graders will also test your code.
Everyone now knows how important it is to count the votes correctly. In the aftermath of Bush v. Gore and their hanging chads, computer scientists have been important figures in thinking about the design of election equipment (or Internet voting) that is as resistant as possible to error and fraud.
Computer scientists (along with economists) also like to think about alternative voting systems. In this and subsequent assignments, we will be working with real data from the 1999 City Council election in Cambridge, MA. Like Ireland and Malta and many professional societies, Cambridge uses an interesting alternative voting system that we will deal with in homework 8.
For this assignment, we'll just count the votes in the usual way. There were 24 candidates for 9 seats in the election. So you will end up with a heap of size 24, which is large enough to test your code.
Your data are in the file ballots
, which contains nearly 19,000
ballots. Download it. The candidates are numbered from 1 to 24, and
each ballot is represented by a single line with the number of the
selected candidate. Here are the names of the candidates in an array
for you:
private static final String[] CANDIDATES = { null, // there is no candidate 0 "Born", "Braude", "Chase", "Christenson", "Davis", "Decker", "Dixon", "Galluccio", "Giacobbe", "Goodwin", "Hoicka", "Jones", "Maher", "Nidle", "Peixoto", "Reeves", "Snowberg", "Sullivan", "Toomey", "Triantafillou", "Trumbull", "Williamson", "Winters", "Wormwood-Malone" }; |
You should write an SimpleElection
class with an appropriate
main()
method so that
java SimpleElection 9 ballots
ballots
file and print out the names of the 9
council-seat winners, in order, starting like this:
(2730 votes) Candidate 8: Galluccio (1669 votes) Candidate 1: Born (1653 votes) Candidate 6: Decker ... |
You will use the HeapPriorityQueue
class to sort the
candidates into order (winner first). The easiest overall solution is
to start by counting up each candidate's total and storing it in an
array or java.util.Vector
. Once you have done this, you
can insert all the candidates into your heap, as discussed in the
assignment introduction, and proceed in the obvious way. The elements
on your heap can be candidate names or numbers, as you prefer.
Try to observe good style. It is bad style for your
SimpleElection
class to consist of a single
main()
function. You should break it up into logical
units. It would also be bad style to make all your methods
static
. You should allow for the possibility of many
SimpleElections
going on at once. Ask yourself: What
kind of data should be maintained by a SimpleElection
object? What are the natural methods to update and access these data
during an election?
We will be gentle about testing your code this week, but it is good to try to anticipate and handle any weird conditions and exceptions that may arise. In next week's assignment you will be expected to make this code very robust. You should also worry about which methods are public. After all, elections need to be both error-proof and tamper-proof!
Caution: Of course, your program should not be specialized
to the number 9
or the filename ballots
; it
should get them from the command-line arguments that are passed to
main()
. In particular, you should be able to handle
any number of candidates, numbered 1 through n. It is true
that the CANDIDATES
array we gave you above only holds 24
names, so for candidates 25 and up (if n > 24) you should just use an
empty string for the name: that is, just print "Candidate 25:
"
. The CANDIDATES
array is just a hack that we'll
fix in next week's assignment (when the ballot format will change to
specify names directly, not numbers).
Hint: A web search will turn up lots of help on Java file
I/O. Here is a
short lesson on how to read from a file in Java, among other
things. (It has one error: parseInt()
in one
place is incorrectly written as parseInteger()
.)
Hint: By default, your heap will use a
DefaultComparator
that compares Integer
s in
the usual way -- so the candidate with the fewest votes will
end up on top of the heap. You will need to write your own comparator
that orders the vote counts in the opposite way. How? That's for you
to figure out, but here are a few other comparator
classes that might inspire you.
// Uses the "natural order" on objects by calling their own compareTo methods. // (A copy of this appears in HeapPriorityQueue as an inner class.) public class DefaultComparator implements java.util.Comparator { public int compare(Object a, Object b) { return ((Comparable) a).compareTo(b); } } // Like DefaultComparator, but specialized to Integers. public class DefaultIntegerComparator implements java.util.Comparator { public int compare(Object a, Object b) { return ((Integer) a).compareTo(b); } } // Does the same thing as above, but more directly (without calling compareTo()). public class IntegerComparator implements java.util.Comparator { public int compare(Object a, Object b) { return (Integer)a - (Integer)b; } } // Wraps an existing comparator to make another comparator with related behavior. // You might find this wrapping technique useful in reversing an existing comparator, // such as DefaultComparator. // // In this case, we are wrapping an existing comparator that cannot compare null, // with a new comparator that treats the special key null as less than everything else -- // essentially, as negative infinity. It can be handy to have such a key. public class NullComparator implements java.util.Comparator { java.util.Comparator c; // the original comparator public NullComparator(java.util.Comparator c) { this.c = c; } public int compare(Object a, Object b) { if (a==null) return (b==null ? 0 : -1); // null is less than everything but null else if (b==null) return 1; // everything else is greater than null else return c.compare(a,b); // if neither is null, use original comparator } } |
There are several things that can be improved in the book's
HeapPriorityQueue
and
VectorCompleteBinaryTree
code. Please fix them as
follows. This should not affect the results of the election at all,
nor should it affect the results of the test method
(main()
) that you wrote earlier. So you can keep re-testing
after each change, to make sure everything still works.
HeapPriorityQueue
Eliminate the duplication of code in entry()
and
key()
, and also in min()
and
removeMin()
.
The upHeap()
and downHeap()
methods
use random letters as variable names. It's hard to keep them
straight when you are writing or reading the code. Change these
names: follow Java's usual conventions and use the name
p
for the Position you are
mainly concerned with. If you need another position for a
temporary variable, call it something meaningful like
parent
or parentPosition
or
pParent
. (Be consistent in your naming style.) This
approach results in clear code.
Similarly, in the supplied code for
VectorCompleteBinaryTree
, we already made a change for
you: we changed the name of the vector from T
(as in
the book) to v
. Certainly v
is a
reasonable name: it indicates to the reader that it's an instance of
Vector
and supports vector methods. Whereas
T
was a terrible name: it's not clear that it's a
vector, and in fact it looks like the name of a class (since it is
capitalized) or maybe a constant (since it is in
all-caps).
You should now revise upHeap()
and
downHeap()
to be more efficient. The book's use of
swapElements()
in these functions is elegant but not
completely efficient. (There is sometimes a conflict between
elegance and efficiency.)
Suppose you replace
1
with
15
at the root of the heap, and bubble it down past 3
, 5
, and
7
. Here's what happens using swapElements
:
1 15 3 3 3 (Note: these diagrams / \ / \ / \ / \ / \ only show a portion 3 3 15 5 5 of the complete tree. / \ --> / \ --> / \ --> / \ --> / \ They show only the path 5 5 5 15 7 that 15 follows as it / \ / \ / \ / \ / \ bubbles down. Other 7 7 7 7 15 children in the tree / \ / \ / \ / \ / \ are not shown.) 99 99 99 99 99But it's a waste of time to store
15
at all the intermediate locations like this,
where it will just get immediately overwritten anyway. It
makes more sense to just shift 3
, 5
, and
7
upwards and then insert 15
below them. You can
think of this as bubbling a "hole" downwards and then finally
inserting 15 in the hole:
[ ] 3 3 3 3 / \ / \ / \ / \ / \ 3 [ ] 5 5 5 / \ --> / \ --> / \ --> / \ --> / \ 5 5 [ ] 7 7 / \ / \ / \ / \ / \ 7 7 7 [ ] 15 / \ / \ / \ / \ / \ 99 99 99 99 99You can see above that you are repeatedly filling the hole by sucking a value into it from below, until finally you fill it with
15
. Of course there is not really
a hole. The hole is just the position that you are about to
overwrite. There is still an element physically stored there, although
you don't care what it is. So here is what it really looks like in
memory:
[1] 3 3 3 3 / \ / \ / \ / \ / \ 3 [3] 5 5 5 / \ --> / \ --> / \ --> / \ --> / \ 5 5 [5] 7 7 / \ / \ / \ / \ / \ 7 7 7 [7] 15 / \ / \ / \ / \ / \ 99 99 99 99 99In short, you don't have to swap each parent-child pair. You can merely copy each child up to its parent, as shown above. A swap requires three
=
statements (including the temporary
variable) whereas a copy requires only one. In effect, you are
shifting the whole sequence 3
, 5
,
7
upwards to make a space below it.
How about when you bubble an element up instead of down? You can similarly shift a sequence downward to make a space above it. (The same technique applies in bubble sort, insertion sort, and selection sort, where you shift part of an array sideways to make a space.)
You should fix upHeap()
and downHeap()
to
use these techniques instead of using swapElement()
.
Efficiency does matter, these techniques are pretty elegant
themselves, and copying code out of the book isn't a great learning
experience. :-)
Hint: Improve upHeap
first (it's simpler) and
make sure that it works before trying downHeap()
. I
suggest that you change the method signature to take an extra
argument, as follows:
/** Store e at position p and then bubble it up the tree. * * (For efficiency, we don't actually store e anywhere until we * know where it will end up: we bubble the hole at p up the tree, * and then store e into the hole's final position.) */ protected void upHeap(Position p, Entry e) { ... } // downHeap() should be changed similarly. Note that you can // safely change these method signatures because the methods // are protected -- other classes are not calling them in their // old form. |
When improving downHeap()
, you may also want to
reorganize it so that it looks as much like upHeap()
as
possible. This isn't required, but it is good style and should help
convince you that the code is correct. You might start by making
downHeap()
call a new helper method
childSmaller()
, which should return the smaller child
of an internal node (i.e., one with children). This is analogous to
the way that upHeap()
calls parent()
,
which returns the parent of a non-root node (i.e., one with
parents).
The string messages in throw new
EmptyPriorityQueueException("Priority queue is empty")
and
throw new InvalidKeyException("InvalidKey")
just
uselessly repeat the name of the exception. No point in that.
Please omit these string arguments or change them to somthing useful.
Arguments to an exception constructor should be used to
provide additional information to the user -- or perhaps to the
catch
block that will handle the exception. Good
examples:
throw new EmptyPriorityQueueException()
throw new InvalidKeyException("comparator doesn't know how to compare key " + k)
throw new IllegalArgumentException("Null comparator passed to constructor")
throw new IllegalPositionException("invalid index " + i + " in VectorCompleteBinaryTree of size " + size())
throw new SuccessException(result)
catch
block from deep within some nested
loops or function calls)
VectorCompleteBinaryTree
Several spots in VectorCompleteBinaryTree
independently
convert from an index to a Position
(by looking up a
vector element and casting it to a Position) or vice-versa. You
should eliminate this duplication of code. The trick is to introduce
convenience methods as follows:
/** Converts an index to a position. */ protected Position position(int index) { return ...; } /** Converts a position to an index. */ protected int index(Position p) { VectorNode vp = checkPosition(p); return vp.index(); } |
Now all of those spots in the code can just call
position()
or index()
as appropriate. This
makes the code simpler and more readable; as you'll see below, it also
makes it more maintainable. Make sure the code still works after your
changes.
A more serious problem is that the
VectorCompleteBinaryTree
class is implemented using about
twice as much space as necessary. The figure below shows why, and how
you should fix it.
(What are the elements in the diagram above? They are the objects
stored at the nodes of the CompleteBinaryTree
. When you
use the CompleteBinaryTree
to make a heap priority queue,
those objects will be Entries
, but the
CompleteBinaryTree
class is written as a general-purpose
class that could store other kinds of objects, too, if you were using
it for something else.
Here's some code to get you started. In VectorCompleteBinaryTree.java
,
replace the inner class VectorNode
with this one:
protected static class VectorPosition implements Position { int index; // index of this position in the vector. Vector v; // a pointer to the vector containing this index. // We need to store this pointer in order to support // the element() method. public VectorPosition(Vector v, int index) { this.v = v; this.index = index; } public Object element() { return v.get(index); } // to implement the Position interface public int index() { return index; } public Object setElement(Object elt) { Object temp = element(); v.set(index,elt); return temp; // return the element formerly at this position, for convenience } } |
Notice in the diagram above that your vector should no longer hold
positions, but instead should directly hold the elements (e.g.,
entries). This will affect how you add()
elements to a
vector, for instance.
An important change is to the position()
convenience
method that you defined earlier to convert an index to a position. It
can't just look up the position anymore, since the vector doesn't
store positions. Instead, it will actually have to call the
VectorPosition
constructor to build a position.
Fortunately, since this index-to-position conversion is now localized
in the single position()
method you wrote, that's the
only place you have to make the change. If you hadn't eliminated
duplicate code by writing position()
, then you'd
have to change every copy of the duplicate code!
[Note: This section is all explanation. Just read it, as
you would read the textbook or a handout, and answer the questions in your
Readme
. The next section will ask you to implement this
design.]
The introduction to this assignment gave 3 example applications of a priority queue. Let's think about those applications some more:
The task queue. Suppose you remove a "business meeting" task from the priority queue. Carrying out this task may involve putting new tasks on your queue - "put on coat" and "walk to meeting," not to mention any new tasks that arise during the meeting itself.
More interestingly, carrying out a task may cause you to reprioritize or delete tasks that are already on the queue. At your meeting, you may learn that the company went bankrupt. So you can delete "hire a better assistant" from your queue, while the priority of "find a better job" goes way up.
The appointment reminder queue. If you reschedule or cancel an appointment, it has to be reprioritized or deleted on on the reminder queue.
The politician queue. If some votes for Decker come in late, then she has to be reprioritized. Or if she drops out of the race, she has to be deleted.
Alas, the basic PriorityQueue
interface doesn't let you
manipulate entries that are not at the front of the priority queue.
So it doesn't support reprioritization (a change to an entry's key),
or deletion (removal of the entry).
No object-oriented solution to this problem was published until
1999, in a paper by Goodrich and Tamassia, the authors of the
textbook. That should prove to you that quite basic innovations in
data structures are still possible -- an undergrad like you could have
thought of this one! Section 7.4 of the book describes how to extend
PriorityQueue
to solve this problem, using
"location-aware entries."
As a design lesson, this section will take you through the steps that lead to the invention of location-aware entries. As usual, there is no magic here, just repeated revision of the design to solve the problems that may come up.
Entry
in the Adaptable Priority QueueRemember that the elements stored at the nodes of the
CompleteBinaryTree
are "entries," each of which is a
(key,value) pair. If we can find the Entry
that we want
to change, we can easily change it in any of the following ways:
Change the Entry
's key. So the implementation
of Entry
needs to have a setKey()
method.
But it's not enough just to call that method, as changing a key
might destroy the heap property. We need the priority queue to have a
replaceKey()
method that calls Entry.setKey()
and then bubbles the element upward or downward as necessary to
restore the heap property.
Change the Entry
's value. This can't affect the
heap property, so it really is just a matter of calling a
setValue()
method on the entry. No need to bubble.
But for symmetry with replaceKey()
, we'll want to give
the priority queue a replaceValue()
method that calls
Entry.setValue()
.
Delete the Entry
. We'll give the priority queue
a remove()
method for this. It's your job to figure
out how it should work. (Hint: Generalize the idea behind
removeMin()
.)
We can do all this if we can find the Entry
that
we want to change. Specifically, we want the Entry
's
Position
in the heap tree, so that we can bubble it up and
down to ancestor and decendant Position
s if
necessary.
A priority queue that can support these operations is called an "adaptable priority queue" in the textbook. I sometimes call it a "repriority queue," because you can reprioritize its entries.
The main problem is that searching a heap is slow. As we saw in
class, it is O(n). If the Entry
we we want is not at the
root of the heap, we need to search under both children of the root,
and so on recursively.
But forget about efficiency for the moment: suppose we don't mind
O(n). There's still a problem: how do we know when we've found the
desired entry? We can't just search for an existing entry with the
right key and value, because the value type might not implement an
appropriate equals
method.
(We could use the comparator to test keys for equality. But that comparator isn't guaranteed to work for values, which may be of a different type. It would be a nuisance for the user to give us a second comparator that could test values for equality -- and more important, comparing values may be slow or impossible. For example, in Homework 8, each value on our priority queue will be an entire collection of ballots for a candidate. Such large data structures would be slow to compare. In a keyed data structure, it should really not be necessary to compare values -- they are just inert objects attached to the keys, along for the ride.)
A better answer is to hang onto the Entry
we want
(i.e., keep a pointer to it). If we want to reprioritize or delete
that entry later, we can find it in O(n) time by comparing it to all
the existing entries in the heap. Each comparison can be done using
fast ==
(not slow equals
).
Where would we get a pointer to the Entry
? Entries
are constructed by the insert(key,value)
method. So
let's suppose that insert
returns the Entry
it constructs. So we can do something like this this:
Entry e = pq.insert(new Integer(17), new StringBuffer("seventeen")); ... // do some other random stuff to the priority queue ... Position p = pq.find(e); // finds the current position of e
Answer the following in your Readme
file:
How would this find(e)
method work,
in terms of the HeapPriorityQueue
and VectorCompleteBinaryTree
classes?
Entry
by Lookup Instead of SearchNow that we have a reasonable interface, let's revisit the speed
issue. O(n) is just too slow for finding the entry's position on the
heap. What is the point of having fast operations on heap entries
(replaceKey
and remove
are O(log n), and
replaceValue
is O(1)), if finding the entry to operate on
takes O(n) anyway? We would like to find the entry in O(1) time!
A consistent theme in this course has been that you can often trade
space to get speed. "If you'll need it later, store it now. It's
usually faster to look it up than to compute it." So a natural idea
is to use a Dictionary
that maps from each Entry
to its Position
. This indeed takes
O(1) time on average.
(But can one really use Entry
as a hash key? Yes: it
implements both hashCode()
and equals()
methods inherited from Object
. The inherited
equals()
method is the same as ==
, so it
compares the memory addresses where two Entry
s are stored,
which is what we want. The inherited hashCode()
method
is
always designed to give the same hash code to values that
equals()
says are equal: typically it will hash the
memory address where the Entry
is stored.)
Answer the following in your Readme
file:
The hash table dictionary lets you look up an
Entry
to find its Position
. But existing
entries move to different positions as new entires bubble up past
them. How could you keep the dictionary up to date as the priority
queue changes? (Hint: Each HeapPriorityQueue
should now store a CompleteBinaryTree
and ... what?)
How would find(e)
work using this design?
Entry
by Direct LookupBut there is a better way! "If you'll need it later, store
it now," sure ... but it might make a difference where you
store it. Remember that you are already hanging onto the entry (which
insert
returned) for later lookup. Why not store the
entry's position right inside the entry? Then you don't have to
look it up; you've already got it! This is faster by a constant
factor than using an auxiliary hash table. It is also more
space-efficient by a constant factor. And it is certainly
simpler.
The diagrams at the end of this page may help you visualize this design.
Answer the following in your Readme
file:
Your adaptable heap will now store
LocationAwareEntry
objects instead of
MyEntry
objects. The book's
LocationAwareEntry
inner class extends the MyEntry
inner class so that it also stores the entry's current
Position
. (It still has the key()
and
value()
methods needed to implement Entry
, of course, but it has
other methods too.)
As always, existing entries may move to new positions in the heap.
How do you keep the LocationAwareEntry
objects up to date
when they move, so that they always know their Position
s?
(Check section 7.4 of the textbook if you get stuck, but try to figure
it out yourself first.)
There is one more step, which is to wrap the whole set of ideas in a
nice AdaptablePriorityQueue
interface.
According to our design above, the user's calls will look something like this:
Entry e = pq.insert(new Integer(17), new StringBuffer("seventeen")); ... // do some other random stuff to the priority queue ... Position p = pq.find(e); // just returns the position stored inside e, namely // the position ((LocationAwareEntry)e).location() pq.replaceKey(p, new Integer(14)); // change the key from 17 to 14 pq.remove(p);But there is no reason for the user to have to deal with the intermediate
Position
. We can redesign
the reprioritization methods slightly so the user can just say
Entry e = pq.insert(new Integer(17), new StringBuffer("seventeen")); ... // do some other random stuff to the priority queue ... pq.replaceKey(p, new Integer(14)); // change the key from 17 to 14 pq.remove(e);and the lookup of
e
's position is handled inside
those methods.
Here is the resulting interface:
/** Extends the priority queue interface to support removal * and reprioritization of elements. */ public interface AdaptablePriorityQueue extends PriorityQueue { /** Remove an entry, and return that entry (for convenience). * Note that you can say, for example, * Entry e = pq.remove(pq.min()) * which is equivalent to * Entry e = pq.removeMin(); */ public Entry remove(Entry e) throws InvalidEntryException; /** Replace the key of the given entry. * Return the old key, for convenience. */ public Object replaceKey(Entry e, Object key) throws InvalidEntryException, InvalidKeyException; /** Replace the value of the given entry. * Return the old value, for convenience. */ public Object replaceValue(Entry e, Object value) throws InvalidEntryException; } |
Answer the following in your Readme
file:
Suppose e
is a
LocationAwareEntry
. What would go wrong if the
user called e.setKey(k)
rather than
pq.replaceKey(e,k)
?
Notice that setKey()
is a public method of LocationAwareEntry
.
That's because it has to get called from a class other than LocationAwareEntry
(specifically, by HeapAdaptablePriorityQueue
's replaceKey
method). So what prevents the user from making the bad call in the
previous question?
Could the AdaptablePriorityQueue
interface be
implemented without using location-aware entries? Describe two
such implementations. (You should still use a heap.
Hint: Both alternatives were suggested in the previous
discussion.)
Your job in this section is to write a
HeapAdaptablePriorityQueue
class that extends
HeapPriorityQueue
and implements the
AdaptablePriorityQueue
interface.
We've given you some code to start with.
Hints:
Write a test method, main()
, as usual.
Your answer to question 7 suggests that you will have
to override some methods of
HeapPriorityQueue
. I suggest that you first
modify HeapPriorityQueue
to make use of the
following protected convenience method:
protected void replace(Position p, Entry e) { T.replace(p, e); } |
If you carefully ensure that HeapPriorityQueue
always uses replace
when it puts an entry at a
position of the heap tree, then you won't have to override
upHeap()
and downHeap()
, just
replace
.
The first line of the overriding method replace(k,e)
should probably be super.replace(k,e)
, to call the
original method.
You will also have to override insert()
, so that it
creates a LocationAwareEntry
rather than a MyEntry
.
When writing remove()
, think
carefully about the special case where the locator you are removing
is the rightmost one in the heap vector - that is, the very same one
that is removed by CompleteBinaryTree.remove()
.
(Even removeMin()
already had to deal somewhat with
this special case, since for a heap of size 1, it removed the
rightmost (and only!) element in the heap vector. How was that case
different than when the heap had size > 1?)
Elections are newsworthy! The best part of an election is sitting around the TV with your friends, watching your candidate's fortunes lurch up and down as the returns come in. I like to alternately celebrate on chocolate-chip cookies and drown my sorrows in milk.
Using locators, you will now improve your
SimpleElection
class to obtain a
ReportedSimpleElection
class. Remember that
SimpleElection
counted the ballots first and then put the
candidates on the heap. That made for a boring election night. This
time you will put the candidates on the heap first and then count the
ballots. As candidates accumulate votes, their positions on the heap
will change.
You will print a news bulletin whenever the front-runner changes, something like this:
Pulling into the lead with 1 of 1 votes is Candidate 1: Born Pulling into the lead with 2 of 4 votes is Candidate 18: Sullivan Pulling into the lead with 4 of 17 votes is Candidate 19: Toomey ... [At the end, you should print the top candidates as before.] |
This horse race is actually pretty realistic. The ballots are sorted by polling place, so you can watch as the returns come in from different districts of Cambridge. Within polling place, the ballots are sorted by ballot number, which probably means pretty much chronologically.
Start by copying your SimpleElection
class to
ReportedSimpleElection
. (You will have to hand both in.)
Then modify the latter to solve the new problem. The code will be
fairly similar, with the biggest change coming to your countVote
method (if you
have one). Of course you should use a
HeapAdaptablePriorityQueue
.
The following diagrams show how the various data structures are laid out in memory, how they interact, and what happens when the front-runner changes. You should often try drawing diagrams like this yourself.
Coming in Homework 7 and Homework 8!
Jason Eisner - jason@cs.jhu.edu
- $Date: 2004/03/18 03:08:38 $