Stacks
Introduction
- Key questions for this section:
- What are stacks? What operations do they have?
- When would you use a stack? How could you use a stack for those cases?
One of the simplest and most commonly-used data structures is the Stack. In a stack, data is added and removed from one end only (typically called the top). Logically, the top item is the only one that can even be seen. Real-life examples of stacks can include:
- a stack of trays at a cafeteria
- post-it notes (although you don't often put post-its back on the stack)
- carts at a store
The fundamental operations of a stack are:
- Push an item onto the top of the stack
- Pop an item from the top of the stack
- Peek at the top item without disturbing it
See the StackInterface.java.
A Stack organizes data by Last In First Out, or LIFO (or FILO - First In Last Out). This access, although simple, is useful for a variety of problems. Let's look at a few applications before we discuss the implementation:
- Run-time Stack for method calls (especially recursive calls)
- We will see this when we discuss recursion in a couple of weeks. When a method is called, its activation record is pushed onto the run-time stack. When it is finished, its activation record is popped from the run-time stack. This is why exceptions that crash your program have a "call stack trace". It's a trace of the calls that were placed on the run-time stack.
- Testing for matching parenthesis:
- (()())() - match
- ((((())))) - match
- ((()) - don't match (not enough right parens)
- ())( - don't match (parens out of order, or too many right parens)
- ([)] - don't match (wrong paren type)
How can we code this using a Stack?
- Let's solve this problem together. What do we need?
- When do we push, when do we pop and how do we test?
Take a look at how a stack can be used by looking at BalanceChecker.java and BalanceCheckerDriver.java
- Stacks can also be used to evaluate post-fix expressions, where operators follow operands. This is useful since no parentheses are needed. For example:
20 10 6 - 5 4 * + 14 - /
The idea behind post-fix notation is that each operator seen is used on the two most recently seen (or generated) operands. So for example, the "-" is used on 10 and 6. So what do we do with operands before seeing an operator, or after we evaluate an intermediate result?
- We can also use a stack to convert from infix notation to postfix notation. For example:
(a + b) * (c - d * e)
becomes:
a b + c d e * - *
This process is somewhat more complicated, since we need to be able to handle operands, operators (of different precedence) and possibly parentheses. We will also need a StringBuilder (or StringBuffer) to store the result. This process is discussed in detail in Section 5.11 - 5.16 of the text. Read over it carefully - it is explained quite well in the book.
Implementation
A Stack can easily be implemented using either an array or a linked list:
In the Java Collections Framework, the Stack class extends the Vector class, defining the stack operations appropriately. Unfortunately, all Vector operations are still available, allowing the user to violate stack restrictions. Later, we will see that the Queue ADT in Java is (more appropriately) an interface. The difference between the stack implementation and the queue implementation in Java is due to history and backward compatibility. What do you think the tradeoffs are between Java's stack implementation and their queue implementation.