Stacks

Seriously, in programming, we do stack things up.

The Basics

A stack is a LIFO sequence. Addition and removal takes place only at one end, called the top.

stack.gif

The operations are:

Stacks are one of the most widely used collection types in computer science. A tiny fraction of their known uses:

An Interface

You should learn how to build a stack ADT from scratch, because:

Here is a Java interface:

Stack.java
/**
 * A small stack interface. You can (1) query the size of the stack, (2) ask
 * whether it is empty, (3) push an item, (4) pop an item, and (5) peek at the
 * top item.
 */
public interface Stack {

    /**
     * Adds the given item to the top of the stack.
     */
    void push(Object item);

    /**
     * Removes the top item from the stack and returns it.
     * 
     * @exception java.util.NoSuchElementException if the stack is empty.
     */
    Object pop();

    /**
     * Returns the top item from the stack without popping it.
     * 
     * @exception java.util.NoSuchElementException if the stack is empty.
     */
    Object peek();

    /**
     * Returns the number of items currently in the stack.
     */
    int size();

    /**
     * Returns whether the stack is empty or not.
     */
    default boolean isEmpty() {
        return size() == 0;
    }
}

Representations

When building data structures, we often find ourselves with multiple options. We often end up with some mix of arrays and links. Sometimes we use links only, sometimes arrays only. We’ll start with a linked approach.

A Linked Stack

A simple representation is to chain the elements together in a structure bounded only by the size of available memory. The stack itself will store its current size and a chain of its elements. Since we always access the top of the stack only, we can call this field top! Here are a couple pictures of linked stacks:

linked-stack-pictures.png

Once you’ve studied the pictures, the following implementation should make some sense:

LinkedStack.java
import java.util.NoSuchElementException;

/**
 * An implementation of the stack interface using singly-linked nodes.
 */
public class LinkedStack implements Stack {
    private record Node(Object data, Node next) {
    }

    private Node top = null;
    private int size = 0;

    public void push(Object item) {
        top = new Node(item, top);
        size++;
    }

    public Object pop() {
        var item = peek();
        top = top.next;
        size--;
        return item;
    }

    @Override
    public boolean isEmpty() {
        // Override the default implementation for efficiency
        return top == null;
    }

    public Object peek() {
        if (isEmpty()) {
            throw new NoSuchElementException();
        }
        return top.data;
    }

    public int size() {
        return size;
    }
}

A Bounded Array Stack

The simplest array-based approach is one that has the array bounds set on construction tine and can never grow. This approach is known as a bounded stack. It is good for security-minded applications, since no one likes to be attacked by users that use your class to fill up memory.

We store the stack elements in the array, with the size field conveniently referencing the next index to push to:

array-stack-picture.png

BoundedStack.java
import java.util.NoSuchElementException;

/**
 * An implementation of a stack using a fixed, non-expandable array whose
 * capacity is set in its constructor. It comes with an extra method that
 * returns whether the stack is full (something that does not apply to the
 * Stack interface, since not all kinds of stacks have a size bound).
 */
public class BoundedStack implements Stack {
    private Object[] elements;
    private int size = 0;

    public BoundedStack(int capacity) {
        elements = new Object[capacity];
    }

    public void push(Object item) {
        if (isFull()) {
            throw new IllegalStateException("Cannot add to full stack");
        }
        elements[size++] = item;
    }

    public Object pop() {
        var item = peek();
        elements[--size] = null;
        return item;
    }

    public Object peek() {
        if (isEmpty()) {
            throw new NoSuchElementException();
        }
        return elements[size - 1];
    }

    @Override
    public boolean isEmpty() {
        return size == 0;
    }

    public int size() {
        return size;
    }

    // This is a bonus method that is not part of the interface
    public boolean isFull() {
        return size == elements.length;
    }
}

Things to note here:

An Expandable Array Stack

You can do a little “mixing” of the array and linked approaches. Start with an array of reasonable size, then, when it fills up, reallocate a new array (of twice the size), copy the contents over, and add the new element. If the array ever gets too small, allocate a smaller array for the queue and copy into it.

Exercise: This will be homework.

Unit Testing

Here is what we need to test:

We really have three classes to test, but the test cases share a bunch of things in common; the common tests can go in a base class.

StackTest.java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.util.NoSuchElementException;

/**
 * Base test for any class that implements the Stack interface.
 */
public abstract class StackTest {

    /**
     * The stack to use in all the tests: set this in subclasses.
     */
    protected Stack s;

    @Test
    public void testNewStackIsEmpty() {
        assertTrue(s.isEmpty());
        assertEquals(0, s.size());
    }

    @Test
    public void testPushesToEmptyStack() {
        int numberOfPushes = 8;
        for (var i = 0; i < numberOfPushes; i++) {
            s.push("zzz");
        }
        assertFalse(s.isEmpty());
        assertEquals(numberOfPushes, s.size());
    }

    @Test
    public void testPushThenPop() {
        var message = "hello";
        s.push(message);
        assertEquals(message, s.pop());
    }

    @Test
    public void testPushThenPeek() {
        var message = "hello";
        s.push(message);
        var size = s.size();
        assertEquals(message, s.peek());
        assertEquals(size, s.size());
    }

    @Test
    public void testPoppingDownToEmpty() {
        int numberOfPushes = 8;
        for (var i = 0; i < numberOfPushes; i++) {
            s.push("zzz");
        }
        for (var i = 0; i < numberOfPushes; i++) {
            s.pop();
        }
        assertTrue(s.isEmpty());
        assertEquals(0, s.size());
    }

    @Test
    public void testPopOnEmptyStackThrows() {
        assertTrue(s.isEmpty());
        assertThrows(NoSuchElementException.class, () -> s.pop());
    }

    @Test
    public void testPeekIntoEmptyStackThrows() {
        assertTrue(s.isEmpty());
        assertThrows(NoSuchElementException.class, () -> s.peek());
    }
}

The concrete test classes instantiate the specific kind of stack. Because they are subclasses of the base test class, they inherit all of the the common test cases for free! The linked stack test has nothing to add to the base test:

LinkedStackTest.java
import org.junit.jupiter.api.BeforeEach;

public class LinkedStackTest extends StackTest {

    @BeforeEach
    public void makeLinkedStack() {
        s = new LinkedStack();
    }
}

Bounded stacks have that extra functionality (fullness) that must be tested:

BoundedStackTest.java
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class BoundedStackTest extends StackTest {
    private static final int CAPACITY = 100;

    @BeforeEach
    public void makeBoundedStack() {
        s = new BoundedStack(CAPACITY);
    }

    @Test
    public void testPushToFullStack() {
        for (var i = 0; i < CAPACITY; i++) {
            s.push("abc");
        }
        assertThrows(IllegalStateException.class, () -> s.push("abc"));
    }

    @Test
    public void testIsFull() {
        for (var i = 0; i < CAPACITY; i++) {
            s.push("abc");
            assertFalse(s.isEmpty());
        }
        assertTrue(((BoundedStack) s).isFull());
    }
}

An Application of Stacks

If you have job interviews in your future, you’re likely familiar with LeetCode. Let’s look at Leet Code Problem 20 (Valid Parentheses).

“Given a string s containing just the characters '(', ')', '{', '}', '[' and ']', determine if the input string is valid. An input string is valid if (1) open brackets must be closed by the same type of brackets and (2) open brackets must be closed in the correct order.”

To solve this problem, we need to keep track of the opening brackets we have seen but not yet matched. As we see them, we stack them up, and as we match them, we check them off in reverse order. So given an input string like {{[]}()}[], we would proceed as follows:

Input   Op        Stack
{       push      {
{       push      {{
[       push      {{[
]       pop       {{
}       pop       {
(       push      {(
)       pop       {
}       pop
[       push      [
]       pop

If we start with an empty stack and finish with an empty stack, the string is valid. Here’s the algorithm:

ValidBracketChecker.java
public class ValidBracketChecker {

    public static boolean validBrackets(String s) {
        var stack = new LinkedStack();
        try {
            for (var c : s.toCharArray()) {
                if (c == '(' || c == '[' || c == '{') {
                    stack.push(String.valueOf(c));
                } else if (c == '}' && !stack.pop().equals("{")) {
                    return false;
                } else if (c == ']' && !stack.pop().equals("[")) {
                    return false;
                } else if (c == ')' && !stack.pop().equals("(")) {
                    return false;
                }
            }
            // If there is anything left over on the stack after running
            // through the whole string, we have "unclosed" brackets
            return stack.isEmpty();
        } catch (Exception e) {
            return false;
        }
    }

    public static void main(String[] args) {
        System.out.println(validBrackets(args[0]));
    }
}

Summary

We’ve covered:

  • Basic information about stacks
  • An interface
  • Two different implementations
  • Unit testing
  • An application