Singly Linked List — The Bug That Lost 8% of User Tabs
8% of users lost browser tabs due to a head pointer traversal bug.
- Node = data + next pointer. List = head pointer + chain of nodes.
- Insert at head: O(1). Just rewire head pointer.
- Insert at tail: O(n) without tail pointer. Must traverse to end.
- Delete: O(n) to find, O(1) to unlink. Just redirect previous node's pointer.
- Random access by index: O(n). No direct memory jump like arrays.
- O(1) front insertion vs O(n) random access. Choose based on access pattern.
- Java's LinkedList implements Deque — O(1) addFirst/removeFirst for queue operations.
- Losing the head reference orms the entire list. Always traverse with a separate variable.
- Forgetting null check before dereferencing nextNode. Causes NullPointerException at the last node.
Picture a treasure hunt where each clue doesn't tell you the final answer — it just tells you where the NEXT clue is hidden. Each clue is a 'node': it holds a piece of information AND a pointer to the next location. The last clue says 'dead end' because there's nowhere left to go. That's a singly linked list — a chain of nodes where each one knows only about the one right after it, never the one before.
A singly linked list is a dynamic data structure where each element (node) stores a value and a pointer to the next node. Unlike arrays, nodes are not contiguous in memory — each node can be anywhere on the heap. This gives O(1) insertion and deletion at the head, but O(n) random access.
The structure solves the rigid-size problem of arrays. No pre-allocated block. No resizing overhead. Nodes are allocated on demand and linked via pointers. Deletion is a arrays.
The common misconception is that linked lists are always better than arrays. They are not. Linked lists have poor cache locality (nodes scattered on the heap), O(n) random access, and per-node pointer overhead. Use them when front insertion/deletion is frequent and random access is rare.
Worked Example — Tracing Insertion, Deletion, and Reversal
Build list: insert 1,2,3 at head → 3→2→1→None. Insert 4 at tail: traverse to tail (node 1), set node1.next = Node(4) → 3→2→1→4→None.
Delete node with value 2: 1. prev=None, curr=3. 3!=2, prev=3, curr=2. 2. 2==2: prev.next = curr.next = 1. Skip node 2. List: 3→1→4→None.
Reverse 3→1→4→None: 1. prev=None, curr=3. Save next=1. Set 3.next=None. prev=3, curr=1. 2. Save next=4. Set 1.next=3. prev=1, curr=4. 3. Save next=None. Set 4.next=1. prev=4, curr=None. 4. New head=prev=4. List: 4→1→3→None.
Find middle of 4→1→3→None: Slow=4, fast=4. Step 1: slow=1, fast=3. Step 2: fast.next=None, stop. Middle=slow=1 (second of three nodes).
- prev = null (reversed prefix starts empty). curr = head. next = null.
- Loop: save next = curr.next. Set curr.next = prev. Move prev = curr, curr = next.
- After loop: prev is the new head. The entire list is reversed.
- Time: O(n). Space: O(1). No extra data structure needed.
- This pattern appears in: reverse list, palindrome check, reorder list, reverse k-group.
How a Singly Linked List Works — Plain English and Operations
A singly linked list is a chain of nodes where each node stores a value and a pointer to the next node. The last node's next pointer is None. A head pointer tracks the first node.
Unlike an array, nodes are not contiguous in memory — each node can be anywhere on the heap. This means random access requires traversal (O(n)), but insertion and deletion at a known position are O(1) — just update pointers.
Key operations: 1. Insert at head (O(1)): new.next = head; head = new. 2. Insert at tail (O(n) without tail pointer; O(1) with): traverse to last, last.next = new. 3. Delete node with value v (O(n)): traverse until prev.next.val == v; prev.next = prev.next.next. 4. Search for value v (O(n)): traverse comparing each node's value.
Worked example — insert 1, then 2 at head, then delete 1: Insert 1: head -> [1] -> None. Insert 2 at head: head -> [2] -> [1] -> None. Delete 1: traverse — head.next.val=1. Set head.next = head.next.next = None. Result: head -> [2] -> None.
- Insert at head: newNode.next = head; head = newNode. Two pointer assignments.
- Delete: prev.next = curr.next. One pointer assignment. The deleted node becomes unreachable.
- Reverse: at each node, flip next to point backward. n pointer flips.
- Every operation is O(1) per pointer change. The cost is finding where to change.
- Master pointer rewiring and every linked list problem becomes mechanical.
What a Node Actually Is — The Building Block of Everything
Before you can understand a linked list, you need to deeply understand a node — because the list IS just a collection of nodes.
Think of a node like a Post-it note stuck to a wall. The Post-it has two things written on it: the actual information you care about (say, a person's name) and an arrow pointing to the next Post-it on the wall. That arrow is the 'next pointer'. Without it, each Post-it is just a lonely, disconnected piece of paper.
In Java, a node is a small class with exactly two fields. First: the data it holds (could be a number, a name, anything). Second: a reference — Java's version of a pointer — to another node object. When that reference is null, you've hit the end of the list. Java uses null the same way the last clue in our treasure hunt says 'dead end'.
Here's the key insight that trips beginners up: the node doesn't know its own position. It has no idea it's node number 5. It only knows two things — its own data, and who comes next. That simplicity is what makes the whole structure so flexible.
- Node has two fields: data (the value) and nextNode (the pointer).
- nextNode = null means end of list. No sentinel node needed.
- Node does not know its index. Position is determined by traversal from head.
- In production: encapsulate with getters/setters. For learning: public fields are fine.
- The simplicity of the node (data + pointer) is what makes the list flexible.
neighbors field (a list of references to other nodes). The node structure was the same — data + pointers — but the pointers formed a graph, not a chain. Understanding that a linked list is just a special case of a graph (each node has at most one outgoing edge) helped the team reason about cycle detection, reachability, and memory management.Building a Singly Linked List — Insert, Traverse and Delete
Now that you understand a node, let's build an actual linked list. A singly linked list needs one thing to function: a reference to the very first node, called the 'head'. If you lose the head reference, you lose the entire list — like dropping the first clue in the treasure hunt and having no idea where to start.
The three operations you must master are: inserting a node (at the beginning, end, or middle), traversing the list (walking through it from head to tail), and deleting a node.
Inserting at the front is O(1) — lightning fast. You create a new node, point it at the current head, then make it the new head. Done. Inserting at the end is O(n) because you have to walk all the way to the last node first.
Deletion is where linked lists really shine compared to arrays. To remove a node, you just redirect the previous node's pointer to skip over the target node. The target becomes unreachable and Java's garbage collector cleans it up. No shifting of elements like in an array. No copy operations. Just a pointer redirect.
The code below builds a complete, working linked list with all three operations. Read the comments carefully — they tell the story of what's happening at each step.
- Empty list: head == null. Check before any operation. Return early if empty.
- Head deletion: head.data == value. Handle before the main loop. No previous node exists.
- Traversal: use currentNode, not head. Never modify head during traversal.
- Bonus: tail pointer update. If you delete the last node, update tail to previousNode.
- These three cases appear in every linked list problem. Internalize them.
Singly Linked List vs Array — When to Use Which
Understanding WHEN to use a singly linked list is just as important as knowing how to build one. The honest answer: arrays are often the better default choice, but linked lists win in specific scenarios.
Arrays store elements in contiguous memory, which means accessing element at index 7 is instant — O(1). The CPU can calculate the memory address directly. Linked lists can't do this. To reach node 7, you must start at the head and hop through 7 pointers one by one — O(n). If you're doing a lot of random access by index, an array will run circles around a linked list.
However, linked lists dominate when you're doing frequent insertions and deletions — especially at the front of the list. Adding to the front of an array means shifting every existing element one slot to the right — O(n). Adding to the front of a linked list is three lines of code and O(1). No shifting. No resizing.
The memory story is also interesting. Arrays can waste memory if you pre-allocate a large block and only use half of it. Linked lists only allocate exactly as much memory as they need — but each node pays a tax: it needs extra memory to store the 'next' pointer itself. For small data types like integers, that pointer overhead can actually be larger than the data you're storing.
Use a linked list when: insertion/deletion at the front is frequent, the list size is truly unpredictable, and you never need random index access.
- Array: contiguous memory. CPU prefetcher loads ahead. Cache hits on sequential access.
- Linked list: scattered nodes. Each pointer chase is a potential cache miss.
- For sequential traversal of 10,000 elements: array ~0.1ms, linked list ~0.5ms.
- Big-O is the same O(n). Constant factors differ by 5-10x due to cache.
- In production: profile before choosing. If traversal is the bottleneck, arrays win even with O(n) shifts.
Browser History Service Lost All Tabs: Traversed with Head Instead of CurrentNode
head = head.nextNode instead of a separate currentNode variable. When the first expired tab was found and removed, the head pointer moved past it. All nodes before the expired tab became unreachable — they were still allocated on the heap but no reference pointed to them. The garbage collector reclaimed them on the next cycle. The head pointer now pointed to the node after the expired tab, and the list contained only the remaining tabs after that point.head = head.nextNode traversal with currentNode = currentNode.nextNode using a separate variable. The head pointer never moves during traversal.
2. Added a unit test: insert 5 tabs, expire tab 3, verify all 5 tabs are still in the list (4 active, 1 expired).
3. Added a pre-cleanup snapshot: before cleanup, save the head reference. After cleanup, verify the head reference is unchanged. If changed, rollback and alert.
4. Added a metric: history_list_head_pointer_changed_during_traversal_total to catch future regressions.
5. Documented in code comments: 'NEVER modify head during traversal. Use currentNode. See incident #4821.'- Never modify head during traversal. Use a separate currentNode variable. Head is the entry point — lose it, lose the list.
- The head pointer is the single point of failure. Every operation that traverses the list must preserve it.
- Test with: insert N items, delete an item in the middle, verify all N items are still accessible from head.
- Add a head-unchanged assertion after any traversal operation. If head changed, something is wrong.
- In production: log the head pointer value before and after any list operation. If they differ unexpectedly, investigate.
currentNode != null as the loop condition and check currentNode.nextNode != null before accessing nextNode's fields.head = head.nextNode is used to advance, the head moves and earlier nodes become unreachable. Use a separate currentNode variable for traversal.if (head.data == value) { head = head.nextNode; return; } before the loop.while (currentNode != null). Check currentNode.nextNode != null before accessing nextNode fields.Key takeaways
Interview Questions on This Topic
Frequently Asked Questions
That's Linked List. Mark it forged?
5 min read · try the examples if you haven't