Facebook Pixel

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:

  1. It forms a loop: Starting from some index, following the jump instructions repeatedly must eventually return you to the starting index
  2. 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)
  3. 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. 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 on nums[i] prevents overflow, and adding n before the final modulo handles negative values correctly.

  2. 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).

  3. 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 and fast 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
  4. 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 and fast two steps for the next iteration
  5. 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 with 0. 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 Evaluator

Example 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):

  1. Initialize: slow = 0, fast = next(0) = (0 + 2) % 5 = 2
  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 βœ“
  3. Move pointers: slow = next(0) = 2, fast = next(next(2)) = next(3) = 0
  4. Check direction consistency:
    • nums[2] * nums[0] = 1 * 2 = 2 > 0 βœ“
    • nums[2] * nums[next(0)] = 1 * nums[2] = 1 * 1 = 1 > 0 βœ“
  5. Move pointers: slow = next(2) = 3, fast = next(next(0)) = next(2) = 3
  6. Pointers meet! slow == fast = 3
  7. Check if it's not a self-loop: next(3) = (3 + 2) % 5 = 0 β‰  3 βœ“
  8. 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)?

  1. Initialize: slow = 1, fast = next(1) = (1 + (-1) + 5) % 5 = 0
  2. Check direction consistency:
    • nums[1] * nums[0] = -1 * 2 = -2 < 0 βœ— (different signs)
  3. Direction changes, so no valid cycle from this path
  4. 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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of these pictures shows the visit order of a depth-first search?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More