Stacks are very much like a stack of plates. When you don’t want to do the dishes, you simply push the dirty plate on top of the others. Whereas when you are washing them, you take to plate on the top of the stack and proceed to pop them all.
In the same way, the stack abstract data types allows you to push and pop elements on the top of the stack.1
We can see a stack as a bottle, you can only access the content from the top.
Since the last element pushed on the stack is the first one to be popped, we say that the stack is a last in, first out (LIFO) data structure.
We can define our stack in an imperative manner. Each operation modifies a stack instance.
We could also define the stack in a functional manner. The operations applied to the stack is viewed as a mathematical function that maps the old state to the new state. Refer to the example below for more information.
Imperative Definitions
An imperative stack has two essential operations:
\(Push(S, x)\): Add the element \(x\) on the stack \(S\)
\(Pop(S)\): Remove the most recently added element to \(S\) and return it
We can add non-essential operations to ease programming. We note that non-essential operations.
\(Peek(S)\): Return the most recently added element in \(S\) without removing it
\(Create()\): Create a new Stack
\(IsEmpty(S)\): Return a Boolean value specifying if the stack \(S\) is empty
Peek can be defined in terms of \(Push\) and \(Pop\), so we say that this operation is non-essential.
In pseudo code (Python), we could define \(Peek\) as:
Imperative Implementations
Stack ADT in C
For simplicity, the stack is implemented with an array, but it could be implemented using a linked list.
Note that from the user point of view, stack_t is completely opaque to the user. Only stack.c knows the inner representation of stack_t, since we only declared stack_t in stack.h.
We could completely change the implementation detail and nothing would change from the user point of view.
Notice that we pass the stack to the function so that they know on which instance work. This is akin to Python self.
The self argument is passed automatically to the object when calling the function on the instance. Both lines are equivalents.
Note that the two statements are equivalent because we know that print_value is not overridden. That way we are sure that vp.print_value() is equivalent to ValuePrinter.print_value(vp).
Object Oriented Stack in Java
Note how easy it is to add new represents of the stack. This is one of the strong points of object-oriented programming compared to ADT.
Here the stack is defined with an interface that differs from the way we defined stack_t in C. stack_t is an opaque type that only the operations know the inner details, whereas the interface declares the methods a stack has to implement.
On the contrary to the imperative stack, a functional implementation doesn’t mutate the state of the stack. Instead, the operations are seen as mathematical functions that map from the old state to the new frame.
Much like a function \(f(x) = x^{2}\) that maps a value \(x\) to its square. The initial value of \(x\) stays unchanged.
In a functional paradigm, we need three operations to fully define a stack.
\(Push(S, x)\): Returns a new stack with an element \(x\) pushed on top of \(S\)
\(Pop(S)\): Return the stack \(S\) without its first element
\(Top(S)\): Returns the value on top of \(S\)
Note that we don’t need a \(Create\) operation because there is no instance. Instead, we define a special state \(\Lambda\) (or empty) that represents the empty state.
Also note that we don’t need an isEmpty operation either, because we can simply test the equality to \(\Lambda\).
Note here that this code is much shorter and that the stacks aren’t mutated, instead, the functions return a new stack each time.
Summary
Stacks are an elementary data structure. They are fundamentals in computer science. We note their uses in multiples places such as solving sudokus using backtracking algorithms, recursion, converting equations to reverse Polish notation and much more. We even see stacks used in how CPUs handle subroutines. They truly are fundamental to the computer science field, from the theoretical to the practical.
References
Thomas H. Cormen et al. Introduction to Algorithms. 2009. ↩