Saint Louis University |
Computer Science 2100
|
Dept. of Math & Computer Science |
Topic: Amortizations, Linked Lists
Related Reading: Ch. 5.3 and 6.1
Due:
Friday, 23 October 2015, 10:00am
For this assignment, you must work individually.
Please make sure you adhere to the policies on academic integrity in this regard.
When using a doubly linked list to implement the Deque ADT, we relied on the use of a header and trailer sentinel nodes, claiming that this made the rest of our implementation cleaner by removing the need for special cases.
In this problem, we explore a potential implementation of an AltLinkedDeque class without the use of any sentinels. Instead, the class will explicitly maintain two Node* pointers, head and tail, which should reference the first and last nodes of the list, respectively, and which should be NULL when the list does not have any nodes. While maintaining such a convention, you are to:
Provide a working implementation of the push_front method.
Provide a working implementation of the pop_back method.
As a starting point, we provide the nearly complete AltLinkedDeque class as a PDF handout. Your solution should be consistent with this code, and you should make sure to consider special cases, such as when working with an empty list.
As a "homework" you are not required to formally implement the code on a computer. However, if you prefer to do so, in order to compile and test your solution, our starting files are available as AltLinkedDeque.h and AltLinkedDeque.tcc. We'll even help you out by providing the following simple test driver: Test_AltLinkedDeque.cpp.
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.