Problem overview
The method hasCycle receives head, the first node of a singly linked list, or null for an empty list. It returns true if following the next pointers from the head can lead back to a node you have already visited, and false if the walk ends at a null.
A cycle exists when the tail's next points at some earlier node instead of null. The position it points to, called pos in the statement, is only used to build the test lists. The method never receives it, so it must detect the cycle from the nodes alone.
In the first example the list is 3 -> 2 -> 0 -> -4 and the tail points back to the node holding 2, so the answer is true. In the third example the single node 1 points to nothing, so the answer is false.
Approach: a slow pointer and a fast pointer
The obvious idea is to remember every node you visit, for example in a Set<ListNode>, and answer true the moment you see a node twice. That works in O(n) time but uses O(n) memory. The statement's follow-up asks for constant memory.
The constant-memory idea is Floyd's tortoise and hare. Two pointers start at the head. The slow one moves one node per turn and the fast one moves two.
- If the list has no cycle, the fast pointer reaches the end first, and the walk stops.
- If the list has a cycle, both pointers eventually enter it and keep going round. Inside the cycle, each turn the fast pointer gains exactly one node on the slow one, so the gap between them shrinks by one every turn. A gap that shrinks by one can't skip over zero, so the pointers must land on the same node.
Take 3 -> 2 -> 0 -> -4 with the tail pointing back to the node holding 2. Calling the nodes A (3), B (2), C (0) and D (-4), the pointers move like this:
- Start: slow and fast are both at A.
- After turn 1: slow is at B, fast is at C.
- After turn 2: slow is at C, fast is at B (D leads back to B).
- After turn 3: slow is at D, fast is at D. They meet, so the answer is
true.
Step by step
- Declare both pointers as
ListNode | nulland start them athead. The type includesnullbecause the list can be empty. - Loop while the fast pointer can jump. It moves two nodes, so both
fastandfast.nextmust exist. Checking them in this order matters: the second check reads a field offast, which is only safe once the first has passed. - Move the slow pointer one node, to
slow.next. - Move the fast pointer two nodes, to
fast.next.next. The loop condition already guarantees thatfast.nextexists. - Compare the pointers. If they are the same node, the walk has gone round a cycle, so return
true. - Return
falseafter the loop. The loop only ends when the fast pointer ran off the list, which can't happen in a cycle.
The solution in TypeScript
class Solution {
hasCycle(head: ListNode | null): boolean {
let slow: ListNode | null = head;
let fast: ListNode | null = head;
while (fast !== null && fast.next !== null) {
slow = slow.next;
fast = fast.next.next;
if (slow === fast) return true;
}
return false;
}
}
The TypeScript details worth knowing:
===compares references for objects.slow === fastis true only when both variables hold the very same node object. Two different nodes with equalvalfields are not equal: withnew ListNode(1)twice, the nodes themselves compare false even though their values compare true. That is what makes the check correct for lists with repeated values.- The annotations are erased.
ListNode | nullexists only for the compiler. At run timenullis just a value, and the checkfast !== nullis what actually protects the property reads. - Why
slow.nextis safe. The slow pointer is always behind the fast one or on the same node, so whenever the loop body runs andfastis a node,slowis a node too. The lesson's compiler settings don't complain about readingslow.nextfrom a variable typedListNode | null. - Chained reads.
fast.next.nextwould throw aTypeErroriffast.nextwerenull. The loop condition rules that out.
Complexity
Time: O(n). Without a cycle, the fast pointer reaches the end after about n / 2 turns. With a cycle, the slow pointer enters the cycle after at most n turns and the gap then closes in at most one more lap of the cycle, so the total is still linear.
Space: O(1). Two pointers are all the method stores, however long the list is.
Edge cases and common mistakes
- Empty list. With
head = null, the loop condition is false at once and the method returnsfalse. - One node. A single node whose
nextisnullstops the loop immediately and returnsfalse. A single node pointing at itself is a cycle: after one turn both pointers are on that node, so the method returnstrue. - Two nodes in a cycle. For
[1, 2]with the tail pointing to the head, one turn puts slow on node 2 and fast back on node 1, and the second turn puts both on node 2, so the answer istrue. - Checking only
fast !== null. On a list with an even number of nodes and no cycle, the fast pointer lands on the last node, and on the next turnfast.nextisnull, so readingfast.next.nextthrows aTypeError. - Comparing values.
slow.val === fast.valreports a cycle in[1, 1]even when there is none. - Testing the pointers before moving them. Both start on
head, so the equality check would be true on the first pass of every list.
Testing
[3, 2, 0, -4]with the tail pointing to index 1 givestrue.[1, 2]with the tail pointing to index 0 givestrue.[1]with no cycle givesfalse.[1, 2, 3, 4]with no cycle givesfalse.- An empty list (
null) givesfalse.
