Solutions to Homework 5
R-6.14
[here change "sequence" to "node list", and use only the operations from the
Node List ADT (also called "List ADT" or "Position List ADT")]
Let S be the node list. Using the List ADT we can move position p to front
by doing the following:
E e = S.remove(p);
S.addFirst(e);
Or, even faster:
S.addFirst(S.remove(p));
Even though you were asked to use just the List ADT methods, we accepted
solutions which described how to perform the move-to-form from scratch, i.e.
without using the already-programmed List ADT methods, and assuming the
List ADT is implemented as a double-linked list of DNodes (see pages 239-241).
Then the code could look like this:
p.getPrev().setNext(p.getNext());
p.getNext().setPrev(p.getPrev());
header.getNext().setPrev(p);
p.setNext(header.getNext());
header.setNext(p);
p.setPrev(header);
R-6.16
[give a pseudocode for this iterator]
Some students just gave the procedure that returns a list composed of every
other element of the initial list, or prints out every other element of the list,
but that's not what an Iterator is. See section 6.3. So here is an example
of an implementation of such iterator (compare to the code for an iterator
of elements in a list on page 245):
public class NewIterator implements Iterator {
PositionList L;
Position p;
public NewIterator(PositionList L) {
this.L = L;
if L.isEmpty()
p = null
else p = L.first();
}
public hasNext() { return (p != null); }
public E next() throws NoNextElementException {
if (p == null) throw new NoNextElementException("...");
E elementToReturn = p.element();
p = L.next(p);
if (p != null)
p = L.next(p);
return elementToReturn;
}
R-6.19,
One student noted that the question can be solved only if the initial access
count for every element in list L is zero (or, more generally, if these
access counts are all the same). That was indeed the intention of this
question.
Let "l_i" refer to an element that sits at index i in list L at the
beginning. Let n = L.size(). Assuming that all elements in L have
equal initial counts, the following access pattern will have the effect of
reversing the list if the list is maintained as ordered by decreasing
access counts:
for i = 1 to n
for j = i to n
access element l_i in the list (wherever it currently is)
For example, if L = {A,B,C,D}, this loop encodes the following pattern
of accesses to these elements: A,B,C,D,B,C,D,C,D,D
In general, element l_i will be accessed i times in this access pattern,
and therefore the resulting list will look like this:
{l_n, l_{n-1}, ... , l_3, l_2, l_1}
Note that the number of accesses is O(n^2).
R-6.20,
Let "l_i" refer to an element that sits at index i in list L at the
beginning. Let n = L.size(). The following access pattern will have
the effect of reversing the list if the list is maintained according
to the move-to-front heuristic:
for i = 1 to n
access l_i in the list (wherever it currently is)
Note that the last element accessed is l_n, therefore it'll be at the
front of the list now. The element accessed before that is l_{n-1},
so it'll be second. Etc.
Note that the number of accesses is n.
C-6.11,
The list L can be implemented like this: Whenever element l in L is
accessed, two things happen: (1) Element l is moved to the front of
the list, and (2) The last element of the list is removed from the list,
unless this last element is equal to element l (in which case no
element is removed from the list).
Assuming the list is implemented with linked list of DNodes, the
move-to-front takes O(1) time and so does removal of the last node.
Note that at any time, the list L contains then only the elements which have
been accessed at least once among the past n = L.size() accesses.
Note also, that this is a somewhat strange list, because unless you access
the last element, the list shrinks.
Some students said that you can maintain an additional queue L', on which
you enqueue every element that is accessed, and you dequeue elements from L'
as long as L'.size() > n. This has the effect that list L' contains all
the elements in L that were accessed in the past n accesses. We accepted
such answer, even though the question asked you to make the *original* list
L have this property.
C-6.12,
This was a little tricky. The following access pattern works. Assume
again that "l_i" refer to an element that sits at index i in list L at the
beginning and that n = L.size():
do the following n times:
for i = 1 to n
access element l_i (wherever it currently resides in L)
Note that this pattern consists of n^2 accesses to L.
Note also that whenever element l_i is accessed, it is actually still at
position i in the list, and therefore it takes O(i) time to access this
element (and then O(1) time to move him to the front). Therefore the
total time taken by the "for i = 1 to n" loop is 1+2+3+...+n = \Omega(n^2),
and therefore the total time taken by this access pattern to process is
\Omega(n^3).
Some students gave this as an answer:
for i = 1 to n
do the following n times:
access element l_i (wherever it currently resides in L)
This is not correct because the first access to l_i takes i steps.
All the subsequent accesses to l_i will take 1 step, and hence the
total takes only O(n^2) time.
R-8.7,
The answer depends on how exactly you program Insertion Sort,
but if the code is as on page 333, then the worst case is
when the original list is actually in the *sorted* order.
This is because every time you insert a new element l_i you
have to traverse the already-sorted list, which takes time i,
so the resulting time is 1+2+3+...+n = O(n^2).
You could implement Insertion Sort so that the algorithm
traverses the already-sorted list, searching for the place
in which to insert the new element l_i, from the end to the
beginning, in which case the worst case is when the original
list is in the reverse-sorted order.
C-8.4,
Here you had to implement a Stack using a Priority Queue PQ
and a constant integer c. To do that, you could start up
with c equal to any value (e.g. 0), and an empty PQ,
and implement the stack ADT as follows:
Push(e) { PQ.insert(c,e); c--; }
Pop() { return PQ.removeMin() }
Top() { return PQ.min(); }
size() { return PQ.size(); }
isEmpty() { return PQ.isEmpty(); }
You could also increment c back when popping.
One student observed that you dont even need the constant c:
You could use just the negative of the current system time
instead of it, i.e.
Push(e) { PQ.insert(-System.getTime(),e); }
C-8.5
Here you had to implement a Queue using a Priority Queue PQ
and a constant integer c. Again, you start up with c equal
to any value (e.g. 0), and an empty PQ, and implement the
stack ADT as follows:
Push(e) { PQ.insert(c,e); c++; }
Pop() { return PQ.removeMin() }
Top() { return PQ.min(); }
size() { return PQ.size(); }
isEmpty() { return PQ.isEmpty(); }
Again, you could also decrement c back when popping,
and you could do away with c by using the
current system time instead, i.e.
Push(e) { PQ.insert(System.getTime(),e); }