LeetCode 457 - Circular Array Loop
This problem asks us to determine whether a circular array contains a valid cycle under a specific movement rule. Each element in the array represents how far we move from the current index.
Difficulty: 🟡 Medium
Topics: Array, Hash Table, Two Pointers
Solution
Problem Understanding
This problem asks us to determine whether a circular array contains a valid cycle under a specific movement rule. Each element in the array represents how far we move from the current index. Positive numbers move forward, negative numbers move backward, and movement wraps around because the array is circular.
The key requirement is that a valid cycle must satisfy two conditions:
- Every movement in the cycle must go in the same direction. All values must either be positive or all negative.
- The cycle length must be greater than one. A single element pointing to itself does not count.
The input array nums contains non zero integers. Since the array is circular, moving past the last index wraps back to the beginning, and moving before index 0 wraps to the end.
For example, in the array [2, -1, 1, 2, 2]:
- Starting at index
0, move2steps to index2 - From index
2, move1step to index3 - From index
3, move2steps to index0
This creates the cycle 0 -> 2 -> 3 -> 0, and every value involved is positive, so the answer is true.
The constraints are important:
nums.length <= 5000- Values are between
-1000and1000 - No value is zero
The follow up asks for O(n) time and O(1) extra space, which strongly suggests we should avoid repeatedly revisiting the same indices and avoid using extra data structures like hash sets for every traversal.
Several edge cases are important:
- A self loop such as index
ipointing back to itself is invalid because cycle length must exceed one. - A path that changes direction midway is invalid.
- Arrays containing only one element can never contain a valid cycle.
- Large jump values must still wrap correctly using modular arithmetic.
- Negative modulo handling must be implemented carefully.
Approaches
Brute Force Approach
The brute force strategy is to try every index as a starting point and simulate movement step by step.
For each starting index:
- Keep track of visited indices for this traversal.
- Determine the required direction from the starting value.
- Continue moving while:
- The direction remains consistent
- We have not revisited an index
- If we revisit an index already seen in this traversal, check whether the cycle length is greater than one.
This approach works because it explicitly explores every reachable path and validates all cycle conditions.
However, it is inefficient because many traversals repeatedly visit the same indices. In the worst case, each starting position may traverse nearly the entire array.
This leads to O(n²) time complexity.
Optimal Approach
The optimal solution uses Floyd's Cycle Detection Algorithm, also known as the tortoise and hare technique.
The important insight is that the array can be viewed as a directed graph where each index points to exactly one next index.
Cycle detection naturally fits Floyd's algorithm:
- A slow pointer moves one step at a time
- A fast pointer moves two steps at a time
- If they meet, a cycle exists
However, we must add extra validation because not every graph cycle is valid for this problem.
We must ensure:
- All movements remain in the same direction
- The cycle length is greater than one
To achieve O(n) complexity overall, we also mark explored paths as visited by setting traversed elements to 0. Since the input guarantees no original value is zero, zero becomes a safe marker indicating that the index has already been processed and cannot contribute to a valid cycle.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n²) | O(n) | Simulates traversal from every index using a visited set |
| Optimal | O(n) | O(1) | Uses Floyd cycle detection with in place marking |
Algorithm Walkthrough
- Iterate through every index in the array.
Each index could potentially belong to a valid cycle, so we must examine all unprocessed positions.
2. Skip indices already marked as 0.
We use 0 as a marker for indices already proven incapable of forming a valid cycle. Since original values are never zero, this is safe.
3. Determine the movement direction.
If nums[i] > 0, the traversal direction is forward. Otherwise, it is backward.
4. Initialize slow and fast pointers.
Both pointers begin at the current starting index. 5. Advance the pointers while movement remains valid.
A movement is valid only if:
- The next index maintains the same direction
- The traversal does not encounter a zero marker
The slow pointer advances one step.
The fast pointer advances two steps. 6. Check whether slow and fast pointers meet.
If they meet, a cycle may exist. 7. Reject self loops.
If the next index from the meeting point is the same index itself, then the cycle length is one, which is invalid.
8. Return true if a valid cycle is found.
Once a non trivial same direction cycle is detected, the problem is solved. 9. Mark the traversal path as visited if no valid cycle exists.
Starting from the current index, continue following the path while direction remains consistent, setting each visited element to 0.
This ensures future traversals do not repeat the same work. 10. Continue until all indices are processed.
If no valid cycle is found after processing the entire array, return false.
Why it works
Floyd's algorithm guarantees that if a cycle exists in a directed graph, slow and fast pointers will eventually meet. The additional direction checks ensure that only cycles with consistent movement direction are considered valid. The self loop check excludes cycles of length one. Finally, marking failed paths as zero guarantees that every index is processed at most once, which ensures linear time complexity.
Python Solution
from typing import List
class Solution:
def circularArrayLoop(self, nums: List[int]) -> bool:
n = len(nums)
def next_index(current: int) -> int:
return (current + nums[current]) % n
for i in range(n):
if nums[i] == 0:
continue
direction = nums[i] > 0
slow = i
fast = i
while True:
next_slow = next_index(slow)
if nums[next_slow] == 0 or (nums[next_slow] > 0) != direction:
break
next_fast = next_index(fast)
if nums[next_fast] == 0 or (nums[next_fast] > 0) != direction:
break
next_fast = next_index(next_fast)
if nums[next_fast] == 0 or (nums[next_fast] > 0) != direction:
break
slow = next_slow
fast = next_fast
if slow == fast:
if slow == next_index(slow):
break
return True
current = i
while nums[current] != 0 and (nums[current] > 0) == direction:
next_pos = next_index(current)
nums[current] = 0
current = next_pos
return False
The implementation begins by defining a helper function next_index, which computes the next position while correctly handling circular wrapping using modulo arithmetic.
The main loop iterates through every index. If an index has already been marked as 0, it is skipped because it has already been fully processed.
For each unprocessed index, the algorithm determines the traversal direction from the sign of the current value.
The slow and fast pointers are then initialized. Inside the loop, both pointers move only if direction consistency is maintained. This prevents invalid mixed direction cycles from being considered.
When the slow and fast pointers meet, the algorithm checks whether the cycle length is greater than one. A self loop is rejected by verifying whether the next index equals the current index.
If no valid cycle is found from the current start, the traversal path is cleaned up by marking all reachable same direction nodes as 0. This prevents repeated work and guarantees linear runtime.
Go Solution
func circularArrayLoop(nums []int) bool {
n := len(nums)
nextIndex := func(current int) int {
return ((current + nums[current]) % n + n) % n
}
for i := 0; i < n; i++ {
if nums[i] == 0 {
continue
}
direction := nums[i] > 0
slow := i
fast := i
for {
nextSlow := nextIndex(slow)
if nums[nextSlow] == 0 || (nums[nextSlow] > 0) != direction {
break
}
nextFast := nextIndex(fast)
if nums[nextFast] == 0 || (nums[nextFast] > 0) != direction {
break
}
nextFast = nextIndex(nextFast)
if nums[nextFast] == 0 || (nums[nextFast] > 0) != direction {
break
}
slow = nextSlow
fast = nextFast
if slow == fast {
if slow == nextIndex(slow) {
break
}
return true
}
}
current := i
for nums[current] != 0 && (nums[current] > 0) == direction {
nextPos := nextIndex(current)
nums[current] = 0
current = nextPos
}
}
return false
}
The Go implementation closely mirrors the Python solution. One important difference is modulo handling. Go can produce negative modulo results, so the expression:
((current + nums[current]) % n + n) % n
ensures the final index is always non negative.
Go slices are used directly, and no extra memory structures are allocated, preserving the O(1) space requirement.
Worked Examples
Example 1
Input:
nums = [2, -1, 1, 2, 2]
Initial State
| Index | Value |
|---|---|
| 0 | 2 |
| 1 | -1 |
| 2 | 1 |
| 3 | 2 |
| 4 | 2 |
Start at index 0.
Direction is positive.
| Step | Slow | Fast | Explanation |
|---|---|---|---|
| Initial | 0 | 0 | Both start at index 0 |
| 1 | 2 | 3 | Slow moves once, fast moves twice |
| 2 | 3 | 2 | Continue moving |
| 3 | 0 | 0 | Pointers meet |
Now check whether this is a self loop.
next_index(0) = 2
Since the next index differs from the current index, the cycle length exceeds one.
Return true.
Example 2
Input:
nums = [-1, -2, -3, -4, -5, 6]
Traversal
Starting at index 0:
| Step | Slow | Fast |
|---|---|---|
| Initial | 0 | 0 |
| 1 | 5 | 4 |
| 2 | 5 | 5 |
Pointers meet at index 5.
Check self loop:
next_index(5) = 5
This is a cycle of length one, which is invalid.
The algorithm marks the explored path as zero and continues searching.
No valid cycle exists.
Return false.
Example 3
Input:
nums = [1, -1, 5, 1, 4]
Starting at index 0:
| Step | Slow | Fast | Notes |
|---|---|---|---|
| Initial | 0 | 0 | Direction positive |
| 1 | 1 | 0 | Fast pointer encounters direction mismatch |
This path is invalid because direction changes.
Now start at index 3.
| Step | Slow | Fast |
|---|---|---|
| Initial | 3 | 3 |
| 1 | 4 | 3 |
| 2 | 3 | 3 |
Pointers meet.
Check self loop:
next_index(3) = 4
Not a self loop.
Return true.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Each index is processed and marked at most once |
| Space | O(1) | Only a few pointers and variables are used |
Although Floyd's cycle detection may appear nested, the cleanup phase guarantees that every index becomes zero after being processed unsuccessfully. Therefore, no index is revisited excessively, and the total work across all traversals remains linear.
Test Cases
from typing import List
class Solution:
def circularArrayLoop(self, nums: List[int]) -> bool:
n = len(nums)
def next_index(current: int) -> int:
return (current + nums[current]) % n
for i in range(n):
if nums[i] == 0:
continue
direction = nums[i] > 0
slow = i
fast = i
while True:
next_slow = next_index(slow)
if nums[next_slow] == 0 or (nums[next_slow] > 0) != direction:
break
next_fast = next_index(fast)
if nums[next_fast] == 0 or (nums[next_fast] > 0) != direction:
break
next_fast = next_index(next_fast)
if nums[next_fast] == 0 or (nums[next_fast] > 0) != direction:
break
slow = next_slow
fast = next_fast
if slow == fast:
if slow == next_index(slow):
break
return True
current = i
while nums[current] != 0 and (nums[current] > 0) == direction:
next_pos = next_index(current)
nums[current] = 0
current = next_pos
return False
solver = Solution()
assert solver.circularArrayLoop([2, -1, 1, 2, 2]) is True # Valid positive cycle
assert solver.circularArrayLoop([-1, -2, -3, -4, -5, 6]) is False # Self loop only
assert solver.circularArrayLoop([1, -1, 5, 1, 4]) is True # Mixed invalid cycle plus valid cycle
assert solver.circularArrayLoop([1]) is False # Single element self loop
assert solver.circularArrayLoop([1, 1, 1, 1]) is True # Entire array forms cycle
assert solver.circularArrayLoop([-1, -1, -1]) is True # Negative direction cycle
assert solver.circularArrayLoop([2, 2, -1, 2]) is True # Positive cycle exists
assert solver.circularArrayLoop([1, -1]) is False # Direction changes immediately
assert solver.circularArrayLoop([3, 1, 2]) is True # Wrapping cycle
assert solver.circularArrayLoop([-2, 1, -1, -2, -2]) is False # Direction mismatch prevents cycle
Test Summary
| Test | Why |
|---|---|
[2, -1, 1, 2, 2] |
Standard valid cycle example |
[-1, -2, -3, -4, -5, 6] |
Self loop should be rejected |
[1, -1, 5, 1, 4] |
Mixed directions plus valid cycle |
[1] |
Single element edge case |
[1, 1, 1, 1] |
Entire array forms one cycle |
[-1, -1, -1] |
Valid negative cycle |
[2, 2, -1, 2] |
Cycle exists despite unrelated negative value |
[1, -1] |
Immediate direction conflict |
[3, 1, 2] |
Large jumps with wrapping |
[-2, 1, -1, -2, -2] |
Invalid because of direction inconsistency |
Edge Cases
Self Loops
A major source of bugs is treating a single index pointing to itself as a valid cycle. For example:
[1]
Index 0 moves back to itself immediately. Although this technically forms a cycle in graph theory, the problem explicitly requires cycle length greater than one.
The implementation handles this with:
if slow == next_index(slow):
break
This rejects all self loops.
Direction Changes
Another tricky case occurs when movement direction changes inside a traversal.
For example:
[1, -1]
Index 0 moves forward to index 1, but index 1 moves backward. This violates the same direction requirement.
The implementation validates direction before every pointer movement:
(nums[next_index] > 0) == direction
If the sign changes, traversal immediately stops.
Large Circular Wraps
Large jumps may wrap around the array multiple times.
For example:
[7, 1, 1, 1, 1]
Since the array length is 5, moving 7 steps effectively means moving 2 steps.
The implementation safely handles wrapping using modulo arithmetic:
(current + nums[current]) % n
This guarantees the next index always remains within valid bounds.