DSA Linked Structures
Linked List Operations
Brute force is correct: walk from head, then rewire. The time limit is not, if every “O(1) insert” still pays a linear search, or if one overwrite drops the suffix.
Separate finding a from the . Preserve every still-needed pointer before publishing a new edge. A collapses front special cases.
Operation journey
Find
Locate target, predecessor, or stopping condition from head.
Preserve
Save successor links that overwriting would lose.
Change the owning link; repair head, tail, and size.
Check
Empty, singleton, head, interior, and tail cases still hold.
What actually happens
Naive idea: “delete node X” by setting some other node's next to X.next without finding the owner of the inbound link. On a singly linked list that owner is the ; without it you cannot splice safely.
Better idea: find → preserve → publish. Insertion after a known node is Θ(1) rewiring. Deletion of the head uses a so the same loop works. Iterative reverse keeps a so the unprocessed suffix never disappears.
The improved design still fails with a stale tail, foreign handles, accidental cycles, or charging only the splice while the problem statement requires “insert at index i.”
Invariant (pointer safety)
Before any link overwrite, every still-needed neighbor reference is stored in a local. After the publish step, the reachable chain from head (or dummy.next) contains exactly the intended nodes, and metadata (head, tail, size, prev links) matches that chain.
How to choose an operation pattern
1. Index vs handle? Index i implies a Θ(i) walk. A supplied node handle unlocks Θ(1) splice.
2. Singly vs doubly? Singly needs a predecessor for delete/insert-before. Doubly can unlink from the victim alone if both neighbors are live.
3. Boundary style? Explicit empty/front branches, or a / sentinel that makes every data node interior.
Quantitative check
Reverse rewrites each of n next fields once: Θ(n) time, Θ(1) extra space iteratively. Fast/slow midpoint detection examines O(n) edges with two cursors. Claiming “k inserts are O(k)” is true only when each insert already has its predecessor; otherwise budget Θ(n) per locate on a length-n list.
Find
Starting from head, advance until the target, predecessor, or stopping condition is known. This is usually the linear part.
Preserve
Save any successor that would otherwise become unreachable. A temporary reference is part of the correctness proof.
Publish
Change the owning link: head, tail, prev.next, or next.prev. Then repair metadata such as size and tail.
Interactive pointer-rewiring lab
Step through insertion, deletion, and iterative reversal. The node table shows physical identity and next fields; the chain rows show what is actually reachable from each root.
Pointer-rewiring laboratory
Preserve the suffix, then publish the splice.
Start with a known predecessor
Step 1 / 4
next-field writes
0
reachable from head
3
rewiring after lookup
Θ(1)
reachable from head
Current action
The caller already holds B. Finding B is outside this operation's Θ(1) rewiring cost.
Invariant
Following next from head visits A, B, C exactly once and then reaches null.
Original interactive linked-list playground (retained)
Use the original hands-on demo to add nodes at either end, remove nodes, and watch the chain update. It remains available alongside the more focused pointer-rewiring trace above.
Operation costs, with the preconditions exposed
| Operation | Time | Extra space | Why |
|---|---|---|---|
| Read or update index i | Θ(i + 1), worst Θ(n) | Θ(1) | Follow i links, then inspect or update node i. |
| Search by value | Best Θ(1), worst Θ(n) | Θ(1) | Unsorted nodes provide no direction for skipping work. |
| Push front / pop front | Θ(1) | Θ(1) | Only head and one next link change. |
| Push back | Θ(1) with tail; otherwise Θ(n) | Θ(1) | A tail reference avoids traversal. |
| Insert after a known node | Θ(1) | Θ(1) | Lookup time is not included in the splice. |
| Delete after a known predecessor | Θ(1) | Θ(1) | A singly linked node does not point backward. |
| Delete a known node in a doubly list | Θ(1) | Θ(1) | Both neighbors are directly available. |
| Reverse the list | Θ(n) | Θ(1) iterative | Each next field is rewritten once. |
A list that maintains head, tail, and size
These operations state their contracts explicitly. A node passed to insert_after or delete_after must belong to this list; validating membership would itself require a traversal unless the representation stores ownership metadata.
Core updates
Python
from __future__ import annotations
from dataclasses import dataclass
@dataclass(slots=True)
class Node:
value: int
next: Node | None = None
class SinglyLinkedList:
def __init__(self):
self.head: Node | None = None
self.tail: Node | None = None
self.size = 0
def push_front(self, value: int) -> Node:
fresh = Node(value, self.head)
self.head = fresh
if self.tail is None: # empty -> one node
self.tail = fresh
self.size += 1
return fresh
def push_back(self, value: int) -> Node:
fresh = Node(value)
if self.tail is None:
self.head = self.tail = fresh
else:
self.tail.next = fresh
self.tail = fresh
self.size += 1
return fresh
def insert_after(self, node: Node, value: int) -> Node:
fresh = Node(value, node.next) # preserve suffix first
node.next = fresh # publish splice
if self.tail is node:
self.tail = fresh
self.size += 1
return fresh
def pop_front(self) -> int:
if self.head is None:
raise IndexError("pop from empty list")
victim = self.head
self.head = victim.next
if self.head is None:
self.tail = None
victim.next = None
self.size -= 1
return victim.value
def delete_after(self, predecessor: Node) -> int:
victim = predecessor.next
if victim is None:
raise IndexError("no node follows predecessor")
predecessor.next = victim.next
if self.tail is victim:
self.tail = predecessor
victim.next = None
self.size -= 1
return victim.valueJava
import java.util.NoSuchElementException;
final class SinglyLinkedList {
static final class Node {
final int value;
Node next;
Node(int value, Node next) {
this.value = value;
this.next = next;
}
}
private Node head;
private Node tail;
private int size;
Node pushFront(int value) {
Node fresh = new Node(value, head);
head = fresh;
if (tail == null) tail = fresh; // empty -> one node
size++;
return fresh;
}
Node pushBack(int value) {
Node fresh = new Node(value, null);
if (tail == null) {
head = tail = fresh;
} else {
tail.next = fresh;
tail = fresh;
}
size++;
return fresh;
}
Node insertAfter(Node node, int value) {
Node fresh = new Node(value, node.next); // preserve suffix first
node.next = fresh; // publish splice
if (tail == node) tail = fresh;
size++;
return fresh;
}
int popFront() {
if (head == null) throw new NoSuchElementException("pop from empty list");
Node victim = head;
head = victim.next;
if (head == null) tail = null;
victim.next = null;
size--;
return victim.value;
}
int deleteAfter(Node predecessor) {
Node victim = predecessor.next;
if (victim == null) {
throw new NoSuchElementException("no node follows predecessor");
}
predecessor.next = victim.next;
if (tail == victim) tail = predecessor;
victim.next = null;
size--;
return victim.value;
}
}Reverse
Python
def reverse(self) -> None:
old_head = self.head
prev = None
current = self.head
while current is not None:
successor = current.next # save before overwrite
current.next = prev
prev = current
current = successor
self.head = prev
self.tail = old_head
# Invariant before each iteration:
# - prev reaches the reversed processed prefix
# - current reaches the untouched suffix
# - those disjoint chains contain every original nodeJava
final class ReversibleList<T> {
static final class Node<T> {
final T value;
Node<T> next;
Node(T value) { this.value = value; }
}
private Node<T> head;
private Node<T> tail;
void reverse() {
Node<T> oldHead = head;
Node<T> previous = null;
Node<T> current = head;
while (current != null) {
Node<T> successor = current.next; // save before overwrite
current.next = previous;
previous = current;
current = successor;
}
head = previous;
tail = oldHead;
}
}
// Invariant before each iteration:
// - previous reaches the reversed processed prefix
// - current reaches the untouched suffix
// - those disjoint chains contain every original nodeCycle detection
Python
def cycle_entry(head: Node | None) -> Node | None:
slow = fast = head
# Phase 1: find a meeting point, if a cycle exists.
while fast is not None and fast.next is not None:
slow = slow.next
fast = fast.next.next
if slow is fast:
break
else:
return None
# Phase 2: equal-speed pointers meet at the entry.
seeker = head
while seeker is not slow:
seeker = seeker.next
slow = slow.next
return seekerJava
final class CycleEntry {
static final class Node<T> {
final T value;
Node<T> next;
Node(T value) { this.value = value; }
}
static <T> Node<T> find(Node<T> head) {
Node<T> slow = head;
Node<T> fast = head;
// Phase 1: find a meeting point, if a cycle exists.
do {
if (fast == null || fast.next == null) return null;
slow = slow.next;
fast = fast.next.next;
} while (slow != fast);
// Phase 2: equal-speed pointers meet at the entry.
Node<T> seeker = head;
while (seeker != slow) {
seeker = seeker.next;
slow = slow.next;
}
return seeker;
}
}Why pointer-write order matters
Insert after current
fresh.next = current.next
current.next = freshThe first assignment gives the fresh node ownership of the old suffix. Publishing current.next first without another saved reference can disconnect that suffix.
Delete after predecessor
victim = predecessor.next
predecessor.next = victim.nextThe predecessor bypasses exactly one node. If the victim is the tail, tail must move to the predecessor before the victim is reclaimed.
Doubly linked deletion
left.next = right
right.prev = leftRepair both directions. At boundaries, a sentinel makes left and right non-null node objects: sometimes dummy sentinels, and eliminates head/tail branches.
Iterative reversal
successor = current.next
current.next = prevSaving successor is essential: after the second line, current no longer points toward the unprocessed suffix.
Dummy nodes remove boundary branches
A temporary dummy node points to the real head, so deleting the first real node looks like every other deletion. The dummy is an algorithmic helper, not part of the returned list.
Python
def delete_first(head: Node | None, target: int):
dummy = Node(0, head)
predecessor = dummy
while predecessor.next is not None:
if predecessor.next.value == target:
victim = predecessor.next
predecessor.next = victim.next
victim.next = None
return dummy.next, True
predecessor = predecessor.next
return dummy.next, FalseJava
final class DeleteFirst {
static final class Node {
final int value;
Node next;
Node(int value, Node next) {
this.value = value;
this.next = next;
}
}
static final class Result {
final Node head;
final boolean removed;
Result(Node head, boolean removed) {
this.head = head;
this.removed = removed;
}
}
static Result deleteFirst(Node head, int target) {
Node dummy = new Node(0, head);
Node predecessor = dummy;
while (predecessor.next != null) {
if (predecessor.next.value == target) {
Node victim = predecessor.next;
predecessor.next = victim.next;
victim.next = null;
return new Result(dummy.next, true);
}
predecessor = predecessor.next;
}
return new Result(dummy.next, false);
}
}In a list object that caches tail and size, the successful branch must update them too. A helper that returns only the new head cannot maintain metadata it does not own.
Operations that become reusable patterns
Fast and slow pointers
Find a midpoint, detect a cycle, or locate a cycle entry using constant extra space. State the speed and termination guard precisely.
Merge sorted chains
Maintain a tail pointer on a dummy result node. Each step appends the smaller front node; no data values need to be copied.
Split and reverse
Palindrome checks split near the middle, reverse one half, compare, and optionally restore the structure before returning.
Intersection by identity
Two lists intersect only when they share the same node objects. Equal values are insufficient; pointer identity is the relevant relation.
k-group rewiring
Reverse complete blocks of k nodes while preserving the next block's entry. A dummy predecessor simplifies reconnection.
LRU movement
A hash map supplies node handles while a doubly linked list moves a known node to the front in Θ(1). Either structure alone misses one requirement.
Edge cases and failure modes
Empty and singleton transitions
When the last node is removed, both head and tail become null. When the first node is inserted, both must name it.
Stale tail
Appending after the old tail or deleting the tail without moving the cached reference corrupts later operations.
Lost suffix
Overwriting current.next before saving it can make the rest of the chain unreachable.
Accidental cycles
A wrong target link may prevent traversal from ever reaching null. Use visited identities or fast/slow pointers in diagnostics.
Foreign or stale handles
A supposedly known node may belong to another list or may already be detached. Decide whether the API trusts, tags, or validates handles.
Reclamation bugs
Manual-memory code risks leaks, double frees, and use-after-free. Garbage collection prevents those frees but does not prevent logical stale references.
Identity-changing shortcut
Copying successor data into a node and deleting the successor is not a general delete-by-handle operation: it fails at the tail and changes object identity.
Concurrent mutation
Readers can observe half-completed rewiring without synchronization. Lock-free algorithms require atomic operations and safe memory reclamation.
Common failure modes
- Define whether positions are indices, node handles, or iterators.
- Count lookup separately from constant-time rewiring.
- Save outgoing links before overwriting them.
- Update head, tail, size, and both directions together.
- Specify ownership of removed nodes and returned handles.
- Test empty, singleton, head, interior, tail, and missing cases.
- Assert reachability, size, tail.next, and backward-link invariants.
- Prefer a standard-library container unless node-level control is required.
Exercises
- Delete the head of A→B→C using a . Show the predecessor pointer at each step and the final returned head.
- Trace iterative reverse on 1→2→3 with the . What goes wrong if you skip saving
successor? - You must insert before a known singly linked node X. Why is that not Θ(1) from X alone, and what extra information restores a constant-time splice?
Primary sources
- Cormen et al., Introduction to Algorithms (CLRS): list insert/delete and sentinel techniques.
- LeetCode classic statements: Reverse Linked List; Linked List Cycle (Floyd); Merge Two Sorted Lists.
- Java
LinkedList/ListIteratordocs (iterator-based O(1) insert/remove after positioning).
Takeaway
Linked-list operations are find, preserve, and publish. The splice is tiny; the correctness lives in the pointers you refuse to lose and the metadata you repair afterward.