Saint Louis University |
Computer Science A220/P126
|
Dept. of Math & Computer Science |
Topic: Amortization, Vectors and Lists
Related Reading: Ch. 5.1-5.2
Due:
Wednesday, 23 March 2005, 1:10pm
Please make sure you adhere to the policies on academic integrity.
If you wish, you can play with the singly-linked list demonstration Applet.
In the solutions of an earlier homework, we gave a method for implementing a Queue with the use of two Stack data structures. Although this method was relatively simple to understand, it was not very efficient. This is true both in terms of worst-case analysis of the methods and even amortized analysis.
So in a moment, we are going to design a new implementation for a Queue, which again is based on using two Stack's. Although the worst-case time per operation is still not O(1) (as it would be when we implement a Queue directly with an array or a linked list), it turns out that we can show that each operation requires only O(1) amortized time.
Your job is to prove this amortized bound using the accounting method of the text. Specifically, you should imagine yourself in the role of the Queue implementor. The person who has implemented the Stack has demanded that you pay $1 cyber-dollar every time your code calls one of the Stack methods.
As a first step of your solution, you should make a table which lists, for each of your five Queue methods, the number of cyber-dollars you will have to pay to the Stack implementor when your routine is called in the "worst" case and in a "standard" case. For example, the Queue.size() method would always have to pay $2 cyber-dollars, as it makes two distinct calls to Stack.size(). The Queue.isEmpty() routine pays $2 cyber-dollars in the worst case, although when S1 is non-empty, it only pays $1 cyber-dollar, as the expression already evaluates to false. Your answers in this table may also depend on the number of items which are currently in Stack S1 or Stack S2 at the time (use notation |S1| or |S2|), but you should give exact bounds, that is do not use big-Oh notation here.
Here is a sample such table:
Queue operation | Paid to Stack implementor | |
---|---|---|
"worst case" | "standard case" | |
Queue.size() | 2 | 2 |
Queue.isEmpty() | 2 (when S1 is empty) |
1 |
Queue.enqueue() | ||
Queue.dequeue() | ||
Queue.front() |
As the second step of your solution, create a table which serves as a "price list" which you will use to charge users who call your Queue routines. You may feel free to pick a different price for each of your five routines, however each routine must have a fixed price. That is the price you charge can be any constant you wish, but it must not depend on the size of the queue or the stacks at the time.
As an example, you may want to charge $2 cyber-dollars for your Queue.size() method so that you will collect exactly enough money to pay for the two Stack methods you call. You may also feel free to overcharge your customer, so that you can build up a surplus of money to use on a rainy day.
Here is a sample such table:
Queue operation | Price List (paid to us) |
---|---|
Queue.size() | 2 |
Queue.isEmpty() | |
Queue.enqueue() | |
Queue.dequeue() | |
Queue.front() |
Finally, give a detailed argument explaining that the Queue implementor will never go broke. That is, the Queue implementation will always collect enough money from the price list you developed, so that it will be able to pay for all Stack methods that get called (using either the money collected or previous savings).
The remaining text gives the C++ code for the new approach. Whereas the original solution used stack S2 only temporarily to hold objects, this new method uses both stacks S1 and S2 for storing objects. Although you are not required to argue the correctness of this new method, you will certainly need to understand how and why it works to complete this assignment.
class QueueSolution : Queue {
private:
// Do not use any data members other than the following two stacks
StackImplementation<Object> S1;
StackImplementation<Object> S2;
void transferItems() throw(QueueEmptyException) {
// assuming S2 is empty and S1 is non-empty
// transfer all items from S1 onto S2
int s = S1.size();
if (s==0)
throw QueueEmptyException("Queue is empty.");
for (int i=0; i<s; i++) {
S2.push(S1.pop());
}
}
public:
bool isEmpty() const { return S1.isEmpty() && S2.isEmpty(); }
int size() const { return S1.size()+S2.size(); }
void enqueue(const Object& obj) { S1.push(obj); }
Object dequeue() throw(QueueEmptyException) {
if (S2.isEmpty())
transferItems();
return(S2.pop());
}
Object& front() throw(QueueEmptyException) {
if (S2.isEmpty())
transferItems();
return(S2.top());
}
}
Design algorithms for reversing a List using only a restricted subset of the functions, as indicated below. Each algorithm should rearrange the elements of the List. Returing a new List is not allowed, although other lists may be used for auxiliary storage.
Creativity Exercise C-5.4 of the text.
(Hint available through
Hint Server
on textbook's website).
Note: Though you will need to understand Exercise C-5.3, you do not need to writeup that solution.