Problem overview
The method hasCycle receives head, the first node of a singly linked list (a ListNode?, which is nil for an empty list). It returns true if following the next pointers can bring you back to a node you have already visited, and false if the walk reaches the end of the list.
In the first example the list is [3, 2, 0, -4] and the tail points back to the node at index 1, so the answer is true. In the second, [1, 2], the tail points back to the head: true. A single node whose next is nil has no cycle: false. The cycle position is only used to build the test lists; the method never sees it.
The follow-up asks for constant extra memory, and that is what this lesson solves.
Approach: two pointers at different speeds
The obvious solution records every node you visit in a set and reports a cycle when a node shows up twice. It works, but it needs memory proportional to the length of the list.
The constant-memory idea uses two pointers that start on the head. slow moves one node per turn and fast moves two. Two things can happen:
- There is no cycle.
fastruns off the end of the list long beforeslowdoes, and the loop stops. - There is a cycle. Both pointers eventually enter it. Inside the cycle,
fastgains exactly one node onslowevery turn, so the gap between them shrinks by one each time. A gap that shrinks by one can't skip over zero, so the two pointers land on the same node.
With [3, 2, 0, -4] and the tail joined to index 1, the pointers visit these positions: after turn 1, slow is on index 1 and fast on index 2; after turn 2, slow is on 2 and fast on index 1 (it went 2, 3, then back to 1); after turn 3, both are on index 3. They meet, so the method returns true.
Step by step
- Declare
slowandfastwithvar, both starting athead. They change on every turn, and both have the optional typeListNode?because they can becomenil. - Loop while
fastcan move two nodes. That needs the node it stands on, and the one after it, to exist. The conditionfast != nil && fast!.next != nilchecks both. The force unwrap in the second half is safe because&&stops at the firstfalseand never evaluatesfast!whenfastisnil. - Advance
slowby one. It trailsfast, so it is nevernilat this point. - Advance
fastby two withfast!.next!.next. The loop condition already guaranteed the firstnextexists. The result may benil, which ends the loop on the next check. - Compare the pointers with
===. If they are the same object, the walk has gone round a cycle, so returntrue. - Return
falseafter the loop. The loop only ends whenfasthas fallen off the list, which can't happen inside a cycle.
The solution in Swift
class Solution {
func hasCycle(_ head: ListNode?) -> Bool {
var slow = head
var fast = head
while fast != nil && fast!.next != nil {
slow = slow!.next
fast = fast!.next!.next
if slow === fast { return true }
}
return false
}
}
Swift details worth knowing:
===, not==.ListNodeis a class, and the triple-equals operator asks whether two references point to the same object. Two different nodes holding the same value are not equal under===, which is exactly what the problem needs: a list like[1, 1, 1]without a cycle must not be reported as cyclic. A plain==on two class instances that don't conform toEquatabledoesn't compile: the compiler reportsbinary operator '==' cannot be applied to two 'ListNode' operands.- Comparing optionals.
slow === fastworks onListNode?values, so it is also fine when both arenil. In the code above that can't happen, becauseslowwas just moved onto a real node. - Force unwraps. Each
!follows a check that guarantees a value. An unwrap ofnilwould crash the program, so the order of the loop condition matters: swapping its two parts would unwrapfastbefore testing it. - Assignment, not copying. Assigning a node to
slowcopies the reference only. Nothing in the list changes.
Complexity
Time: O(n). Without a cycle, fast reaches the end after about n / 2 turns. With a cycle, slow needs at most n turns to reach the cycle, and then the gap closes by one per turn, which takes at most the length of the cycle. Both parts are bounded by the list size.
Space: O(1). The method keeps two references and nothing that grows with the input.
Edge cases and common mistakes
- Empty list. With
head = nil, the condition fails at once and the method returnsfalse. - One node. If its
nextisnilthe answer isfalse. If the node points to itself, one turn puts both pointers on it and the answer istrue. - Checking only
fast != nil. Thenfast!.next!.nextcrashes on a list with an odd number of nodes, becausefast!.nextisnilwhenfaststands on the last node. - Comparing values.
slow!.val == fast!.valreports a cycle in[1, 1]when there is none. Compare nodes, not values. - Comparing before moving. Both pointers start on
head, so a test before the first move would always succeed. The comparison belongs after the pointers move.
Testing
[3, 2, 0, -4]with the tail joined to index 1 givestrue.[1, 2]with the tail joined to index 0 givestrue.[1]with no cycle givesfalse, and[1]pointing to itself givestrue.[1, 2, 3, 4, 5]with no cycle givesfalse, and an empty list givesfalse.
