Problem overview
The function hasCycle receives head, the first node of a singly linked list, or null for an empty list. It returns true if some node can be reached again by following next pointers, which happens when the tail's next points back to an earlier node. Otherwise it returns false.
The value pos in the examples only describes how the test list was built, by naming the node the tail connects to. The function never receives it, so it must find the cycle from the nodes alone.
For [3, 2, 0, -4] with the tail linked back to the node holding 2, the walk goes 3, 2, 0, -4, 2, 0, -4, 2 and so on forever, so the answer is true. For [1] with no link back, the walk ends after one node, so the answer is false.
Approach: a slow pointer and a fast pointer
The obvious solution remembers every node already visited, for example in a set, and reports a cycle when a node shows up twice. It works, but it needs memory proportional to the list length. The follow-up asks for constant memory, and two pointers deliver it.
Both pointers start at the head. On each turn, slow moves one node forward and fast moves two. There are two outcomes:
- No cycle.
fastruns ahead and reaches the end of the list, wherenextisnull. The loop stops and the answer isfalse. - A cycle. Both pointers eventually enter the cycle and keep going round it. Inside the cycle,
fastgains exactly one node onslowper turn, so the gap between them shrinks by one each time. It can't jump overslow, so the gap reaches zero and they stand on the same node.
On [3, 2, 0, -4] with the tail linked to the node holding 2, the pointers move like this, showing the values they stand on:
turn 1: slow = 2, fast = 0
turn 2: slow = 0, fast = 2
turn 3: slow = -4, fast = -4 (same node: cycle found)
Step by step
- Declare
slowandfastwithvar, both starting athead. They change on every turn, so they can't beval. Their type isListNode?, becauseheadmay benull. - Loop while
fast != nullandfast.next != null.fastjumps two nodes, so both the node it stands on and the next one must exist. If either isnull,fasthas reached the end. Inside the loop Kotlin treatsfastas non-null (a smart cast), sofast.nextneeds no?.. - Move
slowone node, toslow!!.next.slowis always behindfastor on the same node, so it is nevernullhere, and!!tells the compiler so. - Move
fasttwo nodes, tofast.next!!.next. The loop condition has just checked thatfast.nextis notnull. - Compare with
===. Ifslow === fast, both pointers stand on the same node, so returntrue. - After the loop, return
false. The loop only ends whenfasthas run off the list, which a cycle makes impossible.
The solution in Kotlin
class Solution {
fun 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 === fast) return true
}
return false
}
}
The Kotlin details worth a closer look:
===versus==. In Kotlin,==callsequalsand compares contents, while===compares references. Two different nodes can hold the same value, so only===answers the question "is this the very same node?".- Smart cast and
!!. Afterfast != nullsucceeds, Kotlin lets you usefastas a non-nullListNode, because it is a localvarthat nothing else changes in between.fast.nextis a property of the node, so its type is stillListNode?, and the compiler doesn't carry the earlier check over tofast.next!!.next. The!!operator converts it to non-null and throws an exception if it werenull. Here the loop condition guarantees it isn't. slowandfastare nullable. They get the typeListNode?fromhead, soslow!!.nextis needed even thoughslowcan't benullat that point.- The early
return.if (slow === fast) return trueis a one-lineifwith no braces. It is fine for a single statement.
Complexity
Time: O(n). Without a cycle, fast reaches the end after about n / 2 turns. With a cycle, slow enters the cycle after at most n turns, and then fast closes the gap, which is smaller than the cycle's length, at one node per turn. The total stays proportional to n.
Space: O(1). Only two references are stored, however long the list is.
Edge cases and common mistakes
- Empty list. With
headequal tonull, the loop condition fails immediately and the function returnsfalse. - One node. A node whose
nextisnullhas no cycle, and the loop never runs. A single node whosenextpoints to itself is a cycle:fastandslowboth move to that node on the first turn and meet, so the result istrue. - Checking only
fast != null. On a list with an odd number of nodes,fastlands on the last node, andfast.nextisnull. Without the second check,fast.next!!would throw aNullPointerException. - Comparing before moving. Both pointers start on the same node. If you compared them at the top of the loop, you would report a cycle for every non-empty list.
- Comparing values. In
[1, 1]without a cycle, the two nodes hold equal values. Comparing values would report a cycle that isn't there.
Testing
[3, 2, 0, -4], tail linked to index 1:true.[1, 2], tail linked to index 0:true.[1]with no link:false.[1]linked to itself:true.[1, 2, 3, 4, 5]with no link:false.- An empty list:
false.
