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 positionj
- Moving backward (counterclockwise): from position
i
to positionj
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
whereanswer[i]
is the result forqueries[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
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:
- Looking left: For each position, find the closest identical element that appeared before it
- 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 rightright
: 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 positionleft[nums[i]]
, the distance isi - left[nums[i]]
- When scanning right to left, if we'll see
nums[i]
again at positionright[nums[i]]
, the distance isright[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 sizem = 2n
(wheren
is the original array length), initialized withm
(a large value) - Create two hash tables:
left
andright
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 elementx
has appeared before - If yes, calculate the distance
i - left[x]
and updated[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
andi + 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 ind[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 EvaluatorExample 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 (from0
tom-1
wherem = 2n
), performing constant time operations for each element -O(n)
- Second loop: iterates through
2n
elements in reverse (fromm-1
to0
), performing constant time operations for each element -O(n)
- Third loop: iterates through
n
elements to updated[i]
with the minimum ofd[i]
andd[i+n]
-O(n)
- Final list comprehension: iterates through
len(queries)
elements, which in the worst case could beO(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 size2n
-O(n)
- Dictionary
left
which stores at mostn
unique elements fromnums
-O(n)
- Dictionary
right
which stores at mostn
unique elements fromnums
-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.
Which algorithm is best for finding the shortest distance between two points in an unweighted graph?
Recommended Readings
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
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!