Lab 10: Stacks

This lab will consist of two parts. The first part focuses on implementing a node-based stack. The second part uses your stack and the queue from Lab 9 to convert a recusive method implementation to an iterative implemenation.

To begin this lab, you should also review your notes and lecture material for information about linked lists and stacks.

Part I

  1. Create a package lab.util or re-use the package from Lab 9.
  2. Add the classes Node, NodeIterator, and Stack to the package lab.util. You will already have the node classes if you are re-using your package from Lab 9. APIs can be found here.
  3. Complete the implementation of the Stack class:
    1. Your stack implementation must use a node-based linked sequence to represent the stack. The sequence is very similar to a singly linked list but does not need to provide all of the functionality of a linked list. You must use the provided Node class to implement the linked sequence. All of the required fields are already provided for you in the starter code.
    2. The constructor should create an empty stack. An empty stack has no top node, and its size is zero.
    3. The push method should add a new element to the top of the stack, increasing the size of stack by one. It does so by adding a new node to the beginning of the sequence of nodes.
    4. The pop method should remove and return the element at the top of the stack, decreasing the size of the stack by one. It does so by removing the node at the front of the sequence of nodes.
    5. The peek method should return the element at the top of the stack without removing it. It does so by returning the element stored in the node at the front of the sequence of nodes.
    6. The size method should return the size of the stack.
    7. The isEmpty method should return true if the stack is empty and false otherwise.
    8. The equals method should return true if both stacks contain the same sequence of elements (i.e., both stacks have the same size, and the elements in this stack are equal to the elements in the other stack when compared in iteration order). You should use the NodeIterator class to iterate over the elements of both stacks.
  4. Test your stack using the JUnit test StackTest.

Part II

Consider the following recursive implementation for computing the Fibonacci number F(n):

01  public class Fibonacci {
02    private static Map<Integer, BigInteger> cache = new HashMap<Integer, BigInteger>();
03
04    public static BigInteger fib(int n) {
05      BigInteger sum = null;
06      if (n == 0) {
07        sum = BigInteger.ZERO;
08      }
09      else if (n == 1) {
10        sum = BigInteger.ONE;
11      }
12      else if (Fibonacci.cache.containsKey(n)) {
13        sum = cache.get(n); 
14      }
15      else {
16        BigInteger fMinus1 = Fibonacci.fib(n - 1);
17        BigInteger fMinus2 = Fibonacci.fib(n - 2);
18        sum = fMinus1.add(fMinus2);
19        Fibonacci.cache.put(n, sum);
20      }
21      return sum;
22    }
23  }

The implementation above uses a technique called memoization; it stores the value of F(n) in the map named cache so that the value of F(n) can be reused (instead of recursively recomputed) when some larger Fibonacci number is computed. For example, invoking fib(5) causes the values of F(5), F(4), F(3), and F(2) to be stored in cache. If fib(6) is then invoked, the values of F(5) and F(4) can be retrieved from the cache to efficiently compute the value of F(6). It is possible to show that the method above has complexity O(n) in contrast to the naive implementation that does not use memoization which has complexity O(2n).

One problem with the above implementation, and with recursive implementations in general, is that there is a limit to the amount of memory that can be used to store the active method invocations, and this limit is much smaller than the total amount of memory available. For example, on my computer I can successfully invoke fib(5411), but invoking fib(5412) causes a StackOverflowError exception to be thrown. To understand why a StackOverflowError exception is thrown, consider the following illustration of what occurs when fib(3) is invoked:

  1. An invocation of fib begins execution to compute F(3). A block of memory called a frame that contains storage for the method parameters, references to static variables, local variables, and the return value is allocated from a part of memory called the stack (not to be confused with the data structure you implemented in Part I that is also called a stack). Because the frame is allocated from the stack, the frame is often called a stack frame.
  2. To compute F(3), an invocation of fib begins execution to compute F(2). Another block of memory is allocated for the stack frame for F(2).
  3. To compute F(2), an invocation of fib begins execution to compute F(1). Another block of memory is allocated for the stack frame for F(1).
  4. F(1) is a base case so the value of 1 can be returned to the calling method F(2) and the memory associated with the stack frame can be freed from use (or popped from the top of the stack).
  5. The method invocation that is computing F(2) (whose frame is now on the top of the stack) receives the return value of F(1), and can resume execution.
  6. To continue computing F(2), an invocation of fib begins execution to compute F(0). Another block of memory is allocated for the stack frame for F(0).
  7. F(0) is a base case so the value of 0 can be returned to the calling method F(2) and the memory associated with the stack frame can be freed from use (or popped from the top of the stack).
  8. The method invocation that is computing F(2) (whose frame is now on the top of the stack) receives the return value of F(0), and can resume execution. It computes the sum F(1) + F(0) and caches the result.
  9. The method invocation that is computing F(2) is finished execution so the value of 1 can be returned to the calling method F(3) and the memory associated with the stack frame can be freed from use (or popped from the top of the stack).
  10. The method invocation that is computing F(3) (whose frame is now on the top of the stack) receives the return value of F(2), and can resume execution.
  11. To continue computing F(3), an invocation of fib begins execution to compute F(1). Another block of memory is allocated for the stack frame for F(1).
  12. F(1) is a base case so the value of 1 can be returned to the calling method F(3) and the memory associated with the stack frame can be freed from use (or popped from the top of the stack).
  13. The method invocation that is computing F(3) (whose frame is now on the top of the stack) receives the return value of F(1), and can resume execution. It computes the sum F(2) + F(1) and caches the result.
  14. The method invocation that is computing F(3) is finished execution so the value of 2 can be returned to the calling method and the memory associated with the stack frame can be freed from use (or popped from the top of the stack).

Notice that in the picture above, the stack of frames grows because the bottom-most frame (which is trying to compute F(3)) must wait for all of the frames above it to finish executing (to compute F(2) and then F(1)) before it can finish executing. This can become problematic because each stack frame requires enough memory to store:

When we try to invoke fib(5412) we end up with a stack of 5412 frames, each requiring memory as described above. This turns out to exceed the availabe stack space on my JVM. Stack memory is special because it can be allocated and deallocated very quickly; this is important for efficient method execution. However, this speed is obtained by restricting the amount of stack memory.

A larger pool of memory called the heap is available to your program. Heap memory is used to store objects (i.e., heap memory is allocated whenever the new operator is used). Allocating and deallocating heap memory is slower than for stack memory, but the size of the heap is much larger. If we can convert our recursive implementation into an iterative implementation where we replace stack frames with objects, then we might be able to compute values such as fib(5412) and larger. To do so, we have to mimic how the JVM mimics the stack by creating and managing our own stack of frames.

Implement the class FibonacciStackFrame:

Submission instructions