Problem overview
The function has_cycle receives head, the first node of a linked list, and returns true if following the next links ever leads back to a node already visited. If the list ends, it returns false. The list can be empty, and it has at most 10,000 nodes.
For the values [3, 2, 0, -4] where the last node links back to the node at index 1, the answer is true: the walk goes 3, 2, 0, -4, 2, 0, -4 and so on without end. For [1] with no link back, the answer is false. The position of the link back is only a way of describing the test list. The function never receives it.
Approach: a tortoise and a hare
An obvious solution remembers every node it has seen and reports a cycle when a node repeats. That needs O(n) memory. The follow-up asks for O(1), and Floyd's tortoise and hare gets there with two pointers.
slow moves one node per turn and fast moves two. If the list has no cycle, fast reaches the end and the loop stops. If there is a cycle, both pointers eventually enter it and go round. Inside the cycle, each turn moves fast one node closer to slow (it gains one node per turn), so the gap between them shrinks by one each turn. It can't skip over zero, so the two pointers must land on the same node, and that is the signal.
Nodes that can be shared
In a cycle, two different nodes can have the same node as their next, or a pointer can come back to a node that something else already points at. A Box allows only one owner, so it can't describe that. In this lesson each node is an Rc<RefCell<ListNode>>, and Link is short for Option<Rc<RefCell<ListNode>>>. Rc is a reference-counted pointer: cloning it copies the pointer, not the node, and every clone leads to the same node. RefCell lets the node's fields be read or changed through a shared pointer, checking the rules while the program runs.
Step by step
- Create the two pointers.
slowishead.clone()andfastishead. Both point at the first node. - Loop while
fastcan jump twice. Bothfast.is_some()andSelf::next(&fast).is_some()must hold, so that two moves are possible.nextis a helper written last. - Move
slowone node withSelf::next(&slow). - Move
fasttwo nodes withSelf::next(&Self::next(&fast)). - Check for a meeting. Both pointers can be
NoneorSome, soif let (Some(s), Some(f)) = (&slow, &fast)opens a block that runs only when both stand on a node. - Compare the pointers with
Rc::ptr_eq(s, f)and returntrueif they are the same node. - Return
falseafter the loop. The loop only ends whenfastreached the end of the list, so there is no cycle. - Declare the helper
fn next(link: &Link) -> Link. - Write its body as one expression:
link.as_ref().and_then(...)with a closure that returnsnode.borrow().next.clone().
The solution in Rust
impl Solution {
fn has_cycle(head: Link) -> bool {
let mut slow = head.clone();
let mut fast = head;
while fast.is_some() && Self::next(&fast).is_some() {
slow = Self::next(&slow);
fast = Self::next(&Self::next(&fast));
if let (Some(s), Some(f)) = (&slow, &fast) {
if Rc::ptr_eq(s, f) {
return true;
}
}
}
false
}
fn next(link: &Link) -> Link {
link.as_ref().and_then(|node| node.borrow().next.clone())
}
}
The Rust details worth a look:
head.clone()copies the pointer. Cloning anOption<Rc<...>>clones theRcinside, which only bumps a counter. Bothslowandfastthen lead to the same first node, andheaditself is moved intofastbecause it isn't needed afterwards.Rc::ptr_eqcompares nodes, not values. Two different nodes can hold the same number, as in[1, 1, 1]with no cycle, which must givefalse.ptr_eqasks whether both pointers lead to the very same node.as_ref()andand_then.link.as_ref()turns&Option<Rc<...>>into anOptionof a reference, without moving anything.and_thenreturnsNoneif the link isNone, and otherwise the closure's result, which is itself anOption. Sonextof the end of the list isNone, andnextofNoneis alsoNone.node.borrow().next.clone().borrow()opens theRefCellfor reading. Thenextfield is aLink, and cloning it copies a pointer, so the list isn't changed. The borrow ends when the expression ends.- Passing
&fastlends the link. The helper takes&Link, sofastandslowstay usable after each call. - The
if leton a tuple of references. Matching(&slow, &fast)against(Some(s), Some(f))givessandfas references to theRcs, which is whatptr_eqtakes.
Complexity
Time: O(n). Without a cycle, fast reaches the end after about n / 2 turns. With a cycle, slow enters it after at most n steps, and the gap between the pointers then closes by one per turn, which is at most the cycle's length, so the total is still linear.
Space: O(1). The loop keeps two pointers. Cloning a link copies a pointer and doesn't allocate a node.
Edge cases and common mistakes
- An empty list.
headisNone, so the loop condition fails at once and the answer isfalse. - One node. With no link back,
Self::next(&fast)isNoneand the loop never runs:false. If the node links to itself, the first turn puts both pointers on it and the answer istrue. - Two nodes in a cycle.
[1, 2]with the tail linked to index 0 givestrue. - Checking only
fast.is_some().fastjumps twice, so its next node must exist too. Without that check, the second jump would walk past the end. - Comparing values. Equal values in different nodes are not a cycle. Compare pointers.
- Checking for a meeting before moving. At the start both pointers are on the same node, so a check at that moment would report a cycle in every non-empty list. The check comes after both pointers have moved.
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 back givesfalse.[1]linked to itself givestrue.[1, 1, 1]with no link back givesfalse.- An empty list gives
false.
