Saint Louis University |
Computer Science 180
|
Dept. of Math & Computer Science |
Topic: Amortizations, Lists
Related Reading: Ch. 6
Due:
Wednesday, 21 March 2012, 10:00am
For this assignment, you must work individually.
Please make sure you adhere to the policies on academic integrity in this regard.
In the solutions for 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 and amortized analysis.
Here, we present an alternative solution. 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.
template <typename Object> class queue { public: int size() const { return S1.size() + S2.size(); } bool empty() const { return S1.empty() && S2.empty(); } void push(const Object& item) { S1.push(obj); } void pop() { if (S2.empty()) transferItems(); S2.pop(); } Object& front() { if (S2.empty()) transferItems(); return(S2.top()); } private: // Do not use any data members other than the following two stacks std::stack<Object> S1; std::stack<Object> S2; void transferItems() { int s = S1.size(); for (int i=0; i<s; i++) { S2.push(S1.top()); S1.pop(); } } };
For the new implementation, the worst-case time per operation is not guaranteed to be O(1). However, it can be shown that each operation requires only O(1) amortized time. Your job is to prove this amortized bound using the accounting method described in Ch. 6.1.3 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, make a table that lists the number of cyber-dollars you will have to pay to the stack implementor when each of your five queue methods is called. In your accounting, separate out the "worst" case and "standard" case analyses. 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::empty() routine pays $2 cyber-dollars in the worst case, although when S1 is non-empty it only pays $1 cyber-dollar (as the && expression short circuits). Your answers in this table may be parameterized by the number of items in stack S1 or stack S2 at the time (use notation |S1| or |S2|). Give exact bounds; 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::empty() | 2 (when S1 is empty) |
1 |
queue::push() | ||
queue::pop() | ||
queue::front() |
As the second step of your solution, create a table which serves as a "price list" of charges to be paid by 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 might charge $2 cyber-dollars for your queue::size() method so that you collect exactly enough money to pay for the two stack methods you call. You may also overcharge your customer at times, so that you build a surplus of money to use at a later time.
Here is a sample such table:
queue operation | Price List (paid to us) |
---|---|
queue::size() | 2 |
queue::empty() | |
queue::push() | |
queue::pop() | |
queue::front() |
Finally, give a detailed argument showing that the queue implementor will never go broke. That is, the queue implementor will always collect enough money from the price list you developed to be able to pay for all stack methods that get called. Although you might gain some intuition by looking at some example usages, your argument should provde the claim for any usage pattern.
In earlier class notes, we provided our own implementation of a NodeList class that mimics the public interface of the std::list. However, the standard list has additional behaviors that were not in our implementation. One of those behaviors has a singature akin to the following.
/** Splice one list into another. * All elements of other list are removed from that list and * inserted into this list immediately before the given position. * * A call runs in constant time and all iterators remain valid, * including iterators that point to elements of other. * * @param position must be an iterator into this list * @param other must be a distinct list (i.e., &other != this) */ void splice(iterator position, NodeList& other);
Give a valid implementation of a splice method in the context of our original list implementation.
Another method supported by the std::list class is the following.
/** Reverse the order of elements in the list. All iterators * remain valid and continue to point to the same elements. * This function is linear time. */ void reverse();
Give a valid implementation of the reverse method in the context of our original list implementation. Please note that to maintain the validity of all existing iterators, you must not create or destroy any nodes nor change the element of a node. The only way to successfully implement this behavior is by relinking the existing nodes into the desired order.
Creativity Exercise C-6.8 of the text.