Foundation · Module 10 · Lesson 1
Singly linked list
Nodes, head/tail references, traversal, insertion, and deletion.
Lesson 53 of 123
1d streakInteractive 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.
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.
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
Contiguous Array: L1/L2 Spatial Locality
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 Dimension | Primitive / Object Array | Singly Linked List |
|---|---|---|
| Physical Layout | Contiguous memory block | Scattered heap allocations |
| Random Access Cost | O(1) via base + i * size arithmetic | O(k) sequential pointer traversal |
| Per-Element Memory Overhead | 0 extra bytes for primitive arrays | 16-24 bytes (object header + reference pointer) |
| Insertion at Known Node | O(n) due to element shifting | O(1) pointer redirection |
| Resizing Behavior | O(n) reallocation and copy when capacity exceeded | O(1) incremental allocation per node |
| Cache Miss Rate | Extremely low (spatial prefetch hits) | High (pointer chasing across heap pages) |
64-Bit Runtime Memory Amplification
// 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
Raw Head Pointer: Fragmented Branching
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.
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!)- 01Allocate an immutable sentinel node: ListNode dummy = new ListNode(0); dummy.next = head;.
- 02Initialize traversal and modification cursors pointing to dummy: ListNode prev = dummy;.
- 03Execute all pointer splices relative to prev (e.g., prev.next = prev.next.next) without checking whether prev is null.
- 04Return dummy.next as the updated list head, allowing the garbage collector to reclaim dummy without memory leaks.
// 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.
--- 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)
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.- 01Insertion Step 1: newNode.next = prev.next; (wire forward link to preserve remaining list).
- 02Insertion Step 2: prev.next = newNode; (redirect predecessor to newNode).
- 03Deletion Step 1: ListNode nodeToDelete = prev.next; (optional: retain handle for GC or unmanaged free).
- 04Deletion Step 2: prev.next = prev.next.next; (bridge around nodeToDelete).
- 05Deletion Step 3: nodeToDelete.next = null; (sever lingering references to prevent GC memory retention).
| Operation | Singly Linked List | ArrayList / Dynamic Array | Notes |
|---|---|---|---|
| Prepend (add first) | O(1) | O(n) | Array shifts all elements right |
| Append (with tail ptr) | O(1) | O(1) amortized | Array occasionally copies on capacity breach |
| Lookup by Index k | O(k) | O(1) | Linked list must step through k nodes |
| Insert After Given Node | O(1) | O(n) | No element shifting in linked list |
| Delete After Given Node | O(1) | O(n) | O(1) pointer bypass in linked list |
| Search Unsorted by Value | O(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.
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
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