Foundation · Module 10 · Lesson 1

Singly linked list

Nodes, head/tail references, traversal, insertion, and deletion.

15–25 min readLesson 53 of 123 Interactive Lab

Lesson 53 of 123

1d streak

Interactive Visualization

See how singly linked list works

Change the inputs, run an operation, and connect each visible state change to the explanation below.

Interactive data structure

Change the structure and watch the operation

Try an operation and connect the visible movement to its runtime cost.

head
10•
24•
36•
51•
null
readyThe head stores the address of the first node; each node points to the next.

Node Anatomy & Non-Contiguous Memory Allocation

A singly linked list is a linear collection of independent node structures connected via unidirectional reference pointers. Unlike arrays, which occupy a single unbroken span of contiguous physical memory addresses, linked list nodes are dynamically instantiated across arbitrary locations on the process heap. Each node encapsulates two distinct members: a data payload and a forward pointer (next) pointing to the downstream successor node, with the terminal node referencing null.

Memory Representation of Singly Linked List
Head Reference
     |
     v
+----------+------+      +----------+------+      +----------+------+
| Data: 10 | next-+----> | Data: 20 | next-+----> | Data: 30 | null |
+----------+------+      +----------+------+      +----------+------+
  Addr: 0x1000             Addr: 0x4800             Addr: 0x9200
  (Heap Block A)           (Heap Block B)           (Heap Block C)

Notice: Nodes reside at arbitrary heap addresses!
To reach Data 30, the CPU must follow 2 pointer hops (0x1000 -> 0x4800 -> 0x9200).

Architectural Boundary · Do Not Confuse

Concept A

Contiguous Array: L1/L2 Spatial Locality

Concept B

Linked List: Pointer Chasing & L3/DRAM Stalls

Arrays guarantee index i + 1 sits at base + (i + 1) * elementSize, allowing hardware prefetchers to load entire 64-byte cache lines into L1/L2 CPU caches before requests arrive. Linked list nodes reside at scattered heap addresses; traversing curr = curr.next forces the CPU core to stall for 50-100ns while fetching un-cached memory lines from DRAM.

Architectural DimensionPrimitive / Object ArraySingly Linked List
Physical LayoutContiguous memory blockScattered heap allocations
Random Access CostO(1) via base + i * size arithmeticO(k) sequential pointer traversal
Per-Element Memory Overhead0 extra bytes for primitive arrays16-24 bytes (object header + reference pointer)
Insertion at Known NodeO(n) due to element shiftingO(1) pointer redirection
Resizing BehaviorO(n) reallocation and copy when capacity exceededO(1) incremental allocation per node
Cache Miss RateExtremely low (spatial prefetch hits)High (pointer chasing across heap pages)

64-Bit Runtime Memory Amplification

On a standard 64-bit HotSpot JVM with Compressed OOPs disabled, storing an integer requires: 16-byte object header + 4-byte int payload + 4-byte padding + 8-byte next pointer = 32 bytes per node to store 4 bytes of real data (an 8x memory footprint expansion over a primitive int[] array). Even with Compressed OOPs enabled (12-byte header + 4-byte int + 4-byte reference = 20 -> aligned to 24 bytes), linked lists impose massive memory amplification and garbage collection overhead.
ListNode.java
// Canonical Singly Linked List Node Definition
public class ListNode {
    public int val;
    public ListNode next;

    public ListNode() {}

    public ListNode(int val) {
        this.val = val;
        this.next = null;
    }

    public ListNode(int val, ListNode next) {
        this.val = val;
        this.next = next;
    }
}

The Sentinel (Dummy Head) Pattern: Banishing Edge Cases

The sentinel pattern—frequently termed a dummy head—is one of the most transformative architectural primitives in pointer-based programming. Without a sentinel, list modifications at index 0 require branching because the list reference variable itself (head) must be reassigned. When deleting or inserting, treating head differently from interior nodes leads to fragile nested conditionals.

Architectural Boundary · Do Not Confuse

Concept A

Raw Head Pointer: Fragmented Branching

Concept B

Sentinel Node: Unified Predecessor Invariant

Raw head code must explicitly guard against null head, head deletion, and empty insertions with separate if-statements. A sentinel guarantees that EVERY authentic node in the list—including the true head—possesses a non-null predecessor, standardizing the deletion and insertion logic across the entire sequence.

Sentinel (Dummy Head) Pattern in Action
Standard List (Deleting head requires special if-condition):
Head
 |
 v
[ 10 ] ----> [ 20 ] ----> [ 30 ] -> null

Sentinel Architecture (Every real node has a guaranteed predecessor!):
Dummy Head            Original Head
+-----------+       +-----------+       +-----------+
| Val:  -1  | ----> | Val:  10  | ----> | Val:  20  | ----> null
+-----------+       +-----------+       +-----------+
      ^
      |-- dummy.next always tracks the real head!
          Deleting node 10: dummy.next = dummy.next.next (Zero edge cases!)
  1. 01Allocate an immutable sentinel node: ListNode dummy = new ListNode(0); dummy.next = head;.
  2. 02Initialize traversal and modification cursors pointing to dummy: ListNode prev = dummy;.
  3. 03Execute all pointer splices relative to prev (e.g., prev.next = prev.next.next) without checking whether prev is null.
  4. 04Return dummy.next as the updated list head, allowing the garbage collector to reclaim dummy without memory leaks.
RemoveElementsSentinel.java
// Eliminating head deletion edge cases using a Sentinel Node
public ListNode removeElements(ListNode head, int targetVal) {
    ListNode dummy = new ListNode(-1);
    dummy.next = head;
    ListNode curr = dummy;

    while (curr.next != null) {
        if (curr.next.val == targetVal) {
            // Unlink matching node: predecessor points around target
            curr.next = curr.next.next;
        } else {
            // Only advance curr when no deletion occurred
            curr = curr.next;
        }
    }

    return dummy.next;
}

Active Recall Checkpoint

Does allocating a dummy head node degrade the auxiliary space complexity of an algorithm?

Show answer & explanation

No. A dummy head allocates exactly one single node reference regardless of list length n. Its auxiliary space complexity is strictly O(1).

Pointer Rewiring: Insertion, Deletion & Memory Reclamation

Pointer mutation operates as a temporal sequence: the order of pointer assignments is non-commutative. Assigning a new reference overwrites the pre-existing address. When inserting a new node into a chain, the new node must latch onto its downstream successor BEFORE the predecessor severs its forward link.

Visualizing Node Insertion & Deletion Step-by-Step
--- INSERTION AFTER NODE 'PREV' ---
Initial State:
  [ Prev ] --------------------------> [ NextNode ]
              [ NewNode ]

Step 1: newNode.next = prev.next;
  [ Prev ] --------------------------> [ NextNode ]
              [ NewNode ] ------------->^

Step 2: prev.next = newNode;
  [ Prev ] ---------> [ NewNode ] ----> [ NextNode ]
  (Order matters! If step 2 runs first, NextNode is lost forever!)

--- DELETION AFTER NODE 'PREV' ---
Initial State:
  [ Prev ] ---------> [ Target ] -----> [ NextNode ]

Step 1: prev.next = prev.next.next;
  [ Prev ] ---------------------------> [ NextNode ]
                      [ Target ] (no references -> Garbage Collected!)

The Overwrite Trap (Orphaned Suffix)

If you execute prev.next = newNode; before newNode.next = prev.next;, you overwrite prev.next with newNode. Subsequently assigning newNode.next = prev.next; sets newNode.next = newNode, creating an infinite 1-node circular loop while permanently orphaning the entire remaining tail of the list from memory.
  1. 01Insertion Step 1: newNode.next = prev.next; (wire forward link to preserve remaining list).
  2. 02Insertion Step 2: prev.next = newNode; (redirect predecessor to newNode).
  3. 03Deletion Step 1: ListNode nodeToDelete = prev.next; (optional: retain handle for GC or unmanaged free).
  4. 04Deletion Step 2: prev.next = prev.next.next; (bridge around nodeToDelete).
  5. 05Deletion Step 3: nodeToDelete.next = null; (sever lingering references to prevent GC memory retention).
OperationSingly Linked ListArrayList / Dynamic ArrayNotes
Prepend (add first)O(1)O(n)Array shifts all elements right
Append (with tail ptr)O(1)O(1) amortizedArray occasionally copies on capacity breach
Lookup by Index kO(k)O(1)Linked list must step through k nodes
Insert After Given NodeO(1)O(n)No element shifting in linked list
Delete After Given NodeO(1)O(n)O(1) pointer bypass in linked list
Search Unsorted by ValueO(n)O(n)Both require linear scan

Use it when

Use a singly linked list when prepend operations dominate, when frequent O(1) insertions/deletions occur at known node references, or when implementing lock-free concurrent queues (Treiber stacks / Michael-Scott queues).

Reconsider when

Reconsider when heavy indexed reads (get(i)), binary searches, or strict memory-density constraints apply; CPU cache misses make array traversal 10-50x faster in practice.

Complete SinglyLinkedList Implementation & Architecture

Here is a complete, production-grade Java implementation of a SinglyLinkedList using the sentinel head pattern and explicit size tracking. Every operation maintains the class invariant: dummy.next points to the true head node, or null when size == 0.

MyLinkedList.java
public class MyLinkedList {
    private static class Node {
        int val;
        Node next;
        Node(int val) { this.val = val; }
    }

    private final Node dummy;
    private int size;

    public MyLinkedList() {
        dummy = new Node(-1);
        size = 0;
    }

    public int get(int index) {
        if (index < 0 || index >= size) return -1;
        Node curr = dummy.next;
        for (int i = 0; i < index; i++) {
            curr = curr.next;
        }
        return curr.val;
    }

    public void addAtHead(int val) {
        addAtIndex(0, val);
    }

    public void addAtTail(int val) {
        addAtIndex(size, val);
    }

    public void addAtIndex(int index, int val) {
        if (index < 0 || index > size) return;
        Node prev = dummy;
        for (int i = 0; i < index; i++) {
            prev = prev.next;
        }
        Node newNode = new Node(val);
        newNode.next = prev.next;
        prev.next = newNode;
        size++;
    }

    public void deleteAtIndex(int index) {
        if (index < 0 || index >= size) return;
        Node prev = dummy;
        for (int i = 0; i < index; i++) {
            prev = prev.next;
        }
        prev.next = prev.next.next;
        size--;
    }

    public int size() {
        return size;
    }
}

Active Recall Checkpoint

In deleteAtIndex(0), why does prev.next = prev.next.next correctly remove the first real element without throwing a NullPointerException?

Show answer & explanation

Because prev starts at dummy, which is guaranteed non-null. Since index 0 is valid (size > 0), dummy.next is the authentic head, and dummy.next.next is either the second node or null. The assignment dummy.next = dummy.next.next cleanly detaches the head.

Cost Model

Complexity & Trade-offs

Access / known-node update
O(n) / O(1)
O(1)
The bound follows from the number of states visited and the work performed per transition.

When to use it

  • Frequent local insertions, deletions, chain rewiring, and cycle analysis.
  • Problems where contiguous storage or random access is unnecessary.
  • In-place sequence transformations.

Common mistakes

  • Losing the unread suffix by overwriting next too early.
  • Failing to update head or tail after a boundary mutation.
  • Dereferencing null or looping forever on a cycle.

Practice prompts

  • Reproduce the worked Singly linked list example without looking at the trace.
  • Change one input condition and explain whether the Singly linked list invariant still holds.
  • Implement the approach, derive its cost, and compare it with a straightforward baseline.

Mastery check

Can you derive it instead of memorizing it?

State the invariant before looking at code.
Explain why every transition is safe.
Construct a worst-case input.
Name one alternative and its trade-off.