Files
codetutor/backend/data/questions/linked-list-cycle.yaml

216 lines
8.8 KiB
YAML

title: Linked List Cycle
slug: linked-list-cycle
difficulty: easy
leetcode_id: 141
leetcode_url: https://leetcode.com/problems/linked-list-cycle/
categories:
- linked-lists
- two-pointers
- hash-tables
patterns:
- slug: fast-slow-pointers
is_optimal: true
function_signature: "def has_cycle(head: ListNode | None) -> bool:"
test_cases:
visible:
- input: { head: [3, 2, 0, -4], pos: 1 }
expected: true
- input: { head: [1, 2], pos: 0 }
expected: true
- input: { head: [1], pos: -1 }
expected: false
hidden:
- input: { head: [], pos: -1 }
expected: false
- input: { head: [1, 2, 3, 4, 5], pos: -1 }
expected: false
- input: { head: [1, 2, 3, 4, 5], pos: 4 }
expected: true
- input: { head: [1, 2], pos: -1 }
expected: false
- input: { head: [1], pos: 0 }
expected: true
- input: { head: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], pos: 0 }
expected: true
description: |
Given `head`, the head of a linked list, determine if the linked list has a cycle in it.
There is a cycle in a linked list if there is some node in the list that can be reached again by continuously following the `next` pointer. Internally, `pos` is used to denote the index of the node that tail's `next` pointer is connected to. **Note that `pos` is not passed as a parameter**.
Return `true` *if there is a cycle in the linked list*. Otherwise, return `false`.
constraints: |
- `0 <= number of nodes <= 10^4`
- `-10^5 <= Node.val <= 10^5`
- `pos` is `-1` or a valid index in the linked list
examples:
- input: "head = [3,2,0,-4], pos = 1"
output: "true"
explanation: "There is a cycle in the linked list, where the tail connects to the 1st node (0-indexed)."
- input: "head = [1,2], pos = 0"
output: "true"
explanation: "There is a cycle in the linked list, where the tail connects to the 0th node."
- input: "head = [1], pos = -1"
output: "false"
explanation: "There is no cycle in the linked list."
explanation:
intuition: |
Imagine two runners on a circular track: one runs twice as fast as the other.
If the track is truly circular (has a cycle), the fast runner will eventually "lap" the slow runner and they'll meet. If the track has an end (no cycle), the fast runner will simply reach the finish line without ever meeting the slow runner again.
This is the core insight behind **Floyd's Cycle Detection Algorithm** (also called the "tortoise and hare" algorithm). We use two pointers moving at different speeds:
- The **slow pointer** moves one step at a time
- The **fast pointer** moves two steps at a time
If there's a cycle, the fast pointer will eventually catch up to the slow pointer from behind (they'll meet inside the cycle). If there's no cycle, the fast pointer will reach `null` and we know the list terminates.
Why does this work? Once both pointers enter the cycle, the fast pointer gains one node on the slow pointer with each iteration. Since the cycle has finite length, they're guaranteed to meet.
approach: |
We solve this using **Floyd's Cycle Detection (Fast-Slow Pointers)**:
**Step 1: Handle edge cases**
- If the list is empty (`head` is `null`) or has only one node with no cycle, return `false`
&nbsp;
**Step 2: Initialise two pointers**
- `slow`: Starts at `head`, moves one node at a time
- `fast`: Starts at `head`, moves two nodes at a time
&nbsp;
**Step 3: Traverse the list**
- While `fast` and `fast.next` are not `null`:
- Move `slow` forward by one: `slow = slow.next`
- Move `fast` forward by two: `fast = fast.next.next`
- If `slow == fast`, we've found a cycle — return `true`
&nbsp;
**Step 4: Return the result**
- If the loop exits (fast reached `null`), there's no cycle — return `false`
&nbsp;
The beauty of this approach is that it uses constant space while guaranteeing detection if a cycle exists.
common_pitfalls:
- title: Using Extra Space with Hash Set
description: |
A straightforward approach is to use a hash set to track visited nodes:
- Traverse the list, adding each node to a set
- If you encounter a node already in the set, there's a cycle
This works correctly with **O(n) time**, but uses **O(n) space** for the hash set. The follow-up asks for O(1) space, which the fast-slow pointer approach achieves.
wrong_approach: "Hash set to track visited nodes (O(n) space)"
correct_approach: "Fast-slow pointers (O(1) space)"
- title: Checking Node Values Instead of References
description: |
A common mistake is comparing `slow.val == fast.val` instead of `slow == fast`.
Node *values* can be duplicated (the constraint allows values from `-10^5` to `10^5`), but node *references* (memory addresses) are unique. Two different nodes might have the same value, so comparing values could give false positives.
Always compare the node references themselves, not their values.
wrong_approach: "Comparing slow.val == fast.val"
correct_approach: "Comparing slow == fast (reference equality)"
- title: Null Pointer Exceptions
description: |
When moving the fast pointer two steps, you must check both `fast` and `fast.next` before accessing `fast.next.next`.
If `fast` is `null`, accessing `fast.next` throws an error. If `fast.next` is `null`, accessing `fast.next.next` throws an error.
The loop condition `while fast and fast.next` ensures both checks are satisfied before moving.
wrong_approach: "Moving fast without null checks"
correct_approach: "Check fast and fast.next before moving"
key_takeaways:
- "**Floyd's algorithm**: The fast-slow pointer technique detects cycles in O(n) time and O(1) space — a fundamental pattern for linked list problems"
- "**Why they meet**: In a cycle, the fast pointer gains one position per iteration on the slow pointer, guaranteeing they meet within one cycle length"
- "**Reference vs value**: Always compare node references, not values, when checking for the same node"
- "**Foundation for harder problems**: This same technique extends to finding the cycle start point (LeetCode 142) and finding the middle of a linked list"
time_complexity: "O(n). In the worst case, both pointers traverse the entire list. If there's a cycle, they meet within O(n) steps."
space_complexity: "O(1). We only use two pointer variables (`slow` and `fast`), regardless of the list size."
solutions:
- approach_name: Floyd's Cycle Detection (Fast-Slow Pointers)
is_optimal: true
code: |
class ListNode:
def __init__(self, val: int = 0, next: 'ListNode | None' = None):
self.val = val
self.next = next
def has_cycle(head: ListNode | None) -> bool:
# Handle empty list
if not head:
return False
# Initialise slow and fast pointers
slow = head
fast = head
# Traverse until fast reaches the end
while fast and fast.next:
# Move slow one step
slow = slow.next
# Move fast two steps
fast = fast.next.next
# If they meet, there's a cycle
if slow == fast:
return True
# Fast reached the end — no cycle
return False
explanation: |
**Time Complexity:** O(n) — Each node is visited at most twice (once by slow, potentially twice by fast).
**Space Complexity:** O(1) — Only two pointer variables are used.
The fast pointer moves twice as fast as the slow pointer. If there's a cycle, the fast pointer will eventually catch up to the slow pointer inside the cycle. If there's no cycle, the fast pointer reaches the end.
- approach_name: Hash Set
is_optimal: false
code: |
class ListNode:
def __init__(self, val: int = 0, next: 'ListNode | None' = None):
self.val = val
self.next = next
def has_cycle(head: ListNode | None) -> bool:
# Track visited nodes
visited: set[ListNode] = set()
current = head
while current:
# If we've seen this node before, there's a cycle
if current in visited:
return True
# Mark this node as visited
visited.add(current)
current = current.next
# Reached the end — no cycle
return False
explanation: |
**Time Complexity:** O(n) — We traverse each node once.
**Space Complexity:** O(n) — We store up to n node references in the hash set.
This approach is intuitive: track every node you visit, and if you see the same node twice, there's a cycle. While correct, it uses extra space that the fast-slow pointer approach avoids.