Problem overview
The method HasCycle receives the head of a singly linked list of ListNode objects and returns a bool: true if following the next pointers can bring you back to a node you already visited, false if the chain ends in null.
The statement mentions pos, the index of the node the tail links back to. It only describes how the test list was built. The method never receives it, so it has to find the cycle from the nodes alone.
Take [3, 2, 0, -4] where the last node points back to the node holding 2. Walking from the head gives 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 at once and the answer is false. The list can also be empty (head is null), which has no cycle.
Approach: a slow and a fast pointer
The obvious solution keeps a HashSet<ListNode> of the nodes seen so far and returns true when a node shows up twice. It is correct, but it uses O(n) extra memory. The follow-up asks for constant memory.
The constant-memory idea is Floyd's cycle detection, nicknamed the tortoise and the hare. Keep two references that start at the head. In every round, the slow one moves one node and the fast one moves two. Then:
- If there is no cycle, the fast pointer runs off the end of the list and the loop stops.
- If there is a cycle, both pointers end up inside it. Inside a loop, the fast pointer gains exactly one node per round on the slow one, so the gap between them shrinks by one each round until it is zero, and the two references point at the same node.
The gap can't jump over zero, because it changes by one at a time. That is why moving by 1 and 2 is the right pair of speeds: the pointers cannot pass each other without landing on the same node first.
On [3, 2, 0, -4] with the tail linked to the node holding 2, the pointers move like this:
| Round | Slow | Fast |
|---|---|---|
| start | 3 | 3 |
| 1 | 2 | 0 |
| 2 | 0 | 2 |
| 3 | -4 | -4 |
After round 3 both are on the node holding -4, so a cycle exists.
Step by step
- Create two references,
temp(slow) andtemp1(fast), both starting athead. - Guard the short lists. If
headisnullorhead.nextisnull, the list has zero or one node with nothing after it, so it can't loop. The||operator stops at the first true operand, sohead.nextis never read whenheadisnull. - Return
falseinside the guard. - Loop while the fast pointer can move two nodes. That needs both
temp1andtemp1.nextto be non-null. Checkingtemp1first with&&protects the second check. - Move the fast pointer with
temp1 = temp1.next.next. - Move the slow pointer with
temp = temp.next. - Compare the pointers after moving them. Both start on
head, so comparing before the first move would report a cycle for every list. - Return
truewhen they are the same node. - Return
falseafter the loop. The loop only ends when the fast pointer reached the end of the list.
The solution in C#
class Solution {
public bool HasCycle(ListNode head) {
ListNode temp = head;
ListNode temp1 = head;
if(head == null || head.next == null){
return false;
}
while (temp1 != null && temp1.next != null){
temp1 = temp1.next.next;
temp = temp.next;
if(temp == temp1){
return true;
}
}
return false;
}
}
The C# details worth knowing:
ListNodeis a class, so==compares references.temp == temp1is true only when both variables point at the very same object. A list[1, 1, 1, 1]without a cycle returnsfalse, because four different nodes that hold equal values are still four different objects. Comparingtemp.val == temp1.valinstead would be a bug.- Chained member access.
temp1.next.nextthrows aNullReferenceExceptioniftemp1.nextisnull. The loop condition exists to make sure that never happens. - Short-circuit operators. In
temp1 != null && temp1.next != null, the right side runs only when the left side is true. Swapping the two operands would crash on anullfast pointer. - Self-loop. A single node whose
nextis itself passes the guard, becausehead.nextis notnull. In the loop both pointers land on that node, so the method returnstrue.
Complexity
Time: O(n). Without a cycle, the fast pointer reaches the end after about n / 2 rounds. With a cycle, the slow pointer enters the cycle after at most n steps, and once both pointers are inside, the fast one closes the gap within one lap of the cycle. The total number of rounds stays proportional to the number of nodes.
Space: O(1). The method keeps two references and nothing else, whatever the length of the list. This is the improvement over a hash set of visited nodes.
Edge cases and common mistakes
- Empty list.
head == nullreturnsfalse. The loop condition would also handle it, so the guard is a clear early exit rather than a necessity. - One node, no cycle. Returns
false. One node pointing at itself returnstrue. - Two nodes forming a loop.
[1, 2]with the tail linked to the head returnstrue: after round 1 the slow pointer is on the second node and the fast pointer on the head, and after round 2 both are on the second node. - The tail links to the head.
[1, 2, 3, 4, 5]with the last node pointing at the first one returnstrue, as does a tail that links to itself. - Comparing values instead of nodes. Duplicate values are normal in a list, and they say nothing about a cycle.
- Checking equality before moving. Both pointers begin on the same node, so the comparison has to come after the moves.
- Only checking
temp1.next. When the list has an even number of nodes,temp1itself becomesnullafter a jump, so the loop condition has to test it too.
Testing
[3, 2, 0, -4]with the tail linked to index 1 givestrue.[1, 2]with the tail linked to index 0 givestrue.[1]with no link givesfalse.[1, 1, 1, 1]with no link givesfalse.[1, 2, 3, 4, 5]with the tail linked to index 4 (itself) givestrue.
