Problem overview
hasCycle(head *ListNode) receives the first node of a singly linked list and reports whether the list contains a cycle: a node that can be reached again by following Next pointers. An empty list is nil. The list can have up to 10,000 nodes.
For the list 3 -> 2 -> 0 -> -4 whose tail points back to the node holding 2, the answer is true. For 1 -> 2 where the tail points back to the head, it is true as well. For a single node with no link back, the answer is false. How the cycle was built is hidden from the function: it only sees the nodes.
Approach: two pointers at different speeds
Following Next pointers in a list without a cycle ends at nil. In a list with a cycle, it never does, so a plain walk would loop forever. A first fix is to remember every node already visited in a map and report a cycle when a node comes up twice. That works in O(n) time, but the map takes O(n) space, and the follow-up asks for constant memory.
The constant-memory fix uses two pointers that move at different speeds. slow advances one node per step and fast advances two. Without a cycle, fast reaches the end and the walk stops. With a cycle, both pointers eventually enter it, and from then on fast gains exactly one node on slow per step. A gap that shrinks by one each step must reach zero, and then the pointers are on the same node. So the pointers meet if and only if there is a cycle.
For 3 -> 2 -> 0 -> -4 with the tail linked to the node 2, the pointers move as follows, counting nodes from 0:
| Step | slow at | fast at |
|---|---|---|
| start | node 0 (3) | node 0 (3) |
| 1 | node 1 (2) | node 2 (0) |
| 2 | node 2 (0) | node 1 (2) |
| 3 | node 3 (-4) | node 3 (-4) |
After three steps both are on the same node, so the function returns true.
Step by step
- Return
falsefor fewer than two nodes. An empty list or a list whose first node has noNextcan't have a cycle, unless that single node points to itself, which the guard doesn't catch because itsNextisn'tnil. - Start both pointers at
headwithslow, fast := head, head. - Loop while
fastcan move two nodes, which needs bothfastandfast.Nextto be non-nil. - Move
slowone node:slow = slow.Next. - Move
fasttwo nodes:fast = fast.Next.Next. - Compare the pointers. If
slow == fast, they are on the same node, so there is a cycle: returntrue. - Return
falseafter the loop. The loop only ends whenfasthas fallen off the end of the list, which can't happen in a cycle.
The solution in Go
func hasCycle(head *ListNode) bool {
if head == nil || head.Next == nil {
return false
}
slow, fast := head, head
for fast != nil && fast.Next != nil {
slow = slow.Next
fast = fast.Next.Next
if slow == fast {
return true
}
}
return false
}
The Go details worth a closer look:
- Pointers compare by address.
slow == fastasks whether the two pointers refer to the very same node, not whether the nodes hold equal values. A list such as1 -> 1 -> 1has equal values and no cycle, and it correctly returnsfalse. - Short-circuit evaluation protects the dereference. In
fast != nil && fast.Next != nil, the second test only runs whenfastisn'tnil. Readingfast.Nexton anilpointer would panic. The condition also guarantees thatfast.Next.Nextin the body is safe, sincefast.Nextis known not to benil. - Multiple assignment.
slow, fast := head, headdeclares both variables in one line. Both are*ListNode, the type ofhead. - Only
for. Go has nowhile. Aforwith just a condition is its while loop. - The type is the lesson's. The code uses a struct
ListNodewithVal intandNext *ListNode. A program that runs this function outside the lesson needs that declaration.
Complexity
Time: O(n). Without a cycle, fast reaches the end after about n / 2 steps. With a cycle, slow enters it within n steps, and the pointers then meet within one more lap of the cycle, which is at most another n steps. Every step is constant work.
Space: O(1). Two pointers, no matter the length of the list.
Edge cases and common mistakes
- Empty list.
headisnil, and the guard returnsfalsebefore any dereference. - One node, no cycle.
head.Nextisnil, so the answer isfalse. - One node pointing to itself. The guard passes. The first step leaves both pointers on that node, and the function returns
true. - Two nodes forming a loop.
1 -> 2 -> 1returnstrue: after one stepslowis on the second node andfastis back on the first, and after the second step both are on the first node. - Comparing values instead of pointers. Equal values don't prove a cycle. Compare the pointers.
- Checking the meeting point before moving. Both pointers start on the same node, so a comparison before the first move would always report a cycle. The comparison belongs after the moves.
- Testing only
fast.Nextin the loop condition. When the list has an even length,fastbecomesnil. The condition has to testfastitself first.
Testing
- Values
[3, 2, 0, -4]with the tail linked to index 1 givestrue. - Values
[1, 2]with the tail linked to index 0 givestrue. - Values
[1]with no link back givesfalse. - Values
[1, 2, 3]with no link back givesfalse. - An empty list gives
false.
