A linked list stores elements as a chain of nodes β each holding a value and a pointer to the next β with O(1) insert/delete at known positions and O(n) access.
Linear
Linked List Fundamentals
A chain of nodes scattered across memory β O(1) insertion and deletion at any known position, O(n) access by index. The structure behind Java's LinkedList and every undo/redo stack.
A linked list stores each element in a separate node β an object that holds one value and one pointer to the next node. Nodes are allocated independently on the heap, so they sit at arbitrary memory addresses rather than in a contiguous block. To reach element k, you must start at the head and follow k pointers β that sequential walk is why random access costs O(n). The payoff is structural flexibility: when you already hold a reference to the preceding node, inserting or deleting takes O(1) because you only redirect one or two pointers β no elements shift. Java’s LinkedList<T> is a doubly-linked list (each node also points backward), giving O(1) operations at both head and tail. You encounter this structure behind undo history, LRU caches (LinkedHashMap), OS task scheduling, and any scenario that demands frequent mid-sequence modification without costly array copies.
Analogy: A treasure-hunt game where each clue card contains one item and the address of the next clue card hidden somewhere in the building. You cannot jump to clue 5 directly β you must follow the chain from clue 1. But if you want to add a new clue between clue 3 and clue 4, you simply write the new card, hand it clue 4’s address, and update clue 3 to point to the new card β nobody else moves.
02
Section Two Β· Mental Model
How It Thinks
Each node lives at an independent heap address
βΆ
No contiguous block required β memory can be fragmented
Only way to reach node k is to follow k pointers from head
βΆ
Random access costs O(n) β no address formula exists
Insert/delete only redirects one or two pointers
βΆ
O(1) modification when you already hold a reference to the target node
Nodes are scattered β CPU cannot prefetch next node
βΆ
Poor cache locality makes sequential iteration slower than arrays in practice
Each node stores an extra pointer (8 bytes on 64-bit JVM)
βΆ
Higher per-element memory overhead compared to a plain array
03
Section Three Β· Operations
Core Operations
Traverse
Walks from head to tail, visiting every node once by following next pointers β O(n) to process all elements.
Pseudocode
functiontraverse(head):
current = head
while current != null:
process(current.value) // visit this node
current = current.next // advance pointer// O(n) β one pass through entire list
Traverse β visit every node from head to null
Always check current != null, not current.next != null:
Using current.next != null as the loop condition skips processing of the last node and also fails when the list is empty (head is null).
The correct idiom is while (current != null) with current = current.next at the end of the body.
Insert at Head
Creates a new node, points it at the current head, and updates head β O(1) because no traversal is needed.
Pseudocode
functioninsertAtHead(head, value):
newNode = new Node(value) // allocate on heap
newNode.next = head // point to old head
head = newNode // update head referencereturn head // O(1) β no traversal
Insert at Head β prepend value 3
Order matters β point newNode.next first:
If you update head before setting newNode.next, you lose the reference to the old list.
Always wire the new node’s next pointer to the existing head before swinging head to the new node.
Insert at Tail
Walks to the last node and appends a new node β O(n) for a singly-linked list without a tail pointer, O(1) if a tail reference is maintained.
Pseudocode
functioninsertAtTail(head, value):
newNode = new Node(value) // allocateif head == null:
return newNode // empty list edge case
current = head
while current.next != null:
current = current.next // walk to last node
current.next = newNode // appendreturn head // O(n) traversal + O(1) link
Maintain a tail pointer for O(1) append:
Java’s LinkedList keeps both head and tail references internally.
If you only keep head, every append is O(n).
Adding a single tail field drops append to O(1) β a trade-off of one extra reference for a massive improvement in append-heavy workloads.
Insert at Middle
Traverses to the node before the target position, then rewires pointers to splice in a new node β O(n) to find the position, O(1) to link.
Pseudocode
functioninsertAt(head, index, value):
if index == 0:
returninsertAtHead(head, value)
current = head
for i from0to index - 2:
current = current.next // walk to node before target
newNode = new Node(value)
newNode.next = current.next // point new node to successor
current.next = newNode // wire predecessor to new nodereturn head // O(n) find + O(1) splice
Insert at Middle β insert 99 between nodes 12 and 25
Wire new node first, then previous:
Set newNode.next = prev.next before prev.next = newNode.
Reversing this order disconnects the rest of the list because prev.next would already point to newNode before you capture the old successor.
Delete by Value
Finds a node with a given value and removes it by re-linking its predecessor’s next pointer to its successor β O(n) search, O(1) unlink.
Pseudocode
functiondeleteByValue(head, target):
if head == null: returnnullif head.value == target:
return head.next // delete head itself
current = head
while current.next != null:
if current.next.value == target:
current.next = current.next.next // bypass target nodereturn head
current = current.next
return head // O(n) scan + O(1) unlink
Delete by Value β remove node with value 12
Handle head deletion separately:
If the target value is in the head node, there is no predecessor to re-link.
You must check the head first and return head.next directly β this is the most common edge case missed in interview solutions.
Cycle Detection (Floyd’s)
Two pointers β slow moves one step, fast moves two. If they meet, a cycle exists. O(n) time, O(1) space.
Pseudocode
functionhasCycle(head):
slow = head
fast = head
while fast != nulland fast.next != null:
slow = slow.next // one step
fast = fast.next.next // two stepsif slow == fast:
returntrue// pointers met β cycle existsreturnfalse// fast hit null β no cycle// O(n) time, O(1) space
Floyd’s Cycle Detection β slow and fast pointers
Why fast catches slow inside the cycle:
Once both pointers are inside a loop of length L, fast closes the gap by 1 node per iteration.
After at most L more iterations they collide.
This guarantees O(n) detection without using a HashSet (which costs O(n) extra space).
Common Mistake: Losing the reference to the rest of the list during insertion. When you write prev.next = newNodebefore setting newNode.next = prev.next, the original successor is orphaned β the GC collects it and every node after it. Always capture or assign the successor first, then re-link the predecessor. This one-line ordering bug causes silent data loss that feels like a “random” list truncation.
04
Section Four Β· Variant
Doubly Linked List
A doubly linked list adds a prev pointer to each node β giving every node references to both its predecessor and successor. This single addition transforms deletion from O(n) to O(1) when you hold the node directly, because you no longer need to traverse to find the predecessor. Java’s LinkedList<T> is a doubly linked list with maintained head and tail references β that is why addFirst(), addLast(), removeFirst(), and removeLast() are all O(1).
Node anatomy β singly vs doubly linked
Insert β 4 pointer updates
Pseudocode
functioninsertAfter(node, value):
newNode = new Node(value)
newNode.prev = node // β new β predecessor
newNode.next = node.next // β‘ new β successorif node.next != null:
node.next.prev = newNode // β’ successor β new
node.next = newNode // β£ predecessor β new// O(1) β no traversal needed
Delete β O(1) with node reference
Delete node B β two pointer updates, no traversal
This is why Java’s LinkedList has O(1) removeFirst/removeLast:
Because it is doubly linked with head and tail references, removing from either end requires only 2 pointer updates.
An iterator’s remove() is also O(1) β it holds the node directly and can unlink it without scanning.
05
Section Five Β· Variant
Circular Linked List
In a circular linked list, the tail node’s next pointer points back to the head instead of null β forming a ring with no terminator. Traversal cannot check for null; it must detect when it has returned to the starting node. This structure appears wherever data cycles endlessly: round-robin task schedulers in operating systems, multiplayer board game turn management, music playlist loops, and the classic Josephus elimination problem.
Circular structure β tail.next points to head
Traversal β the null check breaks
Pseudocode β Singly (wrong for circular)
// β WRONG for circular β runs forever (no null exists)
current = head
while current != null: // never triggers!process(current.value)
current = current.next
Pseudocode β Circular (correct)
// β CORRECT β stop when we return to headif head == null: return
current = head
do:
process(current.value)
current = current.next
while current != head // O(n) β one full rotation
Traversal termination β stop when current returns to head
Use do-while, not while:
A while (current != head) loop with current starting at head would never execute the body (the condition is false immediately).
The correct pattern is do { ... } while (current != head) starting from head, or start from head.next with a regular while.
Missing this causes either zero iterations or infinite loops β both common interview mistakes.
06
Section Six Β· Implementation
Build It Once
Build this from scratch once β it makes the mechanics concrete.
In any real project or interview, reach for the built-in (Section 05).
Java β Linked List core mechanics
// Build once to understand the mechanics β use LinkedList in practice.public classSinglyLinkedList {
privateNode head;
privateint size;
private static classNode {
int value;
Node next;
Node(int v) { value = v; }
}
// O(1) β redirect one pointer, no traversalpublic voidaddFirst(int value) {
Node n = newNode(value);
n.next = head; // point to old head
head = n; // swing head to new node
size++;
}
// O(n) β must walk to predecessorpublic voidaddAt(int index, int value) {
if (index == 0) { addFirst(value); return; }
Node prev = head;
for (int i = 0; i < index - 1; i++) prev = prev.next;
Node n = newNode(value);
n.next = prev.next; // wire new β successor
prev.next = n; // wire predecessor β new
size++;
}
// O(n) β scan for target, bypass itpublicbooleanremove(int value) {
if (head == null) returnfalse;
if (head.value == value) { head = head.next; size--; returntrue; }
Node cur = head;
while (cur.next != null) {
if (cur.next.value == value) {
cur.next = cur.next.next; // bypass
size--;
returntrue;
}
cur = cur.next;
}
returnfalse;
}
}
Linked List β Full Implementation
Java β Complete Linked List implementation
public classSinglyLinkedList<T> {
privateNode<T> head;
privateint size;
private static classNode<T> {
T value;
Node<T> next;
Node(T v) { value = v; }
}
// O(1) β prepend at headpublic voidaddFirst(T value) {
Node<T> n = newNode<>(value);
n.next = head;
head = n;
size++;
}
// O(n) β walk to end, appendpublic voidaddLast(T value) {
Node<T> n = newNode<>(value);
if (head == null) { head = n; size++; return; }
Node<T> cur = head;
while (cur.next != null) cur = cur.next;
cur.next = n;
size++;
}
// O(n) β walk to predecessor, splicepublic voidaddAt(int index, T value) {
if (index < 0 || index > size) throw new IndexOutOfBoundsException();
if (index == 0) { addFirst(value); return; }
Node<T> prev = head;
for (int i = 0; i < index - 1; i++) prev = prev.next;
Node<T> n = newNode<>(value);
n.next = prev.next;
prev.next = n;
size++;
}
// O(n) β access by index (no formula β must walk)publicTget(int index) {
if (index < 0 || index >= size) throw new IndexOutOfBoundsException();
Node<T> cur = head;
for (int i = 0; i < index; i++) cur = cur.next;
return cur.value;
}
// O(n) β scan for value, bypass nodepublicbooleanremove(T value) {
if (head == null) returnfalse;
if (head.value.equals(value)) { head = head.next; size--; returntrue; }
Node<T> cur = head;
while (cur.next != null) {
if (cur.next.value.equals(value)) {
cur.next = cur.next.next;
size--;
returntrue;
}
cur = cur.next;
}
returnfalse;
}
// O(n) β linear scanpublicbooleancontains(T value) {
Node<T> cur = head;
while (cur != null) {
if (cur.value.equals(value)) returntrue;
cur = cur.next;
}
returnfalse;
}
// O(n) β Floyd's cycle detectionpublicbooleanhasCycle() {
Node<T> slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) returntrue;
}
returnfalse;
}
// O(1)publicintsize() { return size; }
publicbooleanisEmpty() { return size == 0; }
@Override
public String toString() {
StringBuilder sb = new StringBuilder("[");
Node<T> cur = head;
while (cur != null) {
sb.append(cur.value);
if (cur.next != null) sb.append(" β ");
cur = cur.next;
}
return sb.append("]").toString();
}
public static voidmain(String[] args) {
SinglyLinkedList<Integer> list = new SinglyLinkedList<>();
// 1. Add at head
list.addFirst(30); list.addFirst(20); list.addFirst(10);
System.out.println(list); // [10 β 20 β 30]// 2. Add at tail
list.addLast(40);
System.out.println(list); // [10 β 20 β 30 β 40]// 3. Insert at index 2
list.addAt(2, 25);
System.out.println(list); // [10 β 20 β 25 β 30 β 40]// 4. Remove by value
list.remove(25);
System.out.println(list); // [10 β 20 β 30 β 40]// 5. Get by index
System.out.println(list.get(2)); // 30// 6. Cycle check
System.out.println(list.hasCycle()); // false
}
}
Python β Linked List implementation
classNode:
def__init__(self, value, next_node=None):
self.value = value
self.next = next_node
classSinglyLinkedList:
def__init__(self):
self.head = None
self._size = 0# O(1) β prepend at headdefadd_first(self, value):
self.head = Node(value, self.head)
self._size += 1# O(n) β walk to end, appenddefadd_last(self, value):
if not self.head:
self.head = Node(value)
else:
cur = self.head
while cur.next:
cur = cur.next
cur.next = Node(value)
self._size += 1# O(n) β walk to predecessor, splicedefadd_at(self, index, value):
if index == 0:
self.add_first(value); return
prev = self.head
for _ in range(index - 1):
prev = prev.next
prev.next = Node(value, prev.next)
self._size += 1# O(n) β scan for value, bypassdefremove(self, value):
if not self.head: returnFalseif self.head.value == value:
self.head = self.head.next
self._size -= 1; returnTrue
cur = self.head
while cur.next:
if cur.next.value == value:
cur.next = cur.next.next
self._size -= 1; returnTrue
cur = cur.next
returnFalse# O(n) β access by indexdefget(self, index):
cur = self.head
for _ in range(index):
cur = cur.next
return cur.value
# O(n) time, O(1) space β Floyd'sdefhas_cycle(self):
slow = fast = self.head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow is fast:
returnTruereturnFalsedef__len__(self): return self._size
def__str__(self):
parts, cur = [], self.head
while cur:
parts.append(str(cur.value))
cur = cur.next
return" β ".join(parts)
# βββ Usage βββ
ll = SinglyLinkedList()
ll.add_first(30); ll.add_first(20); ll.add_first(10)
print(ll) # 10 β 20 β 30
ll.add_last(40)
print(ll) # 10 β 20 β 30 β 40
ll.add_at(2, 25)
print(ll) # 10 β 20 β 25 β 30 β 40
ll.remove(25)
print(ll) # 10 β 20 β 30 β 40
print(ll.get(2)) # 30
print(ll.has_cycle()) # False
07
Section Seven Β· Java Built-in
Use It In Java
IN JAVA
UseLinkedList<T> β or ArrayDeque<T> when you only need stack/queue operations
Importimport java.util.LinkedList;
Method
Complexity
Note
addFirst(value)
O(1)
Prepend β doubly-linked, no traversal
addLast(value)
O(1)
Append β tail pointer maintained internally
add(index, value)
O(n)
Walk to position, then splice β O(n) find + O(1) link
get(index)
O(n)
Must traverse β no address formula
removeFirst()
O(1)
Unlink head node
removeLast()
O(1)
Unlink tail β doubly-linked gives prev pointer
remove(value)
O(n)
Scans for first occurrence, bypasses it
contains(value)
O(n)
Linear scan β no fast lookup
size()
O(1)
Stored as a field β not counted
β Gotcha: Java’s LinkedList implements both List and Deque, so it has get(index) β but calling it in a loop gives O(nΒ²) total because each call walks from the nearest end. If you need indexed access, use ArrayList. If you need stack/queue ops, prefer ArrayDeque which has better cache locality and lower per-element overhead.
ChooseUse LinkedList only when you need O(1) insertions and deletions at known iterator positions (e.g. LRU cache internals, scheduling queues with removals). For pure stack/queue behaviour, ArrayDeque is faster due to contiguous memory. For random access, use ArrayList.
08
Section Eight Β· Decision Guide
Pick The Right Structure
Decision Flowchart
Comparison Table
Structure
Access
Insert (middle)
Delete (middle)
Best For
LinkedList β this
O(n)
O(1)*
O(1)*
Iterator-based insert/delete, LRU cache
ArrayList
O(1)
O(n)
O(n)
Random access, iteration, sorting
ArrayDeque
O(n)
O(1) head/tail
O(1) head/tail
Stack, queue, BFS/DFS
HashMap
O(1) avg
O(1) avg
O(1) avg
Keyβvalue lookup, frequency count
* O(1) when you already hold a reference/iterator to the preceding node β finding that position is O(n).
09
Section Nine Β· Practice
Problems To Solve
Two patterns dominate linked-list interview problems: two-pointer (fast/slow to detect cycles, find midpoints, or identify the k-th from end) and pointer manipulation (reversing sublists, merging sorted lists, or partitioning). Look for phrases like "middle node", "cycle", "reverse", "merge", or "reorder" β they signal one of these patterns.