702. Search in a Sorted Array of Unknown Size 🔒
Problem Description
You are given a sorted array of unique elements with an unknown size. You cannot directly access the array, but you can interact with it through an ArrayReader
interface that provides a get(i)
method. This method:
- Returns the value at index
i
(0-indexed) ifi
is within the array bounds - Returns
2^31 - 1
ifi
is out of bounds
Given an integer target
, your task is to find the index k
where secret[k] == target
. If the target doesn't exist in the array, return -1
.
The challenge is to implement a search algorithm with O(log n)
time complexity without knowing the array's size beforehand.
The solution uses a two-phase approach:
-
Finding the boundary: Start with
r = 1
and keep doubling it (shifting left by 1 bit) whilereader.get(r) < target
. This exponential search helps locate an upper bound where the target must be before positionr
. Oncereader.get(r) >= target
, we know the target (if it exists) is between positionsr/2
andr
. -
Binary search: With the range
[l, r]
established wherel = r >> 1
(equivalent tor/2
), perform standard binary search. Calculatemid = (l + r) >> 1
, and adjust the boundaries based on whetherreader.get(mid)
is greater than or equal to the target. Finally, check if the element at positionl
equals the target to determine the return value.
This approach efficiently handles the unknown array size by first establishing a search range in O(log n)
time, then performing binary search within that range, maintaining the overall O(log n)
complexity.
Intuition
The key challenge here is that we don't know the array's size, so we can't apply binary search directly. In a typical binary search, we start with known boundaries [0, n-1]
. But here, we need to first figure out where our search space ends.
Think of it like searching for a book in a library where you don't know how many shelves there are. You wouldn't check every single shelf linearly - that would be too slow. Instead, you'd make increasingly larger jumps to find roughly where the books end, then narrow down your search.
The insight is to use exponential growth to find an upper boundary. Why exponential? Because if we increment linearly (checking positions 1, 2, 3, 4...), we might need O(n)
steps just to find the boundary. But if we double our position each time (checking positions 1, 2, 4, 8, 16...), we'll find a boundary in just O(log n)
steps.
Once we overshoot the target (when reader.get(r) >= target
), we know two crucial things:
- The target, if it exists, must be before position
r
- The target must be after position
r/2
(because if it were beforer/2
, we would have stopped earlier)
This gives us a bounded range [r/2, r]
that contains at most r/2
elements. Now we can apply standard binary search within this range.
The beauty of this approach is that both phases - finding the boundary and searching within it - take O(log n)
time. The exponential search grows as fast as possible to find the boundary, then binary search efficiently pinpoints the exact location. It's like zooming out quickly to get the big picture, then zooming in precisely to find what we need.
Learn more about Binary Search patterns.
Solution Approach
The implementation follows the two-phase approach described in the reference solution:
Phase 1: Finding the Search Boundary
We start by initializing r = 1
as our right boundary pointer. Then we enter a loop where we continuously double r
by shifting it left (r <<= 1
, equivalent to r * 2
) as long as reader.get(r) < target
. This exponential growth ensures we find a position where the value is greater than or equal to the target in O(log n)
time.
r = 1 while reader.get(r) < target: r <<= 1
After this loop, we know:
reader.get(r) >= target
(orr
is out of bounds, returning2^31 - 1
)reader.get(r/2) < target
(from the previous iteration)
Phase 2: Binary Search in the Bounded Range
We set the left boundary l = r >> 1
(equivalent to r / 2
). This gives us a search range [l, r]
where the target must exist if it's in the array.
l = r >> 1 while l < r: mid = (l + r) >> 1 if reader.get(mid) >= target: r = mid else: l = mid + 1
The binary search works by:
- Calculating the middle point:
mid = (l + r) >> 1
(integer division) - If
reader.get(mid) >= target
, the target is in the left half (includingmid
), so we setr = mid
- Otherwise, the target must be in the right half (excluding
mid
), so we setl = mid + 1
The loop continues until l == r
, converging to a single position.
Final Check
After the binary search, l
points to the position where the target should be if it exists. We verify by checking if reader.get(l) == target
:
return l if reader.get(l) == target else -1
The use of bit shift operations (<<
and >>
) instead of multiplication and division is a common optimization in competitive programming, though modern compilers often optimize these operations automatically.
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 an example where we have a sorted array [1, 3, 5, 7, 9, 11, 13, 15]
and we're searching for target = 9
.
Phase 1: Finding the Search Boundary
Starting with r = 1
:
reader.get(1) = 3
, and3 < 9
, so we double:r = 2
reader.get(2) = 5
, and5 < 9
, so we double:r = 4
reader.get(4) = 9
, and9 >= 9
, so we stop
Now we have r = 4
where reader.get(4) = 9 >= target
.
Phase 2: Binary Search
Set l = r >> 1 = 4 >> 1 = 2
. Our search range is [2, 4]
.
First iteration:
l = 2, r = 4
mid = (2 + 4) >> 1 = 3
reader.get(3) = 7
, and7 < 9
- So we set
l = mid + 1 = 4
Second iteration:
l = 4, r = 4
- Loop terminates since
l == r
Final Check
Check reader.get(4) = 9
, which equals our target, so we return 4
.
Let's trace another example where target = 10
(not in array):
Phase 1: Same as before, we end up with r = 4
(since reader.get(4) = 9 < 10
would cause another doubling to r = 8
, and reader.get(8)
returns 2^31 - 1 >= 10
).
Phase 2: With l = 4, r = 8
:
- First iteration:
mid = 6
,reader.get(6) = 13 > 10
, setr = 6
- Second iteration:
mid = 5
,reader.get(5) = 11 > 10
, setr = 5
- Third iteration:
l = 4, r = 5
,mid = 4
,reader.get(4) = 9 < 10
, setl = 5
- Loop terminates with
l = r = 5
Final Check: reader.get(5) = 11 ≠10
, so we return -1
.
Solution Implementation
1# """
2# This is ArrayReader's API interface.
3# You should not implement it, or speculate about its implementation
4# """
5# class ArrayReader:
6# def get(self, index: int) -> int:
7
8
9class Solution:
10 def search(self, reader: "ArrayReader", target: int) -> int:
11 # Step 1: Find the right boundary where the value is >= target
12 # Start with right boundary at index 1
13 right = 1
14
15 # Double the right boundary until we find a value >= target
16 # This ensures we find an upper bound in O(log n) time
17 while reader.get(right) < target:
18 right <<= 1 # Equivalent to right *= 2
19
20 # Step 2: Binary search within the range [left, right)
21 # Left boundary is half of the right boundary (previous position)
22 left = right >> 1 # Equivalent to left = right // 2
23
24 # Standard binary search to find the target
25 while left < right:
26 # Calculate middle index using bit shifting for efficiency
27 mid = (left + right) >> 1 # Equivalent to (left + right) // 2
28
29 # If mid value is >= target, search in left half
30 if reader.get(mid) >= target:
31 right = mid
32 # Otherwise, search in right half
33 else:
34 left = mid + 1
35
36 # Check if the element at final position equals target
37 # Return the index if found, otherwise return -1
38 return left if reader.get(left) == target else -1
39
1/**
2 * // This is ArrayReader's API interface.
3 * // You should not implement it, or speculate about its implementation
4 * interface ArrayReader {
5 * public int get(int index) {}
6 * }
7 */
8
9class Solution {
10 public int search(ArrayReader reader, int target) {
11 // Step 1: Find the right boundary where the value is >= target
12 // Start with right boundary at index 1 and double it until we find a value >= target
13 int right = 1;
14 while (reader.get(right) < target) {
15 right = right << 1; // Double the right boundary (equivalent to right *= 2)
16 }
17
18 // Step 2: Set left boundary to half of right boundary
19 // This ensures our target is within [left, right] range
20 int left = right >> 1; // Divide right by 2 (equivalent to right / 2)
21
22 // Step 3: Perform binary search within the established boundaries
23 while (left < right) {
24 // Calculate middle index using bit shift for efficiency
25 int mid = (left + right) >> 1; // Equivalent to (left + right) / 2
26
27 // If value at mid is greater than or equal to target,
28 // search in the left half including mid
29 if (reader.get(mid) >= target) {
30 right = mid;
31 } else {
32 // If value at mid is less than target,
33 // search in the right half excluding mid
34 left = mid + 1;
35 }
36 }
37
38 // Step 4: Check if the element at the final position equals target
39 // Return the index if found, otherwise return -1
40 return reader.get(left) == target ? left : -1;
41 }
42}
43
1/**
2 * // This is the ArrayReader's API interface.
3 * // You should not implement it, or speculate about its implementation
4 * class ArrayReader {
5 * public:
6 * int get(int index);
7 * };
8 */
9
10class Solution {
11public:
12 int search(const ArrayReader& reader, int target) {
13 // Step 1: Find the upper bound by exponentially increasing the right boundary
14 // Start with right = 1 and double it until we find a value >= target
15 int right = 1;
16 while (reader.get(right) < target) {
17 right = right << 1; // Equivalent to right *= 2
18 }
19
20 // Step 2: Set left boundary to half of right (the previous position before doubling)
21 int left = right >> 1; // Equivalent to right / 2
22
23 // Step 3: Perform binary search within the range [left, right)
24 while (left < right) {
25 // Calculate middle point using bit shift for efficiency
26 int mid = (left + right) >> 1; // Equivalent to (left + right) / 2
27
28 // Check if the middle element is greater than or equal to target
29 if (reader.get(mid) >= target) {
30 // Target is in the left half (including mid)
31 right = mid;
32 } else {
33 // Target is in the right half (excluding mid)
34 left = mid + 1;
35 }
36 }
37
38 // Step 4: Check if the element at the final position equals target
39 // Return the index if found, otherwise return -1
40 return reader.get(left) == target ? left : -1;
41 }
42};
43
1/**
2 * Search for a target value in a sorted array with unknown size.
3 * Uses exponential search to find the boundary, then binary search to find the target.
4 *
5 * @param reader - The ArrayReader interface to access array elements
6 * @param target - The target value to search for
7 * @returns The index of the target if found, otherwise -1
8 */
9function search(reader: ArrayReader, target: number): number {
10 // Step 1: Exponential search to find the right boundary
11 // Start with index 1 and double it until we find a value >= target
12 let rightBoundary: number = 1;
13 while (reader.get(rightBoundary) < target) {
14 rightBoundary = rightBoundary << 1; // Double the boundary (equivalent to * 2)
15 }
16
17 // Step 2: Binary search within the identified range
18 // The left boundary is half of the right boundary (previous position before doubling)
19 let left: number = rightBoundary >> 1; // Equivalent to rightBoundary / 2
20 let right: number = rightBoundary;
21
22 // Standard binary search to find the leftmost position where value >= target
23 while (left < right) {
24 // Calculate middle point using bit shift for efficiency
25 const middle: number = (left + right) >> 1; // Equivalent to Math.floor((left + right) / 2)
26
27 if (reader.get(middle) >= target) {
28 // Target could be at middle or to the left
29 right = middle;
30 } else {
31 // Target must be to the right of middle
32 left = middle + 1;
33 }
34 }
35
36 // Check if the element at the final position is the target
37 return reader.get(left) === target ? left : -1;
38}
39
Time and Space Complexity
Time Complexity: O(log M)
, where M
is the position of the target value in the array.
The algorithm consists of two phases:
-
Range Finding Phase: The first while loop doubles the index
r
(usingr <<= 1
) until we find a value greater than or equal to the target. Starting from index 1, this takesO(log M)
iterations since we're exponentially increasing the search boundary. If the target is at positionM
, we need approximatelylogâ‚‚(M)
doublings to reach an index beyond it. -
Binary Search Phase: Once we've established the range
[l, r]
wherel = r/2
andreader.get(r) >= target
, we perform standard binary search. The size of this range is at mostM
, so binary search takesO(log M)
time.
The total time complexity is O(log M) + O(log M) = O(log M)
.
Space Complexity: O(1)
The algorithm only uses a constant amount of extra space for variables l
, r
, and mid
, regardless of the input size. No additional data structures are created that scale with the input.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Integer Overflow in Boundary Expansion
When doubling the right boundary (right <<= 1
), the value can grow exponentially and potentially cause integer overflow in languages with fixed integer sizes. In Python, this isn't an issue due to arbitrary precision integers, but in languages like Java or C++, this could lead to unexpected behavior.
Solution: Add a check to ensure right
doesn't exceed a reasonable maximum value (like 10^9
or the maximum array size constraint):
while reader.get(right) < target: if right > 10**9: # Prevent excessive growth break right <<= 1
2. Incorrect Handling of Target at Index 0
The initial right = 1
means we start checking from index 1, potentially missing the target if it's at index 0. The algorithm assumes the exponential search will eventually include index 0 when left = right >> 1
is calculated, but this requires careful verification.
Solution: Add an explicit check for index 0 before starting the exponential search:
if reader.get(0) == target: return 0 right = 1
3. Misunderstanding the Binary Search Convergence
Developers might incorrectly assume left
and right
converge to different values or might use while left <= right
instead of while left < right
. This can lead to infinite loops or incorrect results.
Solution: Understand that the binary search invariant maintains that:
- When
reader.get(mid) >= target
, we setright = mid
(notmid - 1
) - The loop terminates when
left == right
, pointing to the smallest index where value >= target - This is crucial for finding the leftmost occurrence if duplicates existed
4. Not Handling Edge Case Where Target is Larger Than All Elements
If the target is larger than all elements in the array, the exponential search phase will eventually hit out-of-bounds indices, returning 2^31 - 1
. The binary search will then search in a range that includes invalid indices.
Solution: The current implementation actually handles this correctly because:
reader.get(right)
will return2^31 - 1
when out of bounds- Since
2^31 - 1 > target
, the binary search will proceed normally - The final check
reader.get(left) == target
will correctly return -1
However, being explicit about this edge case in comments helps maintainability:
# Note: reader.get() returns 2^31 - 1 for out-of-bounds indices, # which is larger than any valid target, ensuring correct behavior
Which technique can we use to find the middle of a linked list?
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!