DSA Middle of the Linked List
The middle of a linked list is the node halfway along the chain. A slow pointer and a fast pointer find that node in one walk.
The problem
A linked list is a chain of nodes. Each node holds a value. Each node also holds a link to the next node.
The problem gives the head of the chain. The head is the first node. The answer is the middle node of the chain.
An odd-length list has one middle node. An even-length list has two middle nodes. This problem asks for the second of the two.
The cases used in this article
class Node:
def __init__(self, value):
self.value = value
self.next = None
def build(values):
head = None
tail = None
for value in values:
node = Node(value)
if head is None:
head = node
tail = node
else:
tail.next = node
tail = node
return head
def render(head):
parts = []
node = head
while node is not None:
parts.append(str(node.value))
node = node.next
parts.append("None")
return " -> ".join(parts)
cases = [
([1, 2, 3, 4, 5], 3),
([1, 2, 3, 4], 3),
([1, 2], 2),
([7], 7),
]
for values, expected in cases:
print(render(build(values)), "| middle:", expected)Read one row at a time. Five nodes have the single middle 3. Four nodes have middles 2 and 3, and the answer is 3. Two nodes answer 2 for the same reason.
Intuition
The first idea uses a plain Python list. Copy every value into the list. Then read the value at index len // 2.
A list supports direct index access. A linked list does not. The copy buys that access with extra memory.
Copy the values, then index the copy
class Node:
def __init__(self, value):
self.value = value
self.next = None
def build(values):
head = None
tail = None
for value in values:
node = Node(value)
if head is None:
head = node
tail = node
else:
tail.next = node
tail = node
return head
def render(head):
parts = []
node = head
while node is not None:
parts.append(str(node.value))
node = node.next
parts.append("None")
return " -> ".join(parts)
head = build([1, 2, 3, 4, 5])
values = []
node = head
while node is not None:
values.append(node.value)
node = node.next
print("copied values:", values)
print("index:", len(values) // 2)
print("middle value:", values[len(values) // 2])The copy works. It also stores every value a second time. The chain itself already holds those values in order.
A leaner first idea counts the nodes instead. Counting needs no copy. It still walks the chain once just to learn its length.
Brute force
The brute force counts the nodes first. It then walks half that count from the head. The node it lands on is the middle.
Two walks are the cost. The first walk only measures. The second walk does the finding.
Count, then walk half the count
class Node:
def __init__(self, value):
self.value = value
self.next = None
def build(values):
head = None
tail = None
for value in values:
node = Node(value)
if head is None:
head = node
tail = node
else:
tail.next = node
tail = node
return head
def render(head):
parts = []
node = head
while node is not None:
parts.append(str(node.value))
node = node.next
parts.append("None")
return " -> ".join(parts)
def middle_brute(head):
count = 0
node = head
while node is not None:
count += 1
node = node.next
steps = count
node = head
for _ in range(count // 2):
node = node.next
steps += 1
return node, steps
cases = [[1, 2, 3, 4, 5], [1, 2, 3, 4], [1, 2], [7]]
for values in cases:
node, steps = middle_brute(build(values))
print(values, "middle:", node.value, "node visits:", steps)The visit count shows the bill. Five nodes cost seven visits. The count visits five nodes, and the walk then moves two more times.
The method is correct on every case. It is also wasteful. One walk should be enough if the length never needs to be known.
The optimal solution: slow and fast pointers
Move two pointers at two speeds, so the slow pointer sits halfway when the fast pointer finishes. That one sentence is the whole method.
The slow pointer moves one node per round. The fast pointer moves two nodes per round. Both pointers start at the head.
The loop runs while the fast pointer can still move twice. Each round moves slow once and fast twice. The fast pointer covers the chain at double speed, so slow covers exactly half.
The fast-slow solution
class Node:
def __init__(self, value):
self.value = value
self.next = None
def build(values):
head = None
tail = None
for value in values:
node = Node(value)
if head is None:
head = node
tail = node
else:
tail.next = node
tail = node
return head
def render(head):
parts = []
node = head
while node is not None:
parts.append(str(node.value))
node = node.next
parts.append("None")
return " -> ".join(parts)
def middle_node(head):
slow = head
fast = head
while fast is not None and fast.next is not None:
slow = slow.next
fast = fast.next.next
return slow
cases = [[1, 2, 3, 4, 5], [1, 2, 3, 4], [1, 2], [7]]
for values in cases:
node = middle_node(build(values))
print(values, "middle:", node.value)All four cases from the start of the article pass. The even case answers 3. The fast pointer steps past the last node there, and slow rests on the second middle.
No count is stored. No copy is built. The chain is walked once.
Walkthrough
Watch the two pointers on the list 1 to 5. The printout shows both values after each round.
Slow and fast on [1, 2, 3, 4, 5]
class Node:
def __init__(self, value):
self.value = value
self.next = None
def build(values):
head = None
tail = None
for value in values:
node = Node(value)
if head is None:
head = node
tail = node
else:
tail.next = node
tail = node
return head
def render(head):
parts = []
node = head
while node is not None:
parts.append(str(node.value))
node = node.next
parts.append("None")
return " -> ".join(parts)
head = build([1, 2, 3, 4, 5])
slow = head
fast = head
print("start: slow", slow.value, "fast", fast.value)
step = 0
while fast is not None and fast.next is not None:
slow = slow.next
fast = fast.next.next
step += 1
fast_text = fast.value if fast is not None else None
print("round", step, ": slow", slow.value, "fast", fast_text)
print("middle:", slow.value)Round one puts slow on 2 and fast on 3. Round two puts slow on 3 and fast on 5.
The loop then stops. The fast pointer sits on the last node, and its next link is None. Slow rests on 3, the middle node.
Complexity
The time cost is O(n) for a chain of n nodes. The fast pointer makes one pass, and the slow pointer moves inside the same pass.
The space cost is O(1). Two pointers are stored, and nothing grows with the chain.
Counting moves instead of timing them
class Node:
def __init__(self, value):
self.value = value
self.next = None
def build(values):
head = None
tail = None
for value in values:
node = Node(value)
if head is None:
head = node
tail = node
else:
tail.next = node
tail = node
return head
def render(head):
parts = []
node = head
while node is not None:
parts.append(str(node.value))
node = node.next
parts.append("None")
return " -> ".join(parts)
def brute_steps(head):
count = 0
node = head
while node is not None:
count += 1
node = node.next
return count + count // 2
def fast_slow_rounds(head):
slow = head
fast = head
rounds = 0
while fast is not None and fast.next is not None:
slow = slow.next
fast = fast.next.next
rounds += 1
return rounds
for size in [5, 10, 20, 40]:
values = list(range(1, size + 1))
head = build(values)
print("nodes", size, "brute visits:", brute_steps(head), "fast-slow rounds:", fast_slow_rounds(head))The counts grow in a straight line. Doubling the nodes doubles the brute visits. It also doubles the fast-slow rounds.
The saving is a constant factor, not a new growth rate. One pass replaces a pass and a half. The copy version also drops its extra list.
Edge cases and pitfalls
An empty list has no middle node. The function returns None there. The loop test handles that case before any move happens.
The loop test needs both checks. It tests fast itself and then fast.next. Dropping the second check makes the code read next on the last node twice removed, and an even-length walk can fail.
Edge cases, run
class Node:
def __init__(self, value):
self.value = value
self.next = None
def build(values):
head = None
tail = None
for value in values:
node = Node(value)
if head is None:
head = node
tail = node
else:
tail.next = node
tail = node
return head
def render(head):
parts = []
node = head
while node is not None:
parts.append(str(node.value))
node = node.next
parts.append("None")
return " -> ".join(parts)
def middle_node(head):
slow = head
fast = head
while fast is not None and fast.next is not None:
slow = slow.next
fast = fast.next.next
return slow
empty = middle_node(None)
print("empty:", empty)
print("single:", middle_node(build([7])).value)
print("two nodes:", middle_node(build([8, 9])).value)
print("six nodes:", middle_node(build([1, 2, 3, 4, 5, 6])).value)Two nodes answer the second node. Six nodes answer 4, the second of middles 3 and 4. The even-length rule stays consistent at every size.
Exercise
The fast pointer moves two nodes per step and the slow pointer moves one. When the fast pointer reaches the end, where is the slow pointer?