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 a binary search for the first index whose value is greater than or equal to the target. Calculatemid = (l + r) // 2; whenreader.get(mid) >= target, recordmidasfirst_true_indexand movertomid - 1, otherwise moveltomid + 1. Finally, check if the element atfirst_true_indexequals 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.
Pattern Learn more about Binary Search patterns.
Solution Approach
The implementation follows a two-phase approach:
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, whenr > 1)
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. In the code these pointers are named left and right.
left = right >> 1 first_true_index = -1 while left <= right: mid = (left + right) // 2 if reader.get(mid) >= target: first_true_index = mid right = mid - 1 else: left = mid + 1
The binary search looks for the first index whose value is greater than or equal to the target:
- Calculating the middle point:
mid = (left + right) // 2 - If
reader.get(mid) >= target,midis a candidate, so we record it infirst_true_indexand continue searching the left half withright = mid - 1 - Otherwise, the answer is to the right of
mid, so we setleft = mid + 1
The loop ends when left > right. At that point first_true_index holds the smallest index in the range whose value is at least the target.
Final Check
After the binary search, first_true_index points to the position where the target should be if it exists. We verify by checking if reader.get(first_true_index) == target:
if first_true_index != -1 and reader.get(first_true_index) == target: return first_true_index return -1
The use of bit shift operations (<< and >>) instead of multiplication and division is a common habit in competitive programming, though compilers usually optimize these operations automatically.
Example 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 left = right >> 1 = 4 >> 1 = 2. Our search range is [2, 4], and first_true_index = -1.
First iteration:
left = 2, right = 4mid = (2 + 4) // 2 = 3reader.get(3) = 7, and7 < 9- So we set
left = mid + 1 = 4
Second iteration:
left = 4, right = 4mid = (4 + 4) // 2 = 4reader.get(4) = 9, and9 >= 9- So we set
first_true_index = 4andright = mid - 1 = 3
Now left = 4 > right = 3, so the loop terminates.
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: reader.get(1) = 3, reader.get(2) = 5, and reader.get(4) = 9 are all less than 10, so right doubles to 8. reader.get(8) is out of bounds and returns 2^31 - 1 >= 10, so the loop stops with right = 8.
Phase 2: With left = 4, right = 8:
- First iteration:
mid = 6,reader.get(6) = 13 >= 10, setfirst_true_index = 6,right = 5 - Second iteration:
mid = 4,reader.get(4) = 9 < 10, setleft = 5 - Third iteration:
left = 5, right = 5,mid = 5,reader.get(5) = 11 >= 10, setfirst_true_index = 5,right = 4 - Loop terminates since
left = 5 > right = 4
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.
Pattern 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), a concern in languages with fixed-size integers is that right could overflow. Under this problem's constraints it cannot: the array has at most 10^4 elements and every target is smaller than 2^31 - 1. Once right passes the end of the array, reader.get(right) returns 2^31 - 1, which is not less than the target, so the loop stops. The largest value right can reach is 2^14 = 16384.
Solution: No extra guard is needed for this problem. If the out-of-bounds sentinel could be smaller than the target, the loop would need a separate stopping condition.
2. Worrying About the Target at Index 0
The initial right = 1 means the first probe is index 1, which can look like it skips index 0. It does not: if reader.get(1) >= target, the loop stops immediately, and left = right >> 1 = 0, so the binary search range [0, 1] includes index 0. An explicit check of index 0 is harmless but not required.
3. Mixing Binary Search Templates
The code uses while left <= right together with right = mid - 1 and a separate first_true_index variable. A common mistake is to combine while left <= right with right = mid (from the while left < right template). When left == right and reader.get(mid) >= target, right stays equal to mid and the loop never ends.
Solution: Keep each template consistent:
while left <= right: setright = mid - 1and record the candidate infirst_true_indexwhile left < right: setright = mid, and the answer isleftafter the loop
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(first_true_index) == 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
Ready to land your dream job?
Unlock your dream job with a 5-minute quiz for a personalized study roadmap!
Get My RoadmapWhich data structure is used to implement recursion?
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!