457. Circular Array Loop
Problem Description
You have a circular array of non-zero integers where each element tells you how many positions to jump from that index. Positive values mean jump forward, negative values mean jump backward. Since the array is circular, jumping past the end brings you to the beginning, and jumping before the start brings you to the end.
Your task is to determine if there exists a valid cycle in this array. A valid cycle must meet three conditions:
- It forms a loop: Starting from some index, following the jump instructions repeatedly must eventually return you to the starting index
- Consistent direction: All jumps in the cycle must be in the same direction (either all forward with positive values or all backward with negative values)
- Multiple elements: The cycle must contain more than one element (you can't have a cycle that just loops on itself)
For example, if nums[i] = 3
, you jump 3 positions forward from index i
. If nums[i] = -2
, you jump 2 positions backward from index i
.
The function should return true
if at least one valid cycle exists anywhere in the array, false
otherwise.
The solution uses a two-pointer approach (slow and fast pointers) to detect cycles, similar to Floyd's cycle detection algorithm. It checks each potential starting position, uses the two pointers to find if a cycle exists, and validates that the cycle meets all requirements. Once a path is explored, it marks those positions with 0 to avoid redundant checking.
Intuition
The key insight is recognizing that this is fundamentally a cycle detection problem in a directed graph where each index points to exactly one other index. This immediately brings to mind Floyd's cycle detection algorithm (the "tortoise and hare" approach).
Think of the array as a graph where each index is a node, and each element creates a directed edge to another node based on the jump value. Since each node has exactly one outgoing edge, any path we follow will either eventually cycle or terminate (which can't happen here since every element is non-zero and always points somewhere).
The challenge is that not all cycles are valid. We need cycles where:
- All jumps go in the same direction (all positive or all negative values)
- The cycle has more than one element (no self-loops)
This suggests a strategy: for each starting position, we can use two pointers moving at different speeds. If they meet, we've found a cycle. But we need additional checks:
- While moving the pointers, we verify that consecutive jumps maintain the same sign (positive or negative)
- When we find a cycle, we check it's not a self-loop by ensuring
slow != next(slow)
An optimization comes from realizing that once we've explored a path and determined it doesn't lead to a valid cycle, we never need to check any element on that path again as a starting point. Why? Because if starting from any point in that path could lead to a valid cycle, we would have already found it. So we can mark visited elements with 0
to skip them in future iterations.
The modulo arithmetic (i + nums[i] % n + n) % n
handles the circular nature of the array, ensuring we wrap around correctly for both positive and negative jumps.
Solution Approach
The implementation uses Floyd's cycle detection algorithm with additional validations for the specific requirements of this problem.
Core Components:
-
Next Position Calculation:
def next(i): return (i + nums[i] % n + n) % n
This helper function calculates the next index from position
i
. The formula(i + nums[i] % n + n) % n
handles both positive and negative jumps while ensuring the result stays within bounds[0, n-1]
. The% n
onnums[i]
prevents overflow, and addingn
before the final modulo handles negative values correctly. -
Main Loop - Testing Each Starting Position:
for i in range(n): if nums[i] == 0: continue
We iterate through each index as a potential starting point. Skip indices marked as
0
(already processed). -
Two-Pointer Cycle Detection:
slow, fast = i, next(i) while nums[slow] * nums[fast] > 0 and nums[slow] * nums[next(fast)] > 0:
- Initialize
slow
at the current index andfast
one step ahead - The condition
nums[slow] * nums[fast] > 0
ensures both pointers are in regions with the same sign (both positive or both negative) - The condition
nums[slow] * nums[next(fast)] > 0
pre-checks that fast's next position also maintains the same sign - If signs differ, the path cannot form a valid cycle
- Initialize
-
Cycle Validation:
if slow == fast: if slow != next(slow): return True break slow, fast = next(slow), next(next(fast))
- When pointers meet (
slow == fast
), we've detected a cycle - Check
slow != next(slow)
to ensure it's not a self-loop (cycle length > 1) - If valid, return
True
- Otherwise, move
slow
one step andfast
two steps for the next iteration
- When pointers meet (
-
Path Marking Optimization:
j = i while nums[j] * nums[next(j)] > 0: nums[j] = 0 j = next(j)
After exploring a path from index
i
that doesn't lead to a valid cycle, mark all elements in that path with0
. This prevents redundant checking since:- If a valid cycle existed in this path, we would have found it
- Any future path entering this marked path won't find a valid cycle either
The algorithm efficiently finds cycles in O(n)
time per starting position, but with the marking optimization, each element is visited at most once across all iterations, giving an overall O(n)
time complexity and O(1)
extra space.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the algorithm with the array nums = [2, -1, 1, 2, 2]
.
Initial Setup:
- Array length
n = 5
- We'll check each index as a potential starting point
Starting from index 0 (value = 2):
- Initialize:
slow = 0
,fast = next(0) = (0 + 2) % 5 = 2
- Check direction consistency:
nums[0] * nums[2] = 2 * 1 = 2 > 0
β (same sign)nums[0] * nums[next(2)] = 2 * nums[3] = 2 * 2 = 4 > 0
β
- Move pointers:
slow = next(0) = 2
,fast = next(next(2)) = next(3) = 0
- Check direction consistency:
nums[2] * nums[0] = 1 * 2 = 2 > 0
βnums[2] * nums[next(0)] = 1 * nums[2] = 1 * 1 = 1 > 0
β
- Move pointers:
slow = next(2) = 3
,fast = next(next(0)) = next(2) = 3
- Pointers meet!
slow == fast = 3
- Check if it's not a self-loop:
next(3) = (3 + 2) % 5 = 0 β 3
β - Valid cycle found! The cycle is: 0 β 2 β 3 β 0
The cycle 0 β 2 β 3 β 0
is valid because:
- It forms a loop (returns to starting point)
- All values are positive (consistent direction)
- It contains 3 elements (more than one)
What if we had started from index 1 (value = -1)?
- Initialize:
slow = 1
,fast = next(1) = (1 + (-1) + 5) % 5 = 0
- Check direction consistency:
nums[1] * nums[0] = -1 * 2 = -2 < 0
β (different signs)
- Direction changes, so no valid cycle from this path
- Mark path: Set
nums[1] = 0
to avoid rechecking
Example of invalid self-loop:
Consider if we had nums = [1, 2, 3, 4, -1]
and started from index 4:
next(4) = (4 + (-1) + 5) % 5 = 8 % 5 = 3
- Following the path: 4 β 3 β 2 β 0 β 1 β 3 (cycle detected at 3)
- But this mixes positive and negative values, so it would fail the direction check
The algorithm efficiently identifies the valid cycle while avoiding unnecessary rechecks through path marking.
Solution Implementation
1class Solution:
2 def circularArrayLoop(self, nums: List[int]) -> bool:
3 n = len(nums)
4
5 def get_next_index(current_index: int) -> int:
6 """Calculate the next index in the circular array."""
7 # Handle both positive and negative movements with proper wrapping
8 return (current_index + nums[current_index] % n + n) % n
9
10 # Try each position as a potential starting point for a cycle
11 for start_index in range(n):
12 # Skip already visited positions (marked as 0)
13 if nums[start_index] == 0:
14 continue
15
16 # Initialize slow and fast pointers for cycle detection
17 slow_pointer = start_index
18 fast_pointer = get_next_index(start_index)
19
20 # Continue while all movements are in the same direction
21 # nums[slow] * nums[fast] > 0 ensures same sign (same direction)
22 while (nums[slow_pointer] * nums[fast_pointer] > 0 and
23 nums[slow_pointer] * nums[get_next_index(fast_pointer)] > 0):
24
25 # Cycle detected
26 if slow_pointer == fast_pointer:
27 # Check if cycle length > 1 (not a self-loop)
28 if slow_pointer != get_next_index(slow_pointer):
29 return True
30 break
31
32 # Move pointers: slow moves one step, fast moves two steps
33 slow_pointer = get_next_index(slow_pointer)
34 fast_pointer = get_next_index(get_next_index(fast_pointer))
35
36 # Mark all positions in this path as visited (set to 0)
37 # This optimization prevents revisiting invalid paths
38 current_position = start_index
39 while nums[current_position] * nums[get_next_index(current_position)] > 0:
40 nums[current_position] = 0
41 current_position = get_next_index(current_position)
42
43 # No valid cycle found
44 return False
45
1class Solution {
2 private int arrayLength;
3 private int[] array;
4
5 /**
6 * Determines if there exists a circular loop in the array.
7 * A loop must:
8 * 1. Have more than one element
9 * 2. All elements in the loop must have the same direction (all positive or all negative)
10 *
11 * @param nums Array where nums[i] indicates the number of steps to jump from index i
12 * @return true if a valid circular loop exists, false otherwise
13 */
14 public boolean circularArrayLoop(int[] nums) {
15 arrayLength = nums.length;
16 this.array = nums;
17
18 // Try to find a loop starting from each position
19 for (int startIndex = 0; startIndex < arrayLength; startIndex++) {
20 // Skip positions already marked as invalid (value set to 0)
21 if (array[startIndex] == 0) {
22 continue;
23 }
24
25 // Use two pointers (slow and fast) to detect cycles
26 int slowPointer = startIndex;
27 int fastPointer = getNextIndex(startIndex);
28
29 // Continue while all elements have the same sign (same direction)
30 while (array[slowPointer] * array[fastPointer] > 0 &&
31 array[slowPointer] * array[getNextIndex(fastPointer)] > 0) {
32
33 // Cycle detected when slow and fast pointers meet
34 if (slowPointer == fastPointer) {
35 // Check if the loop has more than one element
36 if (slowPointer != getNextIndex(slowPointer)) {
37 return true;
38 }
39 break;
40 }
41
42 // Move slow pointer one step and fast pointer two steps
43 slowPointer = getNextIndex(slowPointer);
44 fastPointer = getNextIndex(getNextIndex(fastPointer));
45 }
46
47 // Mark all elements in the current path as invalid (set to 0)
48 // since they don't form a valid loop
49 int currentIndex = startIndex;
50 while (array[currentIndex] * array[getNextIndex(currentIndex)] > 0) {
51 array[currentIndex] = 0;
52 currentIndex = getNextIndex(currentIndex);
53 }
54 }
55
56 return false;
57 }
58
59 /**
60 * Calculates the next index in the circular array based on the jump value.
61 * Handles both positive and negative jumps with proper wrapping.
62 *
63 * @param currentIndex The current position in the array
64 * @return The next index after jumping from the current position
65 */
66 private int getNextIndex(int currentIndex) {
67 // Calculate next position with proper modulo handling for negative values
68 // First modulo ensures the jump value is within bounds
69 // Adding arrayLength and second modulo handles negative results
70 return (currentIndex + array[currentIndex] % arrayLength + arrayLength) % arrayLength;
71 }
72}
73
1class Solution {
2public:
3 bool circularArrayLoop(vector<int>& nums) {
4 int n = nums.size();
5
6 // Try each position as a potential starting point for a cycle
7 for (int i = 0; i < n; ++i) {
8 // Skip if already visited (marked as 0)
9 if (!nums[i]) continue;
10
11 // Initialize slow and fast pointers for cycle detection
12 int slow = i;
13 int fast = getNextIndex(nums, i);
14
15 // Continue while all movements are in the same direction
16 // Check: current slow direction matches fast direction
17 // AND current slow direction matches next position after fast
18 while (nums[slow] * nums[fast] > 0 &&
19 nums[slow] * nums[getNextIndex(nums, fast)] > 0) {
20
21 // Cycle detected when slow meets fast
22 if (slow == fast) {
23 // Verify it's not a single-element cycle
24 if (slow != getNextIndex(nums, slow)) {
25 return true;
26 }
27 break;
28 }
29
30 // Move slow pointer one step
31 slow = getNextIndex(nums, slow);
32 // Move fast pointer two steps
33 fast = getNextIndex(nums, getNextIndex(nums, fast));
34 }
35
36 // Mark all elements in this path as visited (set to 0)
37 // This optimization prevents revisiting invalid paths
38 int currentIndex = i;
39 while (nums[currentIndex] * nums[getNextIndex(nums, currentIndex)] > 0) {
40 nums[currentIndex] = 0;
41 currentIndex = getNextIndex(nums, currentIndex);
42 }
43 }
44
45 return false;
46 }
47
48private:
49 // Calculate the next index in the circular array
50 // Handles both positive and negative jumps with wraparound
51 int getNextIndex(vector<int>& nums, int currentIndex) {
52 int n = nums.size();
53 // Use modulo arithmetic to handle circular wraparound
54 // Add n before final modulo to handle negative values correctly
55 return (currentIndex + nums[currentIndex] % n + n) % n;
56 }
57};
58
1/**
2 * Detects if there exists a circular loop in the array following movement rules
3 * @param nums - Array where each element represents jump distance and direction
4 * @returns true if a valid circular loop exists, false otherwise
5 */
6function circularArrayLoop(nums: number[]): boolean {
7 const n = nums.length;
8
9 // Try each position as a potential starting point for a cycle
10 for (let i = 0; i < n; i++) {
11 // Skip if already visited (marked as 0)
12 if (nums[i] === 0) continue;
13
14 // Initialize slow and fast pointers for cycle detection
15 let slow = i;
16 let fast = getNextIndex(nums, i);
17
18 // Continue while all movements are in the same direction
19 // Check: current slow direction matches fast direction
20 // AND current slow direction matches next position after fast
21 while (nums[slow] * nums[fast] > 0 &&
22 nums[slow] * nums[getNextIndex(nums, fast)] > 0) {
23
24 // Cycle detected when slow meets fast
25 if (slow === fast) {
26 // Verify it's not a single-element cycle
27 if (slow !== getNextIndex(nums, slow)) {
28 return true;
29 }
30 break;
31 }
32
33 // Move slow pointer one step
34 slow = getNextIndex(nums, slow);
35 // Move fast pointer two steps
36 fast = getNextIndex(nums, getNextIndex(nums, fast));
37 }
38
39 // Mark all elements in this path as visited (set to 0)
40 // This optimization prevents revisiting invalid paths
41 let currentIndex = i;
42 while (nums[currentIndex] * nums[getNextIndex(nums, currentIndex)] > 0) {
43 const nextIndex = getNextIndex(nums, currentIndex);
44 nums[currentIndex] = 0;
45 currentIndex = nextIndex;
46 }
47 }
48
49 return false;
50}
51
52/**
53 * Calculate the next index in the circular array
54 * Handles both positive and negative jumps with wraparound
55 * @param nums - The array containing jump values
56 * @param currentIndex - Current position in the array
57 * @returns The next index after applying the jump
58 */
59function getNextIndex(nums: number[], currentIndex: number): number {
60 const n = nums.length;
61 // Use modulo arithmetic to handle circular wraparound
62 // Add n before final modulo to handle negative values correctly
63 return ((currentIndex + nums[currentIndex]) % n + n) % n;
64}
65
Time and Space Complexity
Time Complexity: O(n)
The algorithm uses a two-pointer (slow and fast) approach to detect cycles. For each starting position i
:
- The inner while loop runs at most
O(n)
times before either finding a cycle or breaking when the direction changes - After processing each starting position, we mark visited elements as
0
to avoid reprocessing them - The marking phase (second inner while loop) also runs at most
O(n)
times total across all iterations
Since each element is visited and processed at most twice (once in cycle detection and once in marking), and we have n
starting positions, the overall time complexity is O(n)
.
Space Complexity: O(1)
The algorithm modifies the input array in-place by setting visited elements to 0
, but doesn't use any additional data structures that scale with input size. Only a constant amount of extra space is used for variables like slow
, fast
, i
, j
, and n
.
Common Pitfalls
1. Incorrect Modulo Operation for Negative Numbers
The Pitfall: When calculating the next position with negative jumps, a naive implementation might write:
def get_next_index(current_index: int) -> int:
return (current_index + nums[current_index]) % n
This fails for negative jumps because in Python, -1 % 5 = 4
, but -6 % 5 = 4
as well. However, if nums[current_index]
is a large negative number like -10
at index 2
in an array of size 5
, we get (2 + (-10)) % 5 = -8 % 5 = 2
, which incorrectly suggests we stay at the same position.
The Solution:
Always add n
before taking the final modulo to ensure positive results:
def get_next_index(current_index: int) -> int:
return (current_index + nums[current_index] % n + n) % n
The % n
on nums[current_index]
first normalizes large jumps, then adding n
ensures the result is positive before the final modulo.
2. Forgetting to Check Fast Pointer's Next Position
The Pitfall:
Only checking if nums[slow] * nums[fast] > 0
without verifying nums[next(fast)]
:
# Incorrect - might move fast into opposite direction while nums[slow_pointer] * nums[fast_pointer] > 0: # ... rest of the code
This could cause the fast pointer to jump into a region with opposite sign values in its next move, violating the consistent direction requirement.
The Solution: Always pre-check the fast pointer's next position to ensure it maintains the same direction:
while (nums[slow_pointer] * nums[fast_pointer] > 0 and nums[slow_pointer] * nums[get_next_index(fast_pointer)] > 0):
3. Not Handling Self-Loops Correctly
The Pitfall: Accepting any cycle without verifying its length:
if slow_pointer == fast_pointer: return True # Wrong! Could be a self-loop
An element that jumps to itself (e.g., nums[2] = 3
in an array of size 3) would be incorrectly identified as a valid cycle.
The Solution: Explicitly check that the cycle contains more than one element:
if slow_pointer == fast_pointer: if slow_pointer != get_next_index(slow_pointer): return True
4. Inefficient Path Marking
The Pitfall: Not marking explored paths or marking them incorrectly:
# Forgetting to mark visited paths
for start_index in range(n):
# ... cycle detection logic
# No marking of visited elements
This causes the algorithm to repeatedly check paths that have already been determined to not contain valid cycles, degrading performance to O(nΒ²).
The Solution: Mark all elements in an invalid path with 0 after exploration:
current_position = start_index while nums[current_position] * nums[get_next_index(current_position)] > 0: next_pos = get_next_index(current_position) nums[current_position] = 0 # Mark as visited current_position = next_pos
5. Array Modification Side Effects
The Pitfall: The solution modifies the input array by setting visited elements to 0. This might be unexpected behavior if the caller needs the original array preserved.
The Solution: If preserving the original array is required, create a separate visited array:
visited = [False] * n # Use visited[i] instead of checking nums[i] == 0 # Mark with visited[i] = True instead of nums[i] = 0
Or make a copy of the array at the beginning if space complexity O(n) is acceptable.
Which of these pictures shows the visit order of a depth-first search?
Recommended Readings
Tech Interview Pattern Two Pointers Introduction If you prefer videos here's a super quick introduction to Two Pointers div class responsive iframe iframe src https www youtube com embed xZ4AfXHQ1VQ title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture allowfullscreen iframe div Two pointers is a common interview
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Donβt Miss This!