← Skill tree CS Skill Tree 0 CSCD211

Removing the Only Element Without Crashing

Textbook: Liang

Try This First

A linked list holds exactly one element. You call removeLast(). The loop guard is cur.next != null, so the loop body never executes. prev stays null. What happens on the line prev.next = cur.next;?

Reveal

prev is null, so prev.next throws a NullPointerException. The single-element list is never touched: the list is still size 1, but the method crashed. This is the exact bug found in real reference code (source: SP19.Lab2-ndh-linkedLists).

What You Need To Walk In With

How It Works

A removeLast() written only for the multi-element case looks like this:

public T removeLast() {
    if (size == 0) throw new NoSuchElementException();
    Node<T> cur = head, prev = null;
    while (cur.next != null) {
        prev = cur;
        cur = cur.next;
    }
    // BUG: if size == 1, prev is still null here
    prev.next = cur.next;   // NullPointerException when size == 1
    size--;
    return cur.data;
}

When size == 1, the while condition is false from the start. prev never gets assigned. The line prev.next = cur.next dereferences null.

The fix is to branch before the loop:

public T removeLast() {
    if (size == 0) throw new NoSuchElementException();

    T data = head.data;

    if (size == 1) {
        // Single-element case: no prev exists; just null out head
        head = null;
        size--;
        return data;
    }

    // Multi-element case: walk to the second-to-last node
    Node<T> cur = head;
    while (cur.next.next != null) {
        cur = cur.next;
    }
    cur.next = null;
    size--;
    return data;
}

After the branch, the invariant is restored: head == null and size == 0.

Here is what the pointer state looks like before and after:

Before (size == 1):
  head --> [ "A" | null ]

After (size == 0):
  head --> null

There are no predecessor pointers to rewire. The single work is setting head = null and decrementing size.

Worked Example: Predict, Then Check

Trace this code. What does the list look like after each call?

MyLinkedList<String> list = new MyLinkedList<>();
list.addFirst("X");    // list: [X]
list.removeLast();     // what is head? what is size?
Reveal

After addFirst("X"): head points to a node containing "X", head.next is null, size is 1.

After removeLast() with the corrected single-element branch: the branch fires (size == 1), data is set to "X", head is set to null, size becomes 0, and "X" is returned.

Result: head == null, size == 0. The list is empty and consistent.

A Common Mistake

The wrong move is writing removeLast only for the case where at least two elements exist and forgetting to handle size 1. The prev-tracking loop initializes prev = null and only assigns it inside the loop body. When the list has one element, the loop body never runs, so prev remains null. The next statement (prev.next = cur.next) then tries to dereference null, throwing NullPointerException. The element is not removed and size is not decremented, leaving the list in an incorrect state on every call after that. The fix is always to check size == 1 before entering the loop and handle it as a direct head = null; size--; assignment. (Source: Liang 10e Ch 24)

Go Deeper (optional)

The extra branch costs O(1) time and zero space, so the overall removeLast stays O(n). If you later add a tail reference to your list (a head-and-tail design), the invariant expands: an empty list requires both head == null and tail == null. Forgetting to null tail in the single-element removal is the analogous bug in that design, and it is just as hard to spot at a glance.

Check Yourself

Close the notes and answer each one from memory, then reveal it. Pulling an idea back from memory is one of the strongest ways to make it stick.

Check your understanding

After correctly removing the only element from a no-dummy-head singly linked list, which invariant must hold?

Tier 1 · Liang 10e Ch 24

Why does the standard prev-tracking removeLast crash when size == 1?

Tier 1 · Liang 10e Ch 24

A list contains one node with value 42. removeLast() uses the corrected single-element branch. What are head and size after the call returns?

Tier 2 · Liang 10e Ch 24

A broken removeLast has no size == 1 branch. The list has one element. After calling removeLast(), what is the state of the list?

Tier 2 · Liang 10e Ch 24

Which guard in the corrected multi-element walk keeps the loop from entering a single-element list?

Tier 3 · Liang 10e Ch 24