Linked Lists
A Python list is a contiguous array under the hood — fast random access, slow
insertion in the middle. A linked list flips that trade-off: no random
access, but insertion or deletion once you’re holding the right node.
Interviewers love linked lists because they test whether you can manipulate
raw pointers correctly, not just call a library function.
What you’ll learn
Section titled “What you’ll learn”- Singly vs doubly linked lists, and the
Nodeclass that builds both. - Why arrays win on cache locality but linked lists win on splice.
- Iterative traversal, and the classic iterative reversal pointer dance.
- Floyd’s Tortoise and Hare cycle detection, in time and space.
- Merging two sorted linked lists — the building block behind merge sort.
The cue
Section titled “The cue”Why a linked list at all
Section titled “Why a linked list at all”A Python list (really a dynamic array) gives you index access but
insertion/deletion anywhere except the end, because every element after
the insertion point has to shift. A linked list stores each value in its own
node, plus a pointer to the next node. Nothing shifts — you just rewire a
couple of pointers.
graph LR
H["head"] --> N1["10"]
N1 --> N2["20"]
N2 --> N3["30"]
N3 --> N4["40"]
N4 --> NUL["None"]
The cost: to reach the 4th node you must walk through the first three — there
is no list[3]-style jump. Traversal is always .
The Node class and a singly linked list
Section titled “The Node class and a singly linked list”class Node:
def __init__(self, value, next=None):
self.value = value
self.next = next
class SinglyLinkedList:
def __init__(self):
self.head = None
def append(self, value):
node = Node(value)
if self.head is None:
self.head = node
return
cur = self.head
while cur.next is not None:
cur = cur.next
cur.next = node
def to_list(self):
out = []
cur = self.head
while cur is not None:
out.append(cur.value)
cur = cur.next
return out
ll = SinglyLinkedList()
for v in [10, 20, 30, 40]:
ll.append(v)
print("traversal:", ll.to_list())Doubly linked lists: pointers both ways
Section titled “Doubly linked lists: pointers both ways”A doubly linked list adds a prev pointer, so you can walk backward and
delete a node in given a reference to it (no need to find its
predecessor first).
class DNode:
def __init__(self, value, prev=None, next=None):
self.value = value
self.prev = prev
self.next = next
class DoublyLinkedList:
def __init__(self):
self.head = None
self.tail = None
def append(self, value):
node = DNode(value)
if self.head is None:
self.head = self.tail = node
return
node.prev = self.tail
self.tail.next = node
self.tail = node
def forward(self):
out, cur = [], self.head
while cur is not None:
out.append(cur.value)
cur = cur.next
return out
def backward(self):
out, cur = [], self.tail
while cur is not None:
out.append(cur.value)
cur = cur.prev
return out
dll = DoublyLinkedList()
for v in [1, 2, 3]:
dll.append(v)
print("forward: ", dll.forward())
print("backward:", dll.backward())Reversing a list: the pointer dance
Section titled “Reversing a list: the pointer dance”Reversal is the single most common linked-list interview question. No new
nodes are allocated — you just walk the list once, flipping each next
pointer to point backward.
class Node:
def __init__(self, value, next=None):
self.value = value
self.next = next
def build(values):
head = tail = None
for v in values:
node = Node(v)
if head is None:
head = tail = node
else:
tail.next = node
tail = node
return head
def to_list(head):
out = []
while head is not None:
out.append(head.value)
head = head.next
return out
def reverse_iterative(head):
prev = None
curr = head
while curr is not None:
nxt = curr.next # save the rest of the list before we overwrite it
curr.next = prev # flip this node's pointer backward
prev = curr # prev advances to curr
curr = nxt # curr advances to the node we saved
return prev # prev ends up as the new head
head = build([1, 2, 3, 4, 5])
print("before:", to_list(head))
new_head = reverse_iterative(head)
print("after: ", to_list(new_head))Three variables (prev, curr, nxt), one pass, zero extra allocation:
time, space.
Detecting a cycle: Floyd’s Tortoise and Hare
Section titled “Detecting a cycle: Floyd’s Tortoise and Hare”If a buggy next pointer (or an intentional circular structure) loops back
on itself, naive traversal never terminates. Two pointers moving at different
speeds solve it without any extra memory: if there’s a cycle, the fast
pointer eventually laps the slow one.
class Node:
def __init__(self, value, next=None):
self.value = value
self.next = next
def build_with_cycle(values, cycle_at=None):
nodes = [Node(v) for v in values]
for i in range(len(nodes) - 1):
nodes[i].next = nodes[i + 1]
if cycle_at is not None:
nodes[-1].next = nodes[cycle_at] # last node points back into the list
return nodes[0]
def has_cycle(head):
slow = fast = head
while fast is not None and fast.next is not None:
slow = slow.next # moves 1 step
fast = fast.next.next # moves 2 steps
if slow is fast: # they met -- must be a cycle
return True
return False
clean = build_with_cycle([1, 2, 3, 4])
looped = build_with_cycle([1, 2, 3, 4], cycle_at=1) # 4 -> back to node at index 1
print("clean list has cycle: ", has_cycle(clean))
print("looped list has cycle:", has_cycle(looped))Merging two sorted linked lists
Section titled “Merging two sorted linked lists”The core routine behind linked-list merge sort: walk both lists once, always taking the smaller head, and re-link nodes — no new nodes are ever created.
class Node:
def __init__(self, value, next=None):
self.value = value
self.next = next
def build(values):
head = tail = None
for v in values:
node = Node(v)
if head is None:
head = tail = node
else:
tail.next = node
tail = node
return head
def to_list(head):
out = []
while head is not None:
out.append(head.value)
head = head.next
return out
def merge_sorted(a, b):
dummy = Node(0) # placeholder so we never special-case an empty result
tail = dummy
while a is not None and b is not None:
if a.value <= b.value:
tail.next = a
a = a.next
else:
tail.next = b
b = b.next
tail = tail.next
tail.next = a if a is not None else b # attach whatever's left
return dummy.next
a = build([1, 3, 5])
b = build([2, 4, 6, 8])
print("merged:", to_list(merge_sorted(a, b)))Time and space complexity
Section titled “Time and space complexity”| Operation | Linked list | Python list (array) |
|---|---|---|
| Access by index | ||
| Search by value | ||
| Insert/delete at head | ||
| Insert/delete at tail (tracked) | amortized | |
| Insert/delete given a node reference | ||
| Auxiliary space per element | extra pointer(s) | none |
LeetCode problem set
Section titled “LeetCode problem set”Generated from the problem database, so each entry carries its sheet membership and reported companies. Tick them off as you go — progress is saved in this browser, and the Export button writes it to a file you can keep.
Work down the ladder. Tick each problem off as you go — progress is saved in this browser, and the Export button in the filter bar writes it to a file you can keep.
- 206Reverse Linked ListeasyThe iterative pointer dance above, from scratch
- 21Merge Two Sorted ListseasyThe dummy-head merge routine above
- 141Linked List CycleeasyFloyd's Tortoise and Hare, exactly as shown
Practice — real LeetCode problems
Section titled “Practice — real LeetCode problems”Each exercise is the actual LeetCode problem with its real method signature and LeetCode’s own examples as the test. Write the body, press Run, and match the expected output.
LC 206 — Reverse Linked List · Easy
Section titled “LC 206 — Reverse Linked List · Easy”Problem. Reverse a singly linked list and return the new head.
Constraints. 0 <= number of nodes <= 5000, -5000 <= Node.val <= 5000.
Examples. [1,2,3,4,5] gives [5,4,3,2,1] · [1,2] gives [2,1] ·
[] gives []
Editorial
Three references and a fixed order of operations. prev trails behind, head
walks forward, and nxt preserves the link you are about to destroy.
Time . Space .
The empty-list case works with no guard: the loop never runs and prev is already
None, which is the correct answer. That is a small sign the design is right.
The recursive version is elegant but stack: reverse the tail, then attach
head to its end. At 5000 nodes that is within Python’s default limit, but only
just — worth mentioning.
This is the core operation behind In-place Linked List Reversal, where the same four lines reverse a segment rather than the whole list.
Follow-ups: “Reverse only positions m..n (LC 92)?” — the same loop, anchored
by a dummy head. “Reverse in groups of k (LC 25)?” — count k ahead, reverse,
relink, repeat. “Recursively?” — have it ready, and mention the stack cost.
LC 203 — Remove Linked List Elements · Easy
Section titled “LC 203 — Remove Linked List Elements · Easy”Problem. Remove every node whose value equals val, and return the new head.
Constraints. 0 <= number of nodes <= 10^4, 1 <= Node.val <= 50,
0 <= val <= 50.
Examples. [1,2,6,3,4,5,6], val = 6 gives [1,2,3,4,5] ·
[7,7,7,7], val = 7 gives []
Editorial
The dummy head
gives the real head a predecessor, so deleting it is no different from deleting any
other node — no if node is head branch anywhere.
Time . Space .
The rule that matters: prev advances only in the else branch. After
unlinking, prev is the predecessor of the next node, which may also need
removing. Advancing unconditionally means [7,7,7,7] leaves stray sevens behind —
that case must return [].
Follow-ups: “Remove the nth node from the end (LC 19)?” — two pointers with a
fixed gap, both starting at the dummy. “Remove duplicates from a sorted list
(LC 83 / LC 82)?” — 83 keeps one of each; 82 removes every duplicated value, which
makes the dummy essential. “Recursively?” — head.next = removeElements(head.next, val), returning head.next when it matches.
LC 876 — Middle of the Linked List · Easy
Section titled “LC 876 — Middle of the Linked List · Easy”Problem. Return the middle node. If there are two middle nodes, return the second one.
Constraints. 1 <= number of nodes <= 100.
Examples. [1,2,3,4,5] gives the node 3 · [1,2,3,4,5,6] gives the node
4
Editorial
If fast moves twice as fast as slow, then when fast reaches the end slow has
covered half the distance.
Time , one pass. Space .
The loop condition is the whole subtlety, and it is worth memorising both forms:
while fast and fast.next—slowlands on the second middle of an even list.[1,2]returns node2. This is what LC 876 wants.while fast.next and fast.next.next—slowlands on the first middle, i.e. the end of the first half. That is what you want when splitting a list, as in Reorder List and merge sort.
Choosing the wrong one is a common source of off-by-one bugs in list problems, and the two-element case is the fastest way to tell them apart.
Follow-ups: “Return the first middle instead?” — switch conditions. “Detect a cycle (LC 141)?” — same pointers; they meet if a cycle exists. “Find the cycle’s start (LC 142)?” — after they meet, reset one to the head and step both by one.
Dry run
Section titled “Dry run”Deleting a node given only a pointer to it (LC 237) — the problem that shows what a singly linked list can and cannot do.
You cannot reach the previous node, so you cannot unlink the target. The trick is to stop trying: copy the successor’s value into the target, then unlink the successor instead.
| step | list | note |
|---|---|---|
| start | 4 → 5 → 1 → 9, delete node 5 | no access to 4 |
| copy value | 4 → 1 → 1 → 9 | 5 became 1 |
| unlink next | 4 → 1 → 9 | the second 1 is gone |
The observable result is correct. Two things to say out loud: the node object you were given still exists (a different one was freed), and this cannot work on the tail, because there is no successor to copy from — which is why the problem guarantees the node is not last.
The variant map
Section titled “The variant map”| Technique | What it solves | Pattern page |
|---|---|---|
| Dummy head | removes every special case at the head | Dummy Head Rewiring |
| Fast and slow pointers | middle, cycle detection, n-th from the end | Fast and Slow Pointers |
| Three-pointer reversal | reverse all or part of a list in place | In-place Reversal |
| Doubly linked + hash map | removal from the middle | Design LRU |
Those four cover essentially every linked-list interview question. This page is the structure; Phase 08 is the techniques.
Interview follow-ups
Section titled “Interview follow-ups”| They ask | What they’re checking | The answer |
|---|---|---|
| “When is a linked list actually better than an array?” | Honest judgement | When you already hold the node and splice frequently — LRU caches, free lists. For iteration or indexing, the array wins on locality |
| “Find the middle in one pass” | Whether you know the idiom | Fast and slow pointers; slow lands on the middle when fast reaches the end |
| “Detect a cycle in space” | Whether you know Floyd’s | Tortoise and hare. A hash set of visited nodes also works but costs space, and the requirement is the point |
| “Why does your solution need a dummy head?” | Whether you can justify it | Because the head may itself be removed or replaced. The dummy gives the previous pointer somewhere to stand, deleting a branch |
| “Reverse in space” | Pointer fluency | Three pointers — prev, cur, nxt. Save next first; the order of the four assignments is the whole problem |
| “What is the space cost versus an array?” | Practical detail | A pointer per node — 8 bytes on 64-bit, plus per-object overhead in Python. For small values that can be several times the payload |
Self-check
Section titled “Self-check”-
Insertion in a linked list is O(1). What does that claim omit?
This is the honest framing, and interviewers probe it. Insert-after-a-known-node is O(1); insert-at-index-k is O(k). Conflating the two is how people over-recommend linked lists.
pch.quizShowAnswer
B — That you must already hold a reference to the position; FINDING it is O(n) — This is the honest framing, and interviewers probe it. Insert-after-a-known-node is O(1); insert-at-index-k is O(k). Conflating the two is how people over-recommend linked lists.
-
Given only a pointer to a middle node, how do you delete it from a singly linked list?
You cannot reach the predecessor, so stop trying to unlink this node and unlink the next one instead. It fails on the tail, which is why LC 237 guarantees the node is not last.
pch.quizShowAnswer
B — Copy the successor's value into this node, then unlink the successor — You cannot reach the predecessor, so stop trying to unlink this node and unlink the next one instead. It fails on the tail, which is why LC 237 guarantees the node is not last.
-
Why does a dummy head simplify list-building and deletion code?
Every bug in this family lives at the head. With a dummy, tail.next = x is unconditionally correct and the answer is just dummy.next.
pch.quizShowAnswer
B — It gives the 'previous' pointer somewhere to stand even when the real head is removed or replaced, deleting the head special case — Every bug in this family lives at the head. With a dummy, tail.next = x is unconditionally correct and the answer is just dummy.next.
-
In the three-pointer reversal, why must nxt be saved before flipping cur.next?
One assignment out of order and the tail of the list is orphaned. This is why the reversal needs three pointers rather than two, and why drawing the arrows first prevents the bug.
pch.quizShowAnswer
B — Because flipping cur.next overwrites the only reference to the rest of the list, making it unreachable — One assignment out of order and the tail of the list is orphaned. This is why the reversal needs three pointers rather than two, and why drawing the arrows first prevents the bug.
Recall card
Section titled “Recall card”- Use when — splice at a node you already hold: LRU caches, free lists, adjacency lists.
- Costs — access by index ; insert/delete at a held node ; space is one pointer per node plus object overhead.
- Honest caveat — finding the position is , and there is no cache locality. Arrays usually win in practice.
- Four techniques cover everything — dummy head, fast/slow pointers, three-pointer reversal, doubly-linked plus hash map.
- Always draw the pointers first. Every bug here is an ordering bug.
- Nodes + pointers trade array cache-locality for splice given a node reference — traversal is always , there’s no random access.
- Reversal is pure pointer rewiring:
prev,curr,nxt, one pass, space. - Floyd’s Tortoise and Hare detects a cycle in time and space — no visited-set required.
- Merging two sorted lists is and re-links existing nodes instead of allocating new ones.
Next: Hash Tables — trading the linked list’s sequential walk for average- lookup by key.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading