Problem overview
The method hasCycle receives head, the first ListNode of a singly linked list, and returns a Boolean: true if following the next pointers can bring you back to a node you have already visited, false if the chain ends. A list can be empty, in which case head is null.
In the first example the list is 3 -> 2 -> 0 -> -4 and the tail's next points back to the node holding 2. Walking forward never ends, so the answer is true. In the second example 1 -> 2 has a tail that points back to the head, again true. In the third, a single node 1 whose next is null, the walk stops after one step, so the answer is false. The index pos in the statement only describes how the test list was built. The method never receives it.
Approach: two pointers at different speeds
The obvious idea is to remember every node you visit and return true when you see one a second time. That works in linear time but needs a set that grows with the list. The follow-up asks for constant memory.
The constant-memory idea is Floyd's tortoise and hare. Start two pointers at head. Move slow one node per turn and fast two nodes per turn.
- If the list has no cycle,
fastreaches the end, wherenextisnull, and the loop stops. The answer isfalse. - If the list has a cycle, both pointers eventually enter it and keep going around. Inside the cycle, each turn
fastgains exactly one node onslow, so the gap between them shrinks by one per turn. A gap that shrinks by one each time reaches zero, and zero means they stand on the same node. They can't jump over each other, because a gap of 1 becomes 0, not -1.
Only two references are stored, so the extra space is constant.
Step by step
- Declare
slowandfastwithvar, both starting athead. They are reassigned on every turn, sovalwould not do. - Loop while
fast != nullandfast.next != null. Fast jumps two nodes, so the node it stands on and the one after it must both exist. The order of the two tests matters:&&stops at the firstfalse, sofast.nextis only read whenfastis notnull. - Move
slowone node withslow = slow.next. - Move
fasttwo nodes withfast = fast.next.next. This is safe because the loop condition just checked both links. - Compare the pointers with
eq. Ifslow eq fast, they are on the same node, so a cycle exists and the method returnstrue. The comparison happens after both pointers moved. At the start they are equal, since both arehead, and checking before moving would report a cycle in every non-empty list. - Finish with
false. If the loop ends,fastfell off the end of the list, so there is no cycle. The last expression of a Scala method is its result, so noreturnis needed here.
The solution in Scala
class Solution {
def hasCycle(head: ListNode): Boolean = {
var slow = head
var fast = head
while (fast != null && fast.next != null) {
slow = slow.next
fast = fast.next.next
if (slow eq fast) return true
}
false
}
}
The Scala details worth knowing:
eqis reference identity. It asks whether both sides are the very same object. That is the question here: two different nodes may hold equal values, and they are still different nodes. For a plain class likeListNode,==would also end up comparing references, buteqstates the intent and keeps working if a class ever defines its ownequals.returninside a loop. Normally the last expression is the value of a method, but a value in the middle of a loop can only be sent out withreturn. Herereturn trueleaves the method at once, and the finalfalseis the value only if the loop ends on its own.nullis allowed forListNode. The lesson's nodes usenullto mean "no next node", so the checksfast != nullandfast.next != nullare ordinary comparisons.- Method as an expression. The
ifwithout anelseis used as a statement, so its value is discarded. That is fine because the branch exits throughreturn.
Complexity
Time: O(n). Without a cycle, fast covers the list in about n / 2 turns. With a cycle, slow enters the cycle after at most n turns, and from then on the gap between the pointers is smaller than the cycle's length and shrinks by one per turn, so they meet within another n turns or fewer.
Space: O(1). The method keeps two references, whatever the length of the list.
Edge cases and common mistakes
- Empty list. With
head = null, the loop condition fails immediately and the method returnsfalse. - One node. A node whose
nextisnullhas no cycle. A node whosenextis itself is a cycle of length 1:fastandslowboth move to that node on the first turn, so they meet and the method returnstrue. - Equal values are not a cycle. A list
1 -> 1 -> 1 -> 1with no loop back returnsfalse. Comparing values (slow.`val` == fast.`val`) would answertruewrongly. Compare the nodes themselves. - Testing for equality before moving. Both pointers start on
head, so the check must come after the moves. - Reading
fast.next.nextwithout checkingfast.next. In a list with an odd number of nodes,fastlands on the last node, and itsnextisnull. Reading.nextofnullwould throw aNullPointerException. - Moving
fastby one node. If both pointers move at the same speed, the gap never changes and they never meet.
Testing
[3, 2, 0, -4]with the tail linked to the second node givestrue.[1, 2, 3]with no link back givesfalse.[1]with itsnextpointing to itself givestrue.[1, 1, 1, 1]with no link back givesfalse.- An empty list (
null) givesfalse.
