Goal of this homework: This homework focuses on implementing data structures (rather than using them for a task). First, you will get lots of practice at manipulating the nodes and pointers in a doubly linked list. This will prepare you to deal with more complicated pointer-based structures like trees. Second, you will get experience with the Iterator abstraction, including one rather difficult Iterator that enumerates the elements of a list in sorted order.
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):
NodeList.java
FancyNodeList.java
FancyListIterator.java
FancyListReverseIterator.java
FancyListSortedIterator.java
NumberedString.java
As usual, you should write clear, readable code (see section 1.9.2 of the textbook). Avoid duplicated code. Make the code as simple and direct as possible. It should lay out a natural way of thinking about the problem.
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: Many thanks to Alexandros Batsakis and Hadi Husain for helping to design this assignment and try it out.
On page 207 of the textbook (or p. 191 of the 2nd edition), Proposition 5.2 says: "Let S be a vector implemented by means of an extendable array with initial length 1. The total time to perform a series of n push operations in S, starting from S being empty, is O(n)."
The textbook goes on to justify this proposition using an amortized analysis, in which each "push" operation deposits a constant amount of runtime in the bank, to be used later when the array is doubled. The bank account is never overdrawn, so the total amount of runtime used is at most proportional to the number of pushes.
In your Readme
file:
Note: The above questions are unrelated to the rest of the assignment, but amortized analysis and extensible arrays are important topics.
List
interface,
and a partial implementation of it as a doubly-linked list class
called NodeList
. Each node or position of a
NodeList
is a DNode
object. In this
homework, you will do the following:
Finish the textbook's NodeList
class.
Improve your NodeList
class to use 1 sentinel per
list instead of 2.
Extend your NodeList
class with additional methods
to concatenate two lists, reverse a list, and iterate over a list in
forward, reverse, or sorted order.
Test your new methods using a test class (which we have mostly provided for you).
HW4-provided-source.zip
contains all of the files below, so that you can download them
all at once if you like.
Position.java
is
an interface for an abstract position in a list or other container class
(vector, tree, etc.). DNode.java
implements
a particular kind of Position, namely, a node of a doubly-linked
list. These classes are also straight from the textbook.
List.java
is the list
interface from the textbook. It comes with three exceptions:
BoundaryViolationException.java
,
EmptyContainerException.java
, and
InvalidPositionException.java
.
NodeList.java
is a partial implementation of the List
interface as a
doubly-linked list of DNodes
. This file contains all
the implementation code provided by the textbook, but you will
have to add a few missing methods and make some other changes.
FancyList.java
is an
interface that is provided to you. The skeleton of an
implementation is in FancyNodeList.java
,
which you will complete.
FancyListIterator.java
,
FancyListReverseIterator.java
, and
FancyListSortedIterator.java
are
skeletons for iterator classes that you will fill in. These classes
implement the java.util.Iterator
interface.
NumberedString.java
is a class for testing your FancyNodeList
class.
You will have to fill in a couple of methods.
NodeList
classThe provided file NodeList.java
does not implement all
of the methods of the List
interface. You should
implement the remaining methods: after
,
isLast
, insertAfter
, and
insertLast
. This should be easy if you have read the
other methods in the class. Test your implementation until you are
sure it works correctly.
NodeList
implementationThe NodeList
class wastes a little memory since the
fields header.prev
and trailer.next
aren't
used for anything; they are always null
.
Modify your NodeList
class to use less memory by using
only one sentinel (called sentinel
) instead of two
sentinels (header
and trailer
).
The list should be stored in a "circular" fashion:
sentinel.next
points to the first element, and
sentinel.prev
points to the last element. The old and
new designs look like this:
From the perspective of a user, your new class should behave exactly like the old one, but internally the implementation is slightly different.
NodeList
with extra methodsYou will now add additional methods to concatenate two lists, reverse a list, and iterate over a list in forward, reverse, or sorted order.
The additional methods are described by the FancyList
interface that we have provided to you. You should implement that
interface in a class FancyNodeList
that extends
NodeList
. So the relationships among the classes and the
interfaces have this rather typical form:
Therefore, your code should have this form:
public class FancyNodeList extends NodeList implements FancyList { ... } |
The append
method concatenates two lists. So if
a
is the list (1,2,3) and b
is the list
(4,5)
, then a.append(b)
is the list
(1,2,3,4,5)
. Similarly b.append(a)
is (4,5,1,2,3)
.
The method you will write is actually called
append_destructive
, to emphasize that it will change the
lists a
and b
. In other words,
a.append_destructive(b)
will return (1,2,3,4,5)
, but it
has a destructive side effect: once you have called it,
a
no longer refers to (1,2,3)
and
b
no longer refers to (4,5)
! In fact
a
and b
will not even be valid list
structures anymore; methods called on them will no longer work
properly.
A non-destructive version would leave a
and
b
alone, and create a new list
(1,2,3,4,5)
.
Hints:
Before implementing append_destructive
, you should
draw at least one example on a piece of paper. Draw two lists
a
and b
, just as in the figure above showing a
circular list. Then draw what you want a.append_destructive(b)
to
look like for your example. This will help you figure out how to
change the prev
and next
fields correctly.
You may want to write some explicit pseudocode.
Sometimes you may need to save a node's prev
or
next
in a temporary variable (for your subsequent use)
before you change the field.
Your code to manipulate pointers may get hairy enough that you
are likely to make an error. If this happens, then try to find a way
to make your code simpler and easier to check or think about. For
example, consider writing a private connect
method that
helps you hook up two DNode
s so that they point to each
other.
Always think about the boundary cases. As you write your code
or pseudocode, consider whether it will work correctly if
a
, b
or both are empty lists.
append_destructive
method should set
a.sentinel
and b.sentinel
to
null
before it returns. Here's why:
According to the FancyList
interface's
documentation, the user should not use either a
or
b
any longer after calling
a.append_destructive(b)
. If you set their sentinels to
null
, then a misguided user who subsequently tries to
call a method on a
or b
will probably get
slapped in the face by a NullPointerException
. (Is this
exception guaranteed to happen? Can you improve the design?)
When you destructively append two lists into one list, your
total number of sentinel nodes drops from 2 to 1. You want Java to be
able to reclaim your unused sentinels via "garbage collection."
Remember that Java can reclaim objects that cannot be reached from any
variables you have. By setting a.sentinel
and
b.sentinel
to null
, you ensure that the
original sentinels can no longer be reached from a
and
b
.
Notice that the textbook's NodeList.remove
does
something similar, for the same reasons. It invalidates the
removed node by setting its prev
and next
fields to null
.
Note: Since you are modifying a
and
b
to make them unusable, your return value must be a
different (and usable) FancyNodeList
object, created with
new
. Its sentinel should point to nodes that
a
or b
used to point to.
Answer the following in your Readme
file:
append_destructive
? Use big-Oh notation as usual.
append
?
append
more efficient? What would its
time and space requirements be?
The reverse
method reverses a list. So if
a
is (1,2,3) as before, then a.reverse()
is
(3,2,1).
Again, you will write a destructive version, so you should actually
call it reverse_destructive
to remind the user not to use
a
anymore after calling
a.reverse_destructive()
.
In class we discussed several ways to write such a method, but you
are required to do it by manipulating the prev
and
next
fields of the DNodes
in the list,
including sentinel
. That is in order to give you
practice with pointers. Draw your solution before implementing it,
and think about boundary cases!
Answer the following in your Readme
file:
reverse_destructive
? Use big-Oh notation as usual.
reverse_destructive
differently, by changing the nodes' element
fields
(via List.swapElements
) while leaving the prev
and next
alone. What would the worst-case time and space
requirements have been then?
reverse
method by making a copy of the list in reverse order. (An easy way to
do this is repeated calls to insertBefore
.) What
would
the worst-case time and space requirements be?
Before implementing iterator
and
iteratorReverse
methods of FancyList
, you
would be wise to review the Iterator
interface and the tree iterators that we
developed in class. Iterators are also discussed in section
5.2 of the textbook.
These methods of FancyList should return iterator objects.
Specifically, objects of type FancyListIterator
and
FancyListReverseIterator
, respectively. These are
subclasses of java.util.Iterator
that you will write. We
have given you skeletons of these classes. In particular, we suggest
that your iterator have the following instance fields:
protected Position p; // position of next element to return, or null if none protected FancyList fl; // have to point to the list too so we can call its methods /* Since the list field only holds a pointer to an existing list, the iterator only takes O(1) memory, not O(n) as you might think. */ |
iterator
and iteratorReverse
should be
fairly easy once you understand iterators. (They are also almost
identical to each other.) But be careful about the following:
An iterator should never modify the collection that it
iterates over! (Except for the remove
method, which you
don't have to implement.)
The iterator's next
method should return an actual
list element (an arbitrary Object
). It should not return
a DNode
or other Position
in the list. (If
you ever want the extra power of positions, don't do it through the
next
method. Instead you can give your iterator an extra
nextPosition
method, like
this.)
You are required to throw a
java.util.NoSuchElementException
if the next
method is called when the iterator is out of elements (i.e., when
hasNext
is false).
Be careful that your iterator's constructors and methods do not
throw other exceptions. In particular, you should not throw EmptyContainerException
(from calling List.first
or List.last
on an empty
list), and you should not throw BoundaryViolationException
(from using List.before
or List.after
to go past the
end of the list). Those exceptions are not part of the defined
behavior of an iterator, and anyone should be able to use your
iterator without running into them.
So you have two options in writing your iterator. One option is to
use try
-catch
blocks to catch those
exceptions when they arise and react appropriately. The other option
is to call the List
methods only when you are sure the
exceptions won't arise. For example, List.after(p)
only
throws an exception if p
is the last element of the list;
you can use List.isLast(p)
to check whether this is the
case
before you call List.after(p)
.
The final problem is the hardest and most interesting exercise you have seen so far in this class. Design an iterator that iterates over the list's elements in ascending order. The fact that this is possible demonstrates the flexibility and power of iterators.
The sort should be in the same order (and throw the same ClassCastException
)
as Java's built-in sort
method. However, unlike that method, you will not modify that
list, nor will you sort it all at once. Instead your iterator will
be able to return the elements one at a time, in increasing order.
Carefully read the javadoc comment for iteratorSorted
,
which explains that you must do a "stable sort":
/** Returns an iterator over the elements of the list in sorted * order. Elements are compared using the compareTo method * and the least one is returned first. Elements that compare * as equal should be returned in their original left-to-right * order (this is called a "stable sort"). See the assignment * for discussion and hints. * * @throws ClassCastException if the list contains elements that * cannot be compared to one another (e.g., strings and * integers, or objects that cannot be cast to Comparable). */
The method should return an iterator of class
FancyListSortedIterator
, which you will design and write.
You should probably store the same two instance fields as in your
previous iterators. What is different is how you advance the Position
p
. That is complicated!
Try to figure it out yourself first. If you get stuck, try this hint.
Your iterator should only take O(1) space. But how about time?
Answer the following in your Readme
file:
You could output a list in sorted order by calling the
next
method until hasNext()
is false. In
fact, that is how the test class below will print lists in sorted
order and will create a new sorted version of a list.
FancyListSortedIterator
's next
method?
next
repeatedly?
Remark: The sorting method we have just discussed is called selection sort, because it repeatedly scans the list to select the next biggest element. Many people use this method to sort their cards during a game of poker. One advantage of using an iterator is that the user could quit after getting the 10 smallest elements, which saves time if that's all he or she needs.
Finally, test your FancyNodeList
implementation by
running the NumberedString
test class that was provided
to you. This test class exercises FancyNodeList
through the
FancyList
interface.
NumberedString
by filling in a couple of methods
(listify
and iterFromList
).
The NumberedString.main
method will create two FancyLists
of integer/string pairs, and will try various methods on them.
For example, running java NumberedString yada yada yada blah
blah yada
will start by constructing this FancyList:
0 / yada 1 / yada 2 / yada 3 / blah 4 / blah 5 / yadaas well as the following "standard" example
FancyList
:
0 / hello 1 / i love you 2 / hello 3 / goodbye
The compareTo
method on integer/string
is
defined so that 0 / hello
and 2 / hello
are
considered to be equal. This lets you check that your
sortedIterator
really does do a stable sort and
preserves their original order. In particular,
iteratorSorted
on the standard example above should yield
the order
3 / goodbye 0 / hello 2 / hello 1 / i love you
which keeps 0 / hello
ahead of 2 / hello
.
But if you first destructively reverse the standard example, so that
2 / hello
precedes 0 / hello
, then
iteratorSorted
should preserve that order too, yielding
3 / goodbye 2 / hello 0 / hello 1 / i love you
Here is the correct result of a sample run. You may want to check that you get the same answer.
$ java NumberedString yada yada yada blah blah yada --- example --- 0 / hello 1 / i love you 2 / hello 3 / goodbye --- example [in reverse order] --- 3 / goodbye 2 / hello 1 / i love you 0 / hello --- example [in sorted order] --- 3 / goodbye 0 / hello 2 / hello 1 / i love you ============================================================ --- sort(example) --- 3 / goodbye 0 / hello 2 / hello 1 / i love you --- sort(example) [in reverse order] --- 1 / i love you 2 / hello 0 / hello 3 / goodbye --- sort(example) [in sorted order] --- 3 / goodbye 0 / hello 2 / hello 1 / i love you ============================================================ --- args --- 0 / yada 1 / yada 2 / yada 3 / blah 4 / blah 5 / yada --- args [in reverse order] --- 5 / yada 4 / blah 3 / blah 2 / yada 1 / yada 0 / yada --- args [in sorted order] --- 3 / blah 4 / blah 0 / yada 1 / yada 2 / yada 5 / yada ============================================================ --- reverse(args) --- 5 / yada 4 / blah 3 / blah 2 / yada 1 / yada 0 / yada --- reverse(args) [in reverse order] --- 0 / yada 1 / yada 2 / yada 3 / blah 4 / blah 5 / yada --- reverse(args) [in sorted order] --- 4 / blah 3 / blah 5 / yada 2 / yada 1 / yada 0 / yada ============================================================ --- reverse(args) + example + args --- 5 / yada 4 / blah 3 / blah 2 / yada 1 / yada 0 / yada 0 / hello 1 / i love you 2 / hello 3 / goodbye 0 / yada 1 / yada 2 / yada 3 / blah 4 / blah 5 / yada --- reverse(args) + example + args [in reverse order] --- 5 / yada 4 / blah 3 / blah 2 / yada 1 / yada 0 / yada 3 / goodbye 2 / hello 1 / i love you 0 / hello 0 / yada 1 / yada 2 / yada 3 / blah 4 / blah 5 / yada --- reverse(args) + example + args [in sorted order] --- 4 / blah 3 / blah 3 / blah 4 / blah 3 / goodbye 0 / hello 2 / hello 1 / i love you 5 / yada 2 / yada 1 / yada 0 / yada 0 / yada 1 / yada 2 / yada 5 / yada ============================================================ |
Note that the NumberedString
test class only tests the
methods specific to FancyList
. It does not
necessarily use all of your basic NodeList
methods. You
are responsible for figuring out how to test those yourself and
finding any bugs before the graders do!
The niftiness of iterators is illustrated by the
NumberedString.printIter
and
NumberedString.listFromIter
methods, which can take any
kind of iterator as an argument. Because of dynamic method lookup, they
don't have to know what the particular iterator's next
method does -- they can just call that method.
Giving an iterator to a method such as printIter
is
very much like giving it a singly-linked list of elements. However,
an iterator is a "virtual" list that takes only O(1) space. Its
next
method will simply construct and return successive
elements as needed, without storing them all in memory. This is most
dramatic in the case of your iteratorSorted
, where there
is never an explicit sorted list in memory. As another example, you
could easily write an iterator to return the sequence of numbers 1, 3,
5, 7, ... and so on forever. printIter
would happily
keep printing those numbers until you turned off the computer, just as
if it were traversing an infinitely long list, but of course no such list
has been built in the computer's memory.
For extra credit, implement and test the remove
method
for each iterator. (That is, actually remove something rather than
throwing an UnsupportedOperationException
.) Note that
remove
is a standard part of Java's Iterator
interface; read the documentation
for how it is supposed to work.
This is not a hard problem, since you already have a
List.remove
method. But make sure your implementations
work exactly as the interface requires! Calling remove
should not affect the sequence of elements that the
next
method will return in future. It should only remove
the element that was just returned (throwing
IllegalStateException
if there's no such element or it
was previously removed).
Here is a different extra credit problem. You may do either or both problems if you like.
Write a class MergedIterator
that implements Iterator.
The constructor MergedIterator(Iterator iter1, Iterator
iter2)
should produce an iterator that returns all the
elements of both iter1
and iter2
. If
iter1
and iter2
both produce elements in
sorted order, then the merged iterator should iterate through all
the elements of both in sorted order.
To accomplish this, the merged iterator's next()
method should return the next element of either iter1
or
iter2
, whichever is smaller according to
compareTo
. Ties should be broken in favor of
iter1
.
To test your class, add the following code to the end of
NumberedString.main
. Note that this code does not use
any destructive operations. Instead of destructively appending two
lists and creating a sorted iterator over the result, it creates
a sorted iterator over each list and then merges them.
System.out.println("*** Non-destructive merge of two sorted lists ***"); fl = listify(args); // back to the original list printIter("args", fl.iterator()); printIter("example", example.iterator()); printIter("args [in sorted order]", fl.iteratorSorted()); printIter("example [in sorted order]", example.iteratorSorted()); printIter("args + example [in sorted order]", MergedIterator(fl.iteratorSorted(), example.iteratorSorted())); |
Jason Eisner - jason@cs.jhu.edu
- $Date: 2004/02/18 07:20:18 $