Facebook Pixel

702. Search in a Sorted Array of Unknown Size

MediumArrayBinary SearchInteractive
LeetCode ↗

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) if i is within the array bounds
  • Returns 2^31 - 1 if i 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:

  1. Finding the boundary: Start with r = 1 and keep doubling it (shifting left by 1 bit) while reader.get(r) < target. This exponential search helps locate an upper bound where the target must be before position r. Once reader.get(r) >= target, we know the target (if it exists) is between positions r/2 and r.

  2. Binary search: With the range [l, r] established where l = r >> 1 (equivalent to r/2), perform a binary search for the first index whose value is greater than or equal to the target. Calculate mid = (l + r) // 2; when reader.get(mid) >= target, record mid as first_true_index and move r to mid - 1, otherwise move l to mid + 1. Finally, check if the element at first_true_index 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. The target, if it exists, must be before position r
  2. The target must be after position r/2 (because if it were before r/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.

New to patterns?Most interview problems reduce to a handful of patterns. This video covers the main eight.

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 (or r is out of bounds, returning 2^31 - 1)
  • reader.get(r/2) < target (from the previous iteration, when r > 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, mid is a candidate, so we record it in first_true_index and continue searching the left half with right = mid - 1
  • Otherwise, the answer is to the right of mid, so we set left = 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, and 3 < 9, so we double: r = 2
  • reader.get(2) = 5, and 5 < 9, so we double: r = 4
  • reader.get(4) = 9, and 9 >= 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 = 4
  • mid = (2 + 4) // 2 = 3
  • reader.get(3) = 7, and 7 < 9
  • So we set left = mid + 1 = 4

Second iteration:

  • left = 4, right = 4
  • mid = (4 + 4) // 2 = 4
  • reader.get(4) = 9, and 9 >= 9
  • So we set first_true_index = 4 and right = 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, set first_true_index = 6, right = 5
  • Second iteration: mid = 4, reader.get(4) = 9 < 10, set left = 5
  • Third iteration: left = 5, right = 5, mid = 5, reader.get(5) = 11 >= 10, set first_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
40
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        // 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}
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        // 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};
45
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    // 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}
41

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:

  1. Range Finding Phase: The first while loop doubles the index r (using r <<= 1) until we find a value greater than or equal to the target. Starting from index 1, this takes O(log M) iterations since we're exponentially increasing the search boundary. If the target is at position M, we need approximately log₂(M) doublings to reach an index beyond it.

  2. Binary Search Phase: Once we've established the range [l, r] where l = r/2 and reader.get(r) >= target, we perform standard binary search. The size of this range is at most M, so binary search takes O(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: set right = mid - 1 and record the candidate in first_true_index
  • while left < right: set right = mid, and the answer is left after 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 return 2^31 - 1 when out of bounds
  • Since 2^31 - 1 > target, the binary search will proceed normally
  • The final check reader.get(first_true_index) == 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

Ready to land your dream job?

Unlock your dream job with a 5-minute quiz for a personalized study roadmap!

Get My Roadmap
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Get a Personalized Study Roadmap:

Which data structure is used to implement recursion?


Recommended Readings

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

Load More