Skip to content

2. Stacks

A stack organizes entries according to the order in which they were added, so instead of the queue's FIFO, a stack is LIFO, last-in first-out. Think of a bowls stack on top of each other, a stack ADT can represent that ordering. Stacks can be used for a lot of things, however, they are especially useful when check if delimiters are balanced. Below is an example of how a stack works with some pushes and pops.

stacks 1

Abstract Data Type: Stack

Data:

  • A collection of objects in reverse chronological order with the same data type.
Pseudocode UML Description
push(newEntry) +push(newEntry: T): void Adds a new entry to the top of the stack.
pop() +pop(): T Removes and returns the top entry.
peek() +peek(): T Returns the top entry and doesn't change the stack.
isEmpty() +isEmpty(): boolean Dectects whether the stack is empty.
clear() +clear(): void Removes all emtries from the stack.

Interface of the ADT Stack

public interface StackInterface<T> {
    /** Adds a new entry to the top of this stack.
    @param newEntry An object to be added to the stack. */
    public void push(T newEntry);

    /** Removes and returns this stack's top entry.
    @return The object at the top of the stack.
    @throws EmptyStackException if the stack is empty before the operation. */
    public T pop() throws EmptyStackException;

    /** Retrieves this stack's top entry.
    @return The object at the top of the stack.
    @throws EmptyStackException if the stack is empty. */
    public T peek() throws EmptyStackException;

    /** Detects whether this stack is empty.
    @return True if the stack is empty. */
    public boolean isEmpty();

    /** Removes all entries from this stack. */
    public void clear();
}

A Linked-Based Implementation

Think of the stack as a chain, with the topmost node starting the chain then it progessively moves down said chain.

stacks 2

Pushing a new entry is as simple as setting the topmost node to the new entry node and have the new entry node point to the previous topmost node.

stacks 3

Poping a node, return the topmost node and point the topmost node to the second node.

stacks 4

public class LinkedStack<T> implements StackInterface<T> {
    private Node topNode;

    public LinkedStack() {
        this.topNode = null;
    }

    public void push(T newEntry) {
        this.topNode = new Node(newEntry, this.topNode); 
    }

    public T peek() throws EmptyStackException {
        if (isEmpty()) {
            throw new EmptyStackException();
        } else {
            return topNode.getData();
        }
    }

    public T pop() throws EmptyStackException {
        if (isEmpty()) {
            throw new EmptyStackException();
        }
        T top = peek();
        this.topNode = this.topNode.getNextNode();
        return top;
    }

    public boolean isEmpty() {
        return this.topNode == null;
    }

    public void clear() {
        this.topNode = null;
    }

    private class Node {
        private T data;
        private Node next;

        public Node(T data, Node next) {
            this.data = data;
            this.next = next;
        }

        public T getData() {
            return this.data;
        }

        public Node getNextNode() {
            return this.next;
        }
    }
}

An Array-Based Implementation

Like with most ADTs, you can use an array. Pushing, just add the new element to the topIndex. Popping, remove the topIndex and decrement by 1.

stacks 5

import java.util.Arrays;

public class ArrayStack<T> implements StackInterface<T> {
    private T[] stack;
    private int topIndex;

    public ArrayStack(int initialCapacity) {
        this.stack = (T[]) new Object[initialCapacity];
        this.topIndex = -1;
    }

    private void ensureCapacity() {
        if (this.topIndex == this.stack.length-1) {
            this.stack = Arrays.copyOf(this.stack, this.stack.length * 2);
        }
    }

    public void push(T newEntry) {
        ensureCapacity();
        this.topIndex++;
        this.stack[this.topIndex] = newEntry;
    }

    public T peek() throws EmptyStackException {
        if (isEmpty()) {
            throw new EmptyStackException();
        } else {
            return this.stack[this.topIndex];
        }
    }

    public T pop() throws EmptyStackException {
        if (isEmpty()) {
            throw new EmptyStackException();
        } else {
            T top = this.stack[this.topIndex];
            this.stack[this.topIndex--] = null;
            return top;
        }
    }

    public boolean isEmpty() {
        return this.topIndex < 0;
    }

    public void clear() {
        this.stack = (T[]) new Object[this.stack.length];
        this.topIndex = -1;
    }
}

A Vector-Based Implementation

Oh... you don't like the linked or array based implementations? Well, you can also use a vector-based implementation! This implementation generally follows the same style as the array implementation, but with more methods than painfully working with arrays.

import java.util.Vector;

public class VectorStack<T> implements StackInterface<T> {
    private Vector<T> stack;

    public VectorStack(int initialCapacity) {
        this.stack = new Vector<>(initialCapacity);
    }

    public void push(T newEntry) {
        this.stack.add(newEntry);
    }

    public T peek() throws EmptyStackException {
        if (isEmpty()) {
            throw new EmptyStackException();
        } else {
            return this.stack.lastElement();
        }
    }

    public T pop() throws EmptyStackException {
        if (isEmpty()) {
            throw new EmptyStackException();
        } else {
            return this.stack.remove(this.stack.size() - 1);
        }
    }

    public boolean isEmpty() {
        return this.stack.isEmpty();
    }

    public void clear() {
        this.stack.clear();
    }
}

Next Page →