Homework Solution

Amortization, Vectors and Lists

Problems to be Submitted (20 points)

  1. (10 points)

    Queue operation Paid to Stack implementor
    (worst-case cost)
    Price List
    (paid to us)
    Queue.size() 2 2
    Queue.empty() 2 2
    Queue.push() 1 4
    transfer loop
    triggered
    transfer loop
    NOT triggered
    Queue.pop() 3 + 3|S1| 2 3
    Queue.front() 3 + 3|S1| 2 3

    Our claim is that we can keep a bank account which is guaranteed to hold $3 in reserve for each element currently stored in stack S1 of our structure.

    The operations size() and isEmpty() do not alter our structure, and to pay for these operations, we charge the customer a price exactly equal to our worst-case expenditures. An examination of the source code shows that the only way in which items can be added onto stack S1 is from within the push() method. We have set a price of $4 for this method, even though our expenditure is always $1. In this way, we are sure to have an extra $3 per item on S1 which we can keep in the bank.

    We analyze pop() and front() by case analysis. When stack S2 is non-empty, our expenditure is exactly $2, and this can be funded from the $3 price we charge our customer (the profit can be thrown away here).

    In the case when S2 is empty, we invoke transferItems. This includes one initial call to S1.size() and the for loop of the transferItems code, which moves all current items from S1 onto S2. This loop has an expenditure of exactly $(3|S1|), as the loop body involves 3 stack methods per iteration. The $3 savings-per-item-of-S1 can pay for this loop, with the $3 collected directly from the customer to pay for the overhead of the call to size and the calls to S2.empty and S2.pop (resp. S2.top) from within the call for queue::pop (resp. queue::top). Although we spend all of our savings, all items have been removed from S1 during this process, so we can still maintain our guarantee of having savings of $3 per item on S1.

  2. (5 points)

    template <typename Object>
    void splice(iterator position, NodeList<Object>& other) {
      if (other.n > 0) {                    // following body would be disaster if other is empty
        Node* successor = position.node;
        Node* predecessor = successor->prev;
        predecessor->next = other.header->next;
        other.header->next->prev = predecessor;
        successor->prev = other.trailer->prev;
        other.trailer->prev->next = successor;
        other.header->next = other.trailer;
        other.trailer->prev = other.header;      
        n += other.n;
        other.n = 0;
      }
    }
    
    

  3. (5 points)

    There are several strategies that can be used to enact the reversal. In the end, every interior node should end up having its next pointer and prev pointers swapped (what used to be after it will end up before it and vice versa). Here is an implementation based on that idea.

    void reverse() {
      if (n > 0) {          // following body would cause problem if empty list
        Node* walk = header->next;
        while (walk != trailer) {
           Node* advance = walk->next;
           walk->next = walk->prev;
           walk->prev = advance;
           walk = advance;
        }
        walk = header->next;                 // this should become the last node
        header->next = trailer->prev;        // old last node is new first node
        trailer->prev = walk;
      }
    }

    If we import <algorithm> library, we can take advantage of the swap function to make the above implementation even easer.

    void reverse() {
      if (n > 0) {          // following body would cause problem if empty list
        Node* walk = header->next;
        while (walk != trailer) {
           swap(walk->next, walk->prev);
           walk = walk->prev;                 // this used to be "next"
        }
        swap(header->next, trailer->prev);
      }
    }

    A different approach is to splice elements, one at a time, in reverse order starting at the back. Implementation might appear as

    void reverse() {
      Node* marker = trailer;
      while (header->next != marker) {
        // first, cut "header->next" out of the list      
        Node* moving = header->next;
        header->next = moving->next;
        header->next->->prev = header;
        // now, re-insert this node just before "there"
        moving->prev = there->prev;
        there->prev->next = moving;
        moving->next = there;
        there->prev = moving;
        there = moving;     // next node should go before this one
      }
    }


Extra Credit

  1. (2 points)

    There is not a prefect solution to detect this problem. The best strategy would require that each Node have an additional field which is a pointer to a NodeList, in particular the pointer to the list containing that node. That would make it easy to detect which list it belongs to. But if we use such a strategy, we would no longer be able to support the constant-time splice routine, for example, because many nodes are switching from one list to another in that operation, and updating each node's list field would require linear time.


Last modified: Thursday, 22 March 2012