Choosing Between No-Dummy-Head and Dummy-Head Lists
Try This First
You are building a print queue: jobs arrive at the back and leave from the front. You have two list designs available. Which one gives you O(1) time at both ends?
Reveal
The dummy-head design. It keeps a tail reference alongside the dummy head node, so addLast is O(1): follow tail.next = newNode; tail = newNode. The no-dummy-head design has no tail field, so addLast must walk every node to find the end, which is O(n).
What You Need To Walk In With
- The no-dummy-head (NDH) list stores a
headreference and asizefield. Every method that touches the first node must special-casehead == nulland must updateheaddirectly. - The dummy-head (DH) list adds a permanent sentinel node at position 0 and a
tailreference pointing at the last real node. Neitherheadnortailever becomes null after construction. - NDH
addFirstandremoveFirstare O(1). NDHaddLastandremoveLastare O(n) because the list must walk to the end. - DH
addLastis O(1) becausetailalready points at the last node. That one field change is the whole tradeoff. - The CSCD 211 lab sequence (Lab 5 then Lab 6) builds NDH first, then upgrades to DH, so the cost difference is visible in your own code.
How It Works
The core question is: which end of the list is “hot”?
NDH is the right fit when only one end is active. A stack pushes and pops at the front. NDH addFirst and removeFirst both run in O(1), and the sentinel node would carry no benefit because tail is never touched. Here is a minimal NDH addFirst:
// NDH -- no sentinel, no tail field
public void addFirst(T element) {
Node<T> newNode = new Node<>(element);
newNode.next = head; // new node points at old head (null if list is empty)
head = newNode; // head is updated directly
size++;
}
The empty-list case is built in: when head == null, newNode.next = null and head = newNode is correct with no extra branch.
DH is the right fit when both ends must be O(1). A queue dequeues from the front and enqueues at the back. The tail reference is what makes O(1) addLast possible:
// DH -- sentinel at position 0, tail points at last real node
public void addLast(T element) {
Node<T> newNode = new Node<>(element);
tail.next = newNode; // splice after current tail
tail = newNode; // advance tail to the new last node
size++;
}
An arrow sketch of the two states:
NDH (stack, 3 elements):
head --> [A] --> [B] --> [C] --> null
DH (queue, 3 elements):
head --> [dummy] --> [A] --> [B] --> [C] --> null
^
tail
In the DH picture, adding a new element at the back touches only tail.next and tail. No traversal is needed.
Summary rule: if your usage touches only the front (or only the back), NDH is simpler. If your usage touches both ends and one of them must be O(1), add the tail field and use DH.
Worked Example: Predict, Then Check
A stack class uses an NDH singly linked list. It calls push(1), push(2), push(3). A classmate suggests switching to DH to “make it better.” What is the concrete benefit of that change for a stack?
Reveal
There is no concrete benefit for stack use. A stack only touches the front of the list (addFirst / removeFirst), and both operations are already O(1) in NDH. Adding a dummy head node and a tail field would use a small amount of extra memory while providing no improvement in time complexity. NDH is the right design for a stack. (Source: Liang 10e Ch 24, S20.Lab5/Lab6)
A Common Mistake
A student builds a text-editor undo queue that must add undo records at the back and consume them from the front, then uses NDH. The code works correctly but slows down noticeably as the undo history grows. The problem is that NDH addLast walks the entire list to find the last node, giving O(n) per enqueue. With 1 000 records queued, each new undo entry triggers 1 000 pointer dereferences. The fix is to switch to DH: the tail field makes addLast O(1) at the cost of one extra field and one extra node. For any queue-shaped usage, DH is the deliberate design choice, not a cosmetic upgrade. (Source: Liang 10e Ch 24)
Go Deeper (optional)
The O(n) versus O(1) difference at the back of the list is the only reason DH exists for singly linked lists. If you are curious about the cost model, try timing NDH addLast on lists of 100, 1 000, and 10 000 elements: the time should scale linearly. DH addLast stays flat across all three. This Big-O intuition carries directly into the Java standard library: java.util.LinkedList keeps both a first and a last reference for exactly the same reason.
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
A queue class must add elements at the back and remove them from the front. Which list design gives O(1) time for both operations?
A stack class only calls addFirst and removeFirst on an NDH list. What is the cost of each call?
An NDH list holds [10 -> 20 -> 30]. What is the worst-case number of node visits for removeLast?
The CSCD 211 lab sequence introduces NDH in Lab 5 and DH in Lab 6. What is the primary learning goal of that ordering?
A DH list’s addLast method sets tail.next = newNode and then tail = newNode. What does the second line do?