Think of a stack of plates, or boxes, or almost anything else. In the real world, you add to a stack by putting something new on top. The only (safe) way to remove something from a stack is remove the top item. Stacks are governed by a LIFO (Last In, First Out) strategy.
In programming, a stack captures these same LIFO ideas. We push an item (add it to the top of a stack) or pop an item (remove it from the top of the stack). In addition, we generally limit ourselves to only looking at the top element.
Why limit ourselves like this?
high recognition factor — people reading your code can more easily recognize what you are doing.
improved choices for the implementing data structure — as a general rule, the more elaborate the ADT interface, the fewer options you will have for implementation.
In Java, a Stack
is a kind of List
. That means that it has all of the oeprations we have seen in other List
s.
Nonetheless, it is generally assumed that, when yu declare something as a Stack
, you will limit yourself to the small handful of “stackish” functions.
skinparam style strictuml
scale 1.0
hide empty members
interface List<E> << interface >>
interface Collection<E> << interface >>
Collection <|.. List
interface Iterable<T> << interface >>
Iterable <|.. Collection
class Stack<E> {
isEmpty(): boolean;
size(): int;
peek(): E;
pop() E;
push(E);
}
List <|.. Stack
You can find the entire interface summarized here
public Stack() {...}
This creates an empty stack.
public int size() {...}
public boolean isEmpty() {...}
These should be familiar by now.
Stack<Integer> s;
assertThat(s.size(), is(0));
assertThat(s.isEmpty(), is(true));
public E push(E item) { ... }
public E pop() { ... }
We can push an item onto the top of a stack. The function returns the item just pushed.
E.g.,
Stack<Integer> s;
s.push(42);
assertThat(s.size(), is(1));
assertThat(s.isEmpty(), is(false));
s.push(13);
assertThat(s.size(), is(2));
assertThat(s.isEmpty(), is(false));
We remove items from a stack by popping it. The function returns the value just removed.
Stack<Integer> s;
s.push(42);
s.push(13);
int removed = s.pop();
assertThat(s.size(), is(1));
assertThat(removed, is(13));
public E peek() { ... }
peek
lets us look at the top element on the stack without removing it.
Stack<Integer> s;
s.push(42);
assertThat(s.peek(), is(42));
s.push(13);
assertThat(s.peek(), is(13));
s.pop();
assertThat(s.peek(), is(42));
Stacks can be easily implemented using array/vector-like data structures or via linked lists. You can see both possibilities below.
Actually, implementation of a stack is pretty trivial using either the ArrayList<E>
or LinkedList<E>
types as an underlying implementation.
push
a value e
onto the stack, do an add(e)
onto the underlying list.pop
from the stack,do a removeLast
on the underlying list.peek
at the top of the stack, do getLast()
onthe underlying list.Stacks are fairly common data structures and crop up in a variety of applications. They are especially useful in parsing and translation of computer languages, mathematics, and other formal notations.
Consider a mathematical expression like $(a+(b)) - c*((-d) + 1)$. Suppose that we waned to check whether the parentheses are properly balanced.
We could scan the string from left to right, following the rules:
After we have scanned the entire string, we look at the stack. If the stack is empty, then everything was balanced. If we ever tried to pop an empty stack, or if we have characters left on the stack, then the parentheses in the string were unbalanced.
For example
Input remaining: (a+(b)) - c*((-d) + 1)
Stack |
The first character that we see is a ‘(’, so we push it on to the stack.
Input remaining: a+(b)) - c*((-d) + 1)
( |
Stack |
Then we see ‘a’, and ‘+’. We throw those away.
Input remaining: (b)) - c*((-d) + 1)
( |
Stack |
Now we see another ‘(’, and we push it.
Input remaining: b)) - c*((-d) + 1)
( |
( |
Stack |
Then we discard the ‘b’.
Input remaining: )) - c*((-d) + 1)
( |
( |
Stack |
Now we see a ‘)’, so we pop it.
Input remaining: ) - c*((-d) + 1)
( |
Stack |
Again we see a ‘)’, so we pop it.
Input remaining: - c*((-d) + 1)
Stack |
We skip past the ‘ ’, ‘-’, ‘ ’, ‘c’, and ’*’.
Input remaining: ((-d) + 1)
Stack |
We see a ‘(’, so we push it.
Input remaining: (-d) + 1)
( |
Stack |
We see another ‘(’, so we push it.
Input remaining: -d) + 1)
( |
( |
Stack |
We skip past the ‘-’ and the ‘d’.
Input remaining: ) + 1)
( |
( |
Stack |
We see a ‘)’, so we pop.
Input remaining: + 1)
( |
Stack |
We skip past the ‘ ’, ‘+’, ‘ ’, and ‘1’.
Input remaining: )
( |
Stack |
We see a ‘)’, so we pop it.
Input remaining: (none)
Stack |
We have processed all of the input, and the stack is empty. so we conclude that the parentheses were properly balanced.
On the other hand, suppose that we had the input (a)+)(b)
Input remaining: (a)+)(b))
Stack |
First we see a ‘(’ so we psh it.
Input remaining: a)+)(b))
( |
Stack |
Then we skip oast the ‘a’.
Input remaining: )+)(b))
( |
Stack |
We see a ‘)’ so we pop.
Input remaining: +)(b))
Stack |
We move past the ‘+’.
Input remaining: )(b))
Stack |
Now we see a ‘)’. Normally we would pop the stack. However, the stack is already empty, so stop, knowing that the parentheses are unbalanced.
You might have realized that we didn’t really need the stack in these examples. We could have simply keep a counter indicating how elements would have been on the stack, incrementing the counter when we would have pushed and decrementing when we would have popped.
But the stack pays off when we introduce other forms of bracketing, such as [ ]
or { }
.
We modify our algorithm slightly:
Is 5*(a[(i-1)] + 1}
properly balanced?
Input remaining: 5*(a[(i-1)] + 1}
Stack |
First we skip past the ‘5’ and the ’*’.
Input remaining: (a[(i-1)] + 1}
Stack |
We see a ‘(’ so we push it.)
Input remaining: a[(i-1)] + 1}
( |
Stack |
We skip past the ‘a’.
Input remaining: [(i-1)] + 1}
( |
Stack |
We see a ‘[’, so we push it.
Input remaining: (i-1)] + 1}
[ |
( |
Stack |
We see a ‘(’, so we push it.
Input remaining: i-1)] + 1}
( |
[ |
( |
Stack |
We skip past the ‘i’, ‘-’, and ‘1’.
Input remaining: )] + 1}
( |
[ |
( |
Stack |
We see a ‘)’. We check it against the top character on the stack and we find that the ‘)’ matches the earlier ‘(’. So we pop the stack.
Input remaining: ] + 1}
[ |
( |
Stack |
We see a ‘]’. We check it against the top character on the stack and we find that the ‘]’ matches the earlier ‘[]’. So we pop the stack.
Input remaining: + 1}
( |
Stack |
We skip past the ‘ ’, ‘+’, ‘ ’, and ‘1’.
Input remaining: }
( |
Stack |
We see a ‘}’, but when we check the top of the stack, we see a ‘(’. Those don’t match, so we stop the algorithm and announce that the grouping characters are not properly balanced.
postfix translation and evaluation
compilers
Your text gives examples of some of these, and you will do one in the next assignment.
Postfix (also known as Reverse Polish Notation or RPN) is a parentheses-free notation for mathematical expressions:
operators appear after their operands, e.g.:
1 2 +
instead of 1+2
1 2 3 * +
instead of 1+2*3
1 2 + 3 *
instead of (1+2)*3
Postfix is easily evaluated using a stack:
when you see a constant, push it onto the stack
when you see an operator that needs k operands,
pop k numbers from the stack
apply the operator to them
push the result back onto the stack
Try out the RPN calculator in an animation.
Now, you might find this example a bit artificial-looking. Most people don’t write their expressions out in postfix, preferring the more conventional infix notation. Postfix does have the slight virtue that it can represent any algebraic expression without parentheses.
In fact, back in the not-so-distant past when scientific calculators did not have parentheses keys, anyone using such a calculator was accustomed to entering their intended calculation in postfix form. It’s still common for calculators, compilers, and other software that must process expressions to convert those into postfix form and then use the above algorithm to evaluate the resulting postfix expression.
Conversion to postfix is, itself, generally performed via a stack (or via recursion, which we will later see has a close relationship to stack-based algorithms). For example, to convert a “normal” infix algebraic expression without parentheses into postfix, you could do this:
S = an empty stack;
while (more input available) {
read the next token, T;
if T is a variable name or a number,
print it;
else { // T is an operator
while (S is not empty and T has lower precedence than top operator on S) {
print top operator on S;
pop S;
}
push T onto S
}
while (S is not empty) {
print top operator on S;
pop S;
}
Try this with an expression like “1 + 2*3 + 4”. The output will be “1 2 3 * + 4 +”.
This algorithm can be modified, without too much trouble, to work with parentheses as well.
Another common application of stacks is in converting recursive algorithms to iterative, which we will discuss later.