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) ifiis within the array bounds - Returns
2^31 - 1ifiis 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 = 1and 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/2andr. -
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 positionlequals 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(orris 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 = 2reader.get(2) = 5, and5 < 9, so we double:r = 4reader.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 = 4mid = (2 + 4) >> 1 = 3reader.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 # Phase 1: Find the right boundary using exponential search
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 # Phase 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 first_true_index = -1
24
25 # Binary search using the template: find first index where reader.get(mid) >= target
26 while left <= right:
27 mid = (left + right) // 2
28
29 if reader.get(mid) >= target:
30 first_true_index = mid
31 right = mid - 1
32 else:
33 left = mid + 1
34
35 # Check if the element at first_true_index equals target
36 # Return the index if found, otherwise return -1
37 if first_true_index != -1 and reader.get(first_true_index) == target:
38 return first_true_index
39 return -1
401/**
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 // Phase 1: Find the right boundary using exponential search
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 // Phase 2: Binary search within the range [left, right]
19 // Left boundary is half of right boundary (previous position)
20 int left = right >> 1; // Divide right by 2 (equivalent to right / 2)
21 int firstTrueIndex = -1;
22
23 // Binary search using the template: find first index where reader.get(mid) >= target
24 while (left <= right) {
25 int mid = left + (right - left) / 2;
26
27 if (reader.get(mid) >= target) {
28 firstTrueIndex = mid;
29 right = mid - 1;
30 } else {
31 left = mid + 1;
32 }
33 }
34
35 // Check if the element at firstTrueIndex equals target
36 // Return the index if found, otherwise return -1
37 if (firstTrueIndex != -1 && reader.get(firstTrueIndex) == target) {
38 return firstTrueIndex;
39 }
40 return -1;
41 }
42}
431/**
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 // Phase 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 // Phase 2: Binary search within the range [left, right]
21 // Left boundary is half of right (the previous position before doubling)
22 int left = right >> 1; // Equivalent to right / 2
23 int firstTrueIndex = -1;
24
25 // Binary search using the template: find first index where reader.get(mid) >= target
26 while (left <= right) {
27 int mid = left + (right - left) / 2;
28
29 if (reader.get(mid) >= target) {
30 firstTrueIndex = mid;
31 right = mid - 1;
32 } else {
33 left = mid + 1;
34 }
35 }
36
37 // Check if the element at firstTrueIndex equals target
38 // Return the index if found, otherwise return -1
39 if (firstTrueIndex != -1 && reader.get(firstTrueIndex) == target) {
40 return firstTrueIndex;
41 }
42 return -1;
43 }
44};
451/**
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 // Phase 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 // Phase 2: Binary search within the range [left, right]
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 let firstTrueIndex: number = -1;
22
23 // Binary search using the template: find first index where reader.get(mid) >= target
24 while (left <= right) {
25 const mid: number = Math.floor((left + right) / 2);
26
27 if (reader.get(mid) >= target) {
28 firstTrueIndex = mid;
29 right = mid - 1;
30 } else {
31 left = mid + 1;
32 }
33 }
34
35 // Check if the element at firstTrueIndex equals target
36 if (firstTrueIndex !== -1 && reader.get(firstTrueIndex) === target) {
37 return firstTrueIndex;
38 }
39 return -1;
40}
41Time 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/2andreader.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 - 1when out of bounds- Since
2^31 - 1 > target, the binary search will proceed normally - The final check
reader.get(left) == targetwill 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 data structure is used in a depth first search?
Recommended Readings
https assets algo monster cover_photos Binary_Search svg Binary Search Intuition Binary search is an efficient array search algorithm It works by narrowing down the search range by half each time If you have looked up a word in a physical dictionary you've already used binary search in real life Let's
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 If you prefer videos here's a video that explains recursion in a fun and easy way 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 him
Want a Structured Path to Master System Design Too? Don’t Miss This!