215 lines
8.8 KiB
YAML
215 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:
|
|
- fast-slow-pointers
|
|
|
|
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`
|
|
|
|
|
|
|
|
**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
|
|
|
|
|
|
|
|
**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`
|
|
|
|
|
|
|
|
**Step 4: Return the result**
|
|
|
|
- If the loop exits (fast reached `null`), there's no cycle — return `false`
|
|
|
|
|
|
|
|
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.
|