Facebook Pixel

3488. Closest Equal Element Queries

Problem Description

You have a circular array nums where the last element connects back to the first element. Given this array and a list of queries, for each query index queries[i], you need to find the minimum distance from position queries[i] to any other position j in the circular array where nums[j] has the same value as nums[queries[i]].

The distance in a circular array can be calculated in two directions:

  • Moving forward (clockwise): from position i to position j
  • Moving backward (counterclockwise): from position i to position j

The minimum of these two distances is the answer for that query.

Key points:

  • The array is circular, meaning after the last element, you wrap around to the first element
  • You're looking for other positions with the same value, not including the query position itself
  • If no other position contains the same value as nums[queries[i]], return -1 for that query
  • Return an array answer where answer[i] is the result for queries[i]

Example understanding: If nums = [1, 2, 3, 2] and queries = [1]:

  • At index 1, we have value 2
  • Another 2 exists at index 3
  • The forward distance is 2 (from index 1 to 3)
  • The backward distance (going circular) is 2 (from index 1 to 3 via 0, 4, 3)
  • The minimum distance is 2
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is that in a circular array, finding the minimum distance to another identical element requires checking both directions - forward and backward. However, dealing with circular wraparound can be tricky.

To handle the circular nature elegantly, we can double the array. If our original array is [1, 2, 3, 2], we create [1, 2, 3, 2, 1, 2, 3, 2]. This transformation allows us to treat the circular distance problem as a linear one - any circular path in the original array becomes a straight path in the doubled array.

For each position in this doubled array, we need to find the closest identical element. We can approach this from two directions:

  1. Looking left: For each position, find the closest identical element that appeared before it
  2. Looking right: For each position, find the closest identical element that will appear after it

We use hash tables to track:

  • left: stores the most recent position where each value appeared as we scan from left to right
  • right: stores the nearest future position where each value will appear as we scan from right to left

For each position i in the doubled array:

  • When scanning left to right, if we've seen nums[i] before at position left[nums[i]], the distance is i - left[nums[i]]
  • When scanning right to left, if we'll see nums[i] again at position right[nums[i]], the distance is right[nums[i]] - i
  • We take the minimum of these two distances

Finally, since we doubled the array, each original index i now appears at both position i and position i + n. We take the minimum distance calculated for both positions. If this minimum distance is greater than or equal to n (the original array length), it means the nearest identical element is actually the same element itself (after going full circle), so we return -1.

Learn more about Binary Search patterns.

Solution Approach

The implementation follows the intuition of doubling the array and tracking positions with hash tables:

Step 1: Initialize the data structures

  • Create a distance array d of size m = 2n (where n is the original array length), initialized with m (a large value)
  • Create two hash tables: left and right to track element positions

Step 2: Left-to-right scan to find previous occurrences

for i in range(m):
    x = nums[i % n]  # Get element from circular array
    if x in left:
        d[i] = min(d[i], i - left[x])  # Update minimum distance
    left[x] = i  # Update last seen position
  • For each position i in the doubled array, we check if the current element x has appeared before
  • If yes, calculate the distance i - left[x] and update d[i] if this distance is smaller
  • Update the hash table to record this position as the new "last seen" position for element x

Step 3: Right-to-left scan to find next occurrences

for i in range(m - 1, -1, -1):
    x = nums[i % n]
    if x in right:
        d[i] = min(d[i], right[x] - i)  # Update minimum distance
    right[x] = i  # Update next occurrence position
  • Similar to the left scan, but moving from right to left
  • For each position, check if the element will appear again to the right
  • Calculate distance right[x] - i and keep the minimum

Step 4: Combine results for original positions

for i in range(n):
    d[i] = min(d[i], d[i + n])
  • Since each original index appears twice in the doubled array (at positions i and i + n), we take the minimum of both
  • This ensures we capture the true minimum distance considering the circular nature

Step 5: Process queries and return results

return [-1 if d[i] >= n else d[i] for i in queries]
  • For each query index i, check the minimum distance stored in d[i]
  • If the distance is greater than or equal to n, it means no other identical element exists (the closest one is itself after a full circle), so return -1
  • Otherwise, return the actual minimum distance

The time complexity is O(n + q) where n is the array length and q is the number of queries. The space complexity is O(n) for the hash tables and distance array.

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 work through a concrete example with nums = [2, 1, 3, 1, 2] and queries = [0, 3].

Step 1: Double the array

  • Original: [2, 1, 3, 1, 2] (indices 0-4)
  • Doubled: [2, 1, 3, 1, 2, 2, 1, 3, 1, 2] (indices 0-9)
  • Initialize distance array d = [10, 10, 10, 10, 10, 10, 10, 10, 10, 10]

Step 2: Left-to-right scan

  • i=0: x=2, left={}, no previous 2, d[0]=10, left={2:0}
  • i=1: x=1, left={2:0}, no previous 1, d[1]=10, left={2:0, 1:1}
  • i=2: x=3, left={2:0, 1:1}, no previous 3, d[2]=10, left={2:0, 1:1, 3:2}
  • i=3: x=1, left={2:0, 1:1, 3:2}, previous 1 at index 1, d[3]=min(10, 3-1)=2, left={2:0, 1:3, 3:2}
  • i=4: x=2, left={2:0, 1:3, 3:2}, previous 2 at index 0, d[4]=min(10, 4-0)=4, left={2:4, 1:3, 3:2}
  • i=5: x=2, left={2:4, 1:3, 3:2}, previous 2 at index 4, d[5]=min(10, 5-4)=1, left={2:5, 1:3, 3:2}
  • i=6: x=1, left={2:5, 1:3, 3:2}, previous 1 at index 3, d[6]=min(10, 6-3)=3, left={2:5, 1:6, 3:2}
  • i=7: x=3, left={2:5, 1:6, 3:2}, previous 3 at index 2, d[7]=min(10, 7-2)=5, left={2:5, 1:6, 3:7}
  • i=8: x=1, left={2:5, 1:6, 3:7}, previous 1 at index 6, d[8]=min(10, 8-6)=2, left={2:5, 1:8, 3:7}
  • i=9: x=2, left={2:5, 1:8, 3:7}, previous 2 at index 5, d[9]=min(10, 9-5)=4, left={2:9, 1:8, 3:7}

After left scan: d = [10, 10, 10, 2, 4, 1, 3, 5, 2, 4]

Step 3: Right-to-left scan

  • i=9: x=2, right={}, no next 2, d[9]=4, right={2:9}
  • i=8: x=1, right={2:9}, no next 1, d[8]=2, right={2:9, 1:8}
  • i=7: x=3, right={2:9, 1:8}, no next 3, d[7]=5, right={2:9, 1:8, 3:7}
  • i=6: x=1, right={2:9, 1:8, 3:7}, next 1 at index 8, d[6]=min(3, 8-6)=2, right={2:9, 1:6, 3:7}
  • i=5: x=2, right={2:9, 1:6, 3:7}, next 2 at index 9, d[5]=min(1, 9-5)=1, right={2:5, 1:6, 3:7}
  • i=4: x=2, right={2:5, 1:6, 3:7}, next 2 at index 5, d[4]=min(4, 5-4)=1, right={2:4, 1:6, 3:7}
  • i=3: x=1, right={2:4, 1:6, 3:7}, next 1 at index 6, d[3]=min(2, 6-3)=2, right={2:4, 1:3, 3:7}
  • i=2: x=3, right={2:4, 1:3, 3:7}, next 3 at index 7, d[2]=min(10, 7-2)=5, right={2:4, 1:3, 3:2}
  • i=1: x=1, right={2:4, 1:3, 3:2}, next 1 at index 3, d[1]=min(10, 3-1)=2, right={2:4, 1:1, 3:2}
  • i=0: x=2, right={2:4, 1:1, 3:2}, next 2 at index 4, d[0]=min(10, 4-0)=4, right={2:0, 1:1, 3:2}

After right scan: d = [4, 2, 5, 2, 1, 1, 2, 5, 2, 4]

Step 4: Combine for original positions

  • d[0] = min(d[0], d[5]) = min(4, 1) = 1
  • d[1] = min(d[1], d[6]) = min(2, 2) = 2
  • d[2] = min(d[2], d[7]) = min(5, 5) = 5
  • d[3] = min(d[3], d[8]) = min(2, 2) = 2
  • d[4] = min(d[4], d[9]) = min(1, 4) = 1

Final d for original indices: [1, 2, 5, 2, 1]

Step 5: Process queries

  • Query 0: d[0] = 1, which is < 5, so answer is 1 (distance from index 0 to index 4)
  • Query 3: d[3] = 2, which is < 5, so answer is 2 (distance from index 3 to index 1)

Result: [1, 2]

Solution Implementation

1class Solution:
2    def solveQueries(self, nums: List[int], queries: List[int]) -> List[int]:
3        # Get the length of the input array
4        n = len(nums)
5      
6        # Double the length to handle circular array logic
7        doubled_length = n * 2
8      
9        # Initialize minimum distances array with maximum possible value
10        min_distances = [doubled_length] * doubled_length
11      
12        # First pass: Calculate distances looking from left to right
13        # Track the last seen position of each value
14        last_position_left = {}
15        for i in range(doubled_length):
16            # Get current value (using modulo for circular array)
17            current_value = nums[i % n]
18          
19            # If we've seen this value before, calculate distance
20            if current_value in last_position_left:
21                min_distances[i] = min(min_distances[i], i - last_position_left[current_value])
22          
23            # Update the last seen position for this value
24            last_position_left[current_value] = i
25      
26        # Second pass: Calculate distances looking from right to left
27        # Track the next position of each value when traversing backwards
28        next_position_right = {}
29        for i in range(doubled_length - 1, -1, -1):
30            # Get current value (using modulo for circular array)
31            current_value = nums[i % n]
32          
33            # If we've seen this value before (while going backwards), calculate distance
34            if current_value in next_position_right:
35                min_distances[i] = min(min_distances[i], next_position_right[current_value] - i)
36          
37            # Update the next position for this value
38            next_position_right[current_value] = i
39      
40        # Merge distances from both halves of the doubled array
41        # This handles the circular nature of the problem
42        for i in range(n):
43            min_distances[i] = min(min_distances[i], min_distances[i + n])
44      
45        # Process queries and return results
46        # Return -1 if no valid distance found (distance >= n), otherwise return the distance
47        return [-1 if min_distances[query_index] >= n else min_distances[query_index] 
48                for query_index in queries]
49
1class Solution {
2    public List<Integer> solveQueries(int[] nums, int[] queries) {
3        int arrayLength = nums.length;
4        int doubledLength = arrayLength * 2;
5      
6        // Array to store minimum distance to nearest duplicate for each position
7        // Initialize with maximum possible value (doubledLength)
8        int[] minDistances = new int[doubledLength];
9        Arrays.fill(minDistances, doubledLength);
10
11        // First pass: Calculate distances looking backward (from left)
12        // Map to store the most recent position of each element
13        Map<Integer, Integer> lastSeenPositions = new HashMap<>();
14        for (int i = 0; i < doubledLength; i++) {
15            int currentElement = nums[i % arrayLength];
16          
17            // If we've seen this element before, calculate distance
18            if (lastSeenPositions.containsKey(currentElement)) {
19                int distanceToLeft = i - lastSeenPositions.get(currentElement);
20                minDistances[i] = Math.min(minDistances[i], distanceToLeft);
21            }
22          
23            // Update the last seen position for this element
24            lastSeenPositions.put(currentElement, i);
25        }
26
27        // Second pass: Calculate distances looking forward (from right)
28        // Map to store the nearest future position of each element
29        Map<Integer, Integer> nextPositions = new HashMap<>();
30        for (int i = doubledLength - 1; i >= 0; i--) {
31            int currentElement = nums[i % arrayLength];
32          
33            // If this element appears again to the right, calculate distance
34            if (nextPositions.containsKey(currentElement)) {
35                int distanceToRight = nextPositions.get(currentElement) - i;
36                minDistances[i] = Math.min(minDistances[i], distanceToRight);
37            }
38          
39            // Update the next position for this element
40            nextPositions.put(currentElement, i);
41        }
42
43        // Combine results from doubled array back to original array
44        // Take minimum of position i and position i+n to handle circular nature
45        for (int i = 0; i < arrayLength; i++) {
46            minDistances[i] = Math.min(minDistances[i], minDistances[i + arrayLength]);
47        }
48
49        // Process queries and build result list
50        List<Integer> results = new ArrayList<>();
51        for (int queryIndex : queries) {
52            // If minimum distance is >= array length, no duplicate exists
53            int distance = minDistances[queryIndex];
54            results.add(distance >= arrayLength ? -1 : distance);
55        }
56      
57        return results;
58    }
59}
60
1class Solution {
2public:
3    vector<int> solveQueries(vector<int>& nums, vector<int>& queries) {
4        int arraySize = nums.size();
5        int doubledSize = arraySize * 2;  // Double the array to handle circular nature
6      
7        // Initialize minimum distances array with maximum possible value
8        vector<int> minDistances(doubledSize, doubledSize);
9      
10        // First pass: Find minimum distance to previous occurrence (left to right)
11        unordered_map<int, int> lastSeenIndex;
12        for (int i = 0; i < doubledSize; i++) {
13            int currentValue = nums[i % arraySize];
14          
15            // If we've seen this value before, calculate distance
16            if (lastSeenIndex.count(currentValue)) {
17                minDistances[i] = min(minDistances[i], i - lastSeenIndex[currentValue]);
18            }
19          
20            // Update last seen index for this value
21            lastSeenIndex[currentValue] = i;
22        }
23      
24        // Second pass: Find minimum distance to next occurrence (right to left)
25        unordered_map<int, int> nextSeenIndex;
26        for (int i = doubledSize - 1; i >= 0; i--) {
27            int currentValue = nums[i % arraySize];
28          
29            // If we've seen this value in future positions, calculate distance
30            if (nextSeenIndex.count(currentValue)) {
31                minDistances[i] = min(minDistances[i], nextSeenIndex[currentValue] - i);
32            }
33          
34            // Update next seen index for this value
35            nextSeenIndex[currentValue] = i;
36        }
37      
38        // Merge results from doubled array back to original array size
39        // Take minimum between position i and position i+n (circular wrap)
40        for (int i = 0; i < arraySize; i++) {
41            minDistances[i] = min(minDistances[i], minDistances[i + arraySize]);
42        }
43      
44        // Process queries and build result
45        vector<int> result;
46        for (int queryIndex : queries) {
47            // If minimum distance is >= array size, no duplicate exists
48            result.push_back(minDistances[queryIndex] >= arraySize ? -1 : minDistances[queryIndex]);
49        }
50      
51        return result;
52    }
53};
54
1/**
2 * Solves queries to find the minimum distance to the nearest duplicate element in a circular array
3 * @param nums - The input array of numbers
4 * @param queries - Array of indices to query
5 * @returns Array of minimum distances for each query, -1 if no duplicate exists
6 */
7function solveQueries(nums: number[], queries: number[]): number[] {
8    const arrayLength: number = nums.length;
9    const doubledLength: number = arrayLength * 2;
10  
11    // Initialize distance array with maximum possible value (doubledLength)
12    const minDistances: number[] = Array(doubledLength).fill(doubledLength);
13
14    // First pass: Calculate minimum distance to duplicate on the left
15    // Map to store the last seen position of each element
16    const lastSeenPositionLeft: Map<number, number> = new Map<number, number>();
17  
18    for (let i = 0; i < doubledLength; i++) {
19        const currentElement: number = nums[i % arrayLength];
20      
21        // If we've seen this element before, calculate distance from last occurrence
22        if (lastSeenPositionLeft.has(currentElement)) {
23            const previousPosition: number = lastSeenPositionLeft.get(currentElement)!;
24            minDistances[i] = Math.min(minDistances[i], i - previousPosition);
25        }
26      
27        // Update last seen position for current element
28        lastSeenPositionLeft.set(currentElement, i);
29    }
30
31    // Second pass: Calculate minimum distance to duplicate on the right
32    // Map to store the next seen position of each element
33    const nextSeenPositionRight: Map<number, number> = new Map<number, number>();
34  
35    for (let i = doubledLength - 1; i >= 0; i--) {
36        const currentElement: number = nums[i % arrayLength];
37      
38        // If we've seen this element in future positions, calculate distance to next occurrence
39        if (nextSeenPositionRight.has(currentElement)) {
40            const nextPosition: number = nextSeenPositionRight.get(currentElement)!;
41            minDistances[i] = Math.min(minDistances[i], nextPosition - i);
42        }
43      
44        // Update next seen position for current element
45        nextSeenPositionRight.set(currentElement, i);
46    }
47
48    // Merge results from doubled array back to original array size
49    // Take minimum distance from both cycles for each position
50    for (let i = 0; i < arrayLength; i++) {
51        minDistances[i] = Math.min(minDistances[i], minDistances[i + arrayLength]);
52    }
53
54    // Process queries and return results
55    // Return -1 if distance is greater than or equal to array length (no duplicate found)
56    return queries.map((queryIndex: number) => 
57        minDistances[queryIndex] >= arrayLength ? -1 : minDistances[queryIndex]
58    );
59}
60

Time and Space Complexity

The time complexity is O(n), where n is the length of the array nums.

The algorithm performs the following operations:

  • First loop: iterates through 2n elements (from 0 to m-1 where m = 2n), performing constant time operations for each element - O(n)
  • Second loop: iterates through 2n elements in reverse (from m-1 to 0), performing constant time operations for each element - O(n)
  • Third loop: iterates through n elements to update d[i] with the minimum of d[i] and d[i+n] - O(n)
  • Final list comprehension: iterates through len(queries) elements, which in the worst case could be O(n), performing constant time operations for each query

Total time complexity: O(n) + O(n) + O(n) + O(n) = O(n)

The space complexity is O(n), where n is the length of the array nums.

The algorithm uses:

  • Array d of size 2n - O(n)
  • Dictionary left which stores at most n unique elements from nums - O(n)
  • Dictionary right which stores at most n unique elements from nums - O(n)
  • Output list of size equal to the number of queries, which could be at most O(n)

Total space complexity: O(n) + O(n) + O(n) + O(n) = O(n)

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Confusing Array Doubling with Simple Concatenation

A common mistake is thinking that doubling the array means simply concatenating it once (nums + nums). However, the algorithm requires treating indices beyond n with modulo operations (i % n) to properly simulate the circular behavior.

Incorrect approach:

# Wrong: This creates an actual doubled array in memory
doubled_array = nums + nums
for i in range(len(doubled_array)):
    current_value = doubled_array[i]  # Direct access without modulo

Correct approach:

# Right: Use modulo to simulate doubling without extra space
for i in range(2 * n):
    current_value = nums[i % n]  # Modulo ensures circular access

2. Incorrect Distance Calculation in Circular Array

Many developers forget that in a circular array, the minimum distance between two positions isn't always the direct difference. You must consider both clockwise and counterclockwise distances.

Pitfall example:

# Wrong: Only considering forward distance
distance = (j - i) % n  # This misses the backward circular distance

Solution: The algorithm handles this by scanning in both directions (left-to-right and right-to-left) and taking the minimum of both distances.

3. Not Handling the "No Other Occurrence" Case Properly

A subtle bug occurs when checking if another occurrence exists. Simply checking if min_distances[i] == doubled_length might miss edge cases.

Pitfall:

# Potentially problematic
if min_distances[i] == doubled_length:
    return -1

Better approach:

# More robust check
if min_distances[i] >= n:  # Any distance >= n means no valid other occurrence
    return -1

4. Forgetting to Merge Results from Both Halves

The most critical step that's often overlooked is merging the minimum distances from positions i and i + n:

Missing step (common mistake):

# Wrong: Directly using distances without merging
return [-1 if min_distances[i] >= n else min_distances[i] for i in queries]

Correct implementation:

# Essential merging step
for i in range(n):
    min_distances[i] = min(min_distances[i], min_distances[i + n])

# Then process queries
return [-1 if min_distances[i] >= n else min_distances[i] for i in queries]

5. Hash Table Key Overwriting Issues

When updating the hash tables (last_position_left and next_position_right), developers might incorrectly handle multiple occurrences of the same value.

Potential issue: Not realizing that the hash table should always store the most recent (for left scan) or nearest upcoming (for right scan) position, which naturally happens with direct assignment but could be misunderstood if trying to store all positions.

Key insight: The algorithm correctly overwrites previous positions because we only need the closest occurrence in each direction, not all occurrences.

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

Which algorithm is best for finding the shortest distance between two points in an unweighted graph?


Recommended Readings

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

Load More