Facebook Pixel

1966. Binary Searchable Numbers in an Unsorted Array

Problem Description

This problem asks you to determine how many elements in an array are guaranteed to be found by a randomized binary search algorithm, regardless of which pivot is chosen at each step.

The algorithm works as follows:

  • Start with a sequence and a target value
  • While the sequence is not empty:
    • Randomly select any element from the sequence as the pivot
    • If pivot equals target, return true
    • If pivot is less than target, remove the pivot and all elements to its left
    • If pivot is greater than target, remove the pivot and all elements to its right
  • If sequence becomes empty, return false

The key insight is that for an element to be guaranteed to be found (when it's the target), it must be findable regardless of which pivots are chosen. This means:

  1. When searching for element nums[i], any element to its left that is chosen as pivot must be smaller than nums[i] (so the algorithm correctly removes the left portion and keeps nums[i])

  2. Any element to its right that is chosen as pivot must be larger than nums[i] (so the algorithm correctly removes the right portion and keeps nums[i])

Therefore, an element nums[i] is guaranteed to be found if and only if:

  • All elements to its left are smaller than nums[i]
  • All elements to its right are larger than nums[i]

In other words, nums[i] must be greater than or equal to the maximum of all elements before it, AND less than or equal to the minimum of all elements after it.

The solution tracks this by:

  • Computing the running maximum from left to right
  • Computing the running minimum from right to left
  • Counting elements that satisfy both conditions

For example, in array [1, 3, 2]:

  • 1 is guaranteed (no elements to left, all elements to right are larger)
  • 3 is NOT guaranteed (element 2 to its right is smaller, so if 2 is chosen as pivot when searching for 3, the algorithm incorrectly removes 3)
  • 2 is NOT guaranteed (element 3 to its left is larger, so if 3 is chosen as pivot when searching for 2, the algorithm incorrectly removes 2)

The answer would be 1.

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

Intuition

To understand when an element is guaranteed to be found, let's think about what could go wrong in this randomized search algorithm.

The algorithm makes decisions based on comparing the pivot with the target. When we're searching for a specific element nums[i], the algorithm keeps or discards portions of the array based on these comparisons. The critical observation is that the algorithm assumes the array is sorted when making these decisions.

Let's trace through what happens when nums[i] is the target:

  • If a pivot from the left of nums[i] is chosen and it's larger than nums[i], the algorithm thinks nums[i] must be to the left of this pivot (since in a sorted array, larger elements come after). It will discard everything to the right, including nums[i] itself - causing a miss!
  • Similarly, if a pivot from the right of nums[i] is chosen and it's smaller than nums[i], the algorithm thinks nums[i] must be to the right. It will discard everything to the left, including nums[i] - again causing a miss!

This leads us to the key realization: for nums[i] to be guaranteed to be found, we need:

  • Every element to its left must be smaller (so when chosen as pivot, the algorithm correctly keeps the right portion containing nums[i])
  • Every element to its right must be larger (so when chosen as pivot, the algorithm correctly keeps the left portion containing nums[i])

This is equivalent to saying nums[i] must be:

  • Greater than or equal to the maximum of all elements before it
  • Less than or equal to the minimum of all elements after it

These elements are essentially in their "correct" sorted positions relative to their neighbors, making them immune to the randomness of pivot selection. We can efficiently check this condition by maintaining a running maximum while traversing left-to-right and a running minimum while traversing right-to-left, marking elements that satisfy both conditions.

Learn more about Binary Search patterns.

Solution Approach

The implementation uses a two-pass approach with a marking array to efficiently identify all guaranteed elements.

Data Structure:

  • ok: A boolean array (initialized with all 1s) to mark whether each element is guaranteed to be found

Algorithm Steps:

  1. First Pass (Left to Right):

    • Initialize mx = -1000000 (a value smaller than any possible element)
    • Traverse the array from left to right
    • For each element nums[i]:
      • If nums[i] < mx, mark ok[i] = 0 (this element is smaller than some element to its left, so it's not guaranteed)
      • Otherwise, update mx = nums[i] (maintain the running maximum)
  2. Second Pass (Right to Left):

    • Initialize mi = 1000000 (a value larger than any possible element)
    • Traverse the array from right to left
    • For each element nums[i]:
      • If nums[i] > mi, mark ok[i] = 0 (this element is larger than some element to its right, so it's not guaranteed)
      • Otherwise, update mi = nums[i] (maintain the running minimum)
  3. Count Result:

    • Return sum(ok) - the count of elements that remain marked as 1

Why This Works:

After the first pass, ok[i] = 1 means nums[i] >= max(nums[0...i-1]) - the element is not smaller than any element to its left.

After the second pass, ok[i] = 1 additionally means nums[i] <= min(nums[i+1...n-1]) - the element is not larger than any element to its right.

Elements with ok[i] = 1 after both passes satisfy both conditions, making them guaranteed to be found regardless of pivot selection.

Time Complexity: O(n) - two linear passes through the array Space Complexity: O(n) - for the ok array

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through the solution with array nums = [2, 1, 3, 5, 4, 6].

We want to find which elements are guaranteed to be found by the randomized binary search, regardless of pivot choice.

Initial Setup:

  • Create ok = [1, 1, 1, 1, 1, 1] (assume all elements are valid initially)

First Pass (Left to Right) - Check if each element is ≥ all elements to its left:

  • mx = -1000000 (initial running maximum)
  • i=0, nums[0]=2: Is 2 < -1000000? No, so keep ok[0]=1. Update mx=2
  • i=1, nums[1]=1: Is 1 < 2? Yes, so set ok[1]=0. Keep mx=2
  • i=2, nums[2]=3: Is 3 < 2? No, so keep ok[2]=1. Update mx=3
  • i=3, nums[3]=5: Is 5 < 3? No, so keep ok[3]=1. Update mx=5
  • i=4, nums[4]=4: Is 4 < 5? Yes, so set ok[4]=0. Keep mx=5
  • i=5, nums[5]=6: Is 6 < 5? No, so keep ok[5]=1. Update mx=6

After first pass: ok = [1, 0, 1, 1, 0, 1]

Second Pass (Right to Left) - Check if each element is ≤ all elements to its right:

  • mi = 1000000 (initial running minimum)
  • i=5, nums[5]=6: Is 6 > 1000000? No, so keep ok[5]=1. Update mi=6
  • i=4, nums[4]=4: Is 4 > 6? No, so keep ok[4]=0. Update mi=4
  • i=3, nums[3]=5: Is 5 > 4? Yes, so set ok[3]=0. Keep mi=4
  • i=2, nums[2]=3: Is 3 > 4? No, so keep ok[2]=1. Update mi=3
  • i=1, nums[1]=1: Is 1 > 3? No, so keep ok[1]=0. Update mi=1
  • i=0, nums[0]=2: Is 2 > 1? Yes, so set ok[0]=0. Keep mi=1

After second pass: ok = [0, 0, 1, 0, 0, 1]

Result: sum(ok) = 2

The guaranteed elements are nums[2]=3 and nums[5]=6:

  • 3 has all smaller elements to its left (2, 1) and all larger elements to its right (5, 4, 6)
  • 6 has all smaller elements to its left (2, 1, 3, 5, 4) and no elements to its right

These elements will always be found because:

  • When searching for 3, any left pivot (2 or 1) correctly keeps the right portion containing 3, and any right pivot (5, 4, or 6) correctly keeps the left portion containing 3
  • When searching for 6, any left pivot correctly keeps the right portion containing 6

Solution Implementation

1class Solution:
2    def binarySearchableNumbers(self, nums: List[int]) -> int:
3        """
4        Count the number of elements that could be found via binary search
5        in their current positions.
6
7        An element is binary searchable if:
8        - All elements to its left are less than or equal to it
9        - All elements to its right are greater than or equal to it
10
11        Args:
12            nums: List of integers
13
14        Returns:
15            Count of binary searchable numbers
16        """
17        n = len(nums)
18
19        # Track which elements are valid for binary search
20        is_searchable = [True] * n
21
22        # First pass: left to right
23        # Check if current element is >= all elements before it
24        max_so_far = float('-inf')
25        for i, num in enumerate(nums):
26            if num < max_so_far:
27                # Current element is smaller than some element before it
28                is_searchable[i] = False
29            else:
30                # Update maximum seen so far
31                max_so_far = num
32
33        # Second pass: right to left
34        # Check if current element is <= all elements after it
35        min_so_far = float('inf')
36        for i in range(n - 1, -1, -1):
37            if nums[i] > min_so_far:
38                # Current element is larger than some element after it
39                is_searchable[i] = False
40            else:
41                # Update minimum seen so far
42                min_so_far = nums[i]
43
44        # Count elements that satisfy both conditions
45        return sum(is_searchable)
46
1class Solution {
2    public int binarySearchableNumbers(int[] nums) {
3        int n = nums.length;
4
5        // Track whether each element is binary searchable (1 = yes, 0 = no)
6        int[] isBinarySearchable = new int[n];
7        Arrays.fill(isBinarySearchable, 1);
8
9        // First pass (left to right): Mark elements that violate the left condition
10        // An element is NOT binary searchable if any element to its left is greater
11        int maxFromLeft = Integer.MIN_VALUE;
12        for (int i = 0; i < n; i++) {
13            // If current element is less than the maximum seen so far from the left,
14            // it cannot be found by binary search
15            if (nums[i] < maxFromLeft) {
16                isBinarySearchable[i] = 0;
17            }
18            maxFromLeft = Math.max(maxFromLeft, nums[i]);
19        }
20
21        // Second pass (right to left): Mark elements that violate the right condition
22        // and count valid elements
23        int minFromRight = Integer.MAX_VALUE;
24        int count = 0;
25
26        for (int i = n - 1; i >= 0; i--) {
27            // If current element is greater than the minimum seen so far from the right,
28            // it cannot be found by binary search
29            if (nums[i] > minFromRight) {
30                isBinarySearchable[i] = 0;
31            }
32            minFromRight = Math.min(minFromRight, nums[i]);
33
34            // Add to count if this element is binary searchable
35            count += isBinarySearchable[i];
36        }
37
38        return count;
39    }
40}
41
1class Solution {
2public:
3    int binarySearchableNumbers(vector<int>& nums) {
4        int n = nums.size();
5
6        // Track which elements can be binary searchable
7        vector<int> isBinarySearchable(n, 1);
8
9        // First pass: left to right
10        // An element cannot be binary searchable if there's a larger element to its left
11        int maxSoFar = INT_MIN;
12        for (int i = 0; i < n; ++i) {
13            if (nums[i] < maxSoFar) {
14                // Current element is smaller than some element on its left
15                isBinarySearchable[i] = 0;
16            }
17            maxSoFar = max(maxSoFar, nums[i]);
18        }
19
20        // Second pass: right to left
21        // An element cannot be binary searchable if there's a smaller element to its right
22        // Also count the valid binary searchable numbers
23        int minSoFar = INT_MAX;
24        int count = 0;
25
26        for (int i = n - 1; i >= 0; --i) {
27            if (nums[i] > minSoFar) {
28                // Current element is larger than some element on its right
29                isBinarySearchable[i] = 0;
30            }
31            minSoFar = min(minSoFar, nums[i]);
32
33            // Add to count if this element is binary searchable
34            count += isBinarySearchable[i];
35        }
36
37        return count;
38    }
39};
40
1function binarySearchableNumbers(nums: number[]): number {
2    const n: number = nums.length;
3
4    // Track which elements can be binary searchable (1 = searchable, 0 = not searchable)
5    const isBinarySearchable: number[] = new Array(n).fill(1);
6
7    // First pass: left to right
8    // An element cannot be binary searchable if there's a larger element to its left
9    let maxSoFar: number = Number.MIN_SAFE_INTEGER;
10
11    for (let i = 0; i < n; i++) {
12        if (nums[i] < maxSoFar) {
13            // Current element is smaller than some element on its left
14            // This violates the binary search property
15            isBinarySearchable[i] = 0;
16        }
17        // Update the maximum value seen so far
18        maxSoFar = Math.max(maxSoFar, nums[i]);
19    }
20
21    // Second pass: right to left
22    // An element cannot be binary searchable if there's a smaller element to its right
23    let minSoFar: number = Number.MAX_SAFE_INTEGER;
24    let count: number = 0;
25
26    for (let i = n - 1; i >= 0; i--) {
27        if (nums[i] > minSoFar) {
28            // Current element is larger than some element on its right
29            // This violates the binary search property
30            isBinarySearchable[i] = 0;
31        }
32        // Update the minimum value seen so far
33        minSoFar = Math.min(minSoFar, nums[i]);
34
35        // Add to count if this element is binary searchable
36        // (passes both left and right conditions)
37        count += isBinarySearchable[i];
38    }
39
40    return count;
41}
42

Time and Space Complexity

Time Complexity: O(n)

The algorithm consists of two sequential passes through the array:

  • First pass (left to right): Iterates through all n elements once to check if each element is greater than or equal to the maximum of all elements to its left. This takes O(n) time.
  • Second pass (right to left): Iterates through all n elements once to check if each element is less than or equal to the minimum of all elements to its right. This takes O(n) time.
  • Final summation: sum(ok) iterates through the ok array once, taking O(n) time.

Total time complexity: O(n) + O(n) + O(n) = O(n)

Space Complexity: O(n)

The algorithm uses:

  • ok array: Stores n elements to track which numbers are binary searchable, requiring O(n) space.
  • Variables mx, mi, i, x: These are constant space variables, requiring O(1) space.

Total space complexity: O(n) + O(1) = O(n)

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Off-by-One Error in Comparison Logic

A common mistake is using incorrect comparison operators when checking conditions. Developers might incorrectly use strict inequalities where non-strict ones are needed.

Incorrect Implementation:

# Wrong: Using strict inequality
if num <= max_so_far:  # Should be <
    is_searchable[i] = False

Why it's wrong: An element can equal the maximum of elements before it and still be searchable. For example, in [1, 2, 2, 3], the second 2 should be searchable.

Correct Implementation:

if num < max_so_far:  # Correct: only mark false if strictly less
    is_searchable[i] = False

2. Incorrect Initialization of Max/Min Values

Another pitfall is initializing max_so_far and min_so_far with the first element of the array instead of infinity values.

Incorrect Implementation:

max_so_far = nums[0]  # Wrong initialization
for i in range(1, n):  # Starting from index 1
    if nums[i] < max_so_far:
        is_searchable[i] = False
    else:
        max_so_far = nums[i]

Why it's wrong: This approach incorrectly handles the first element. The first element should always pass the left-side check since there are no elements to its left.

Correct Implementation:

max_so_far = float('-inf')  # Correct: ensures first element passes
for i in range(n):
    if nums[i] < max_so_far:
        is_searchable[i] = False
    else:
        max_so_far = nums[i]

3. Updating Max/Min in Wrong Condition Branch

A subtle but critical error is updating the running max/min value only when the element is valid, rather than always updating it.

Incorrect Implementation:

for i, num in enumerate(nums):
    if num < max_so_far:
        is_searchable[i] = False
    # Wrong: only updating when element is searchable
    if is_searchable[i]:
        max_so_far = num

Why it's wrong: The maximum should track ALL elements seen so far, not just searchable ones. For example, in [1, 5, 2, 4], element 5 is not searchable, but we still need to track it as the maximum to correctly evaluate element 4.

Correct Implementation:

for i, num in enumerate(nums):
    if num < max_so_far:
        is_searchable[i] = False
    else:
        max_so_far = num  # Always update when num >= max_so_far

4. Edge Case: Empty Array

While not shown in the main solution, handling empty arrays can cause issues.

Potential Issue:

def binarySearchableNumbers(self, nums: List[int]) -> int:
    n = len(nums)
    if n == 0:  # Should handle this case
        return 0
    # ... rest of the code

Solution: Add a guard clause at the beginning to handle empty arrays gracefully.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What's the output of running the following function using input 56?

1KEYBOARD = {
2    '2': 'abc',
3    '3': 'def',
4    '4': 'ghi',
5    '5': 'jkl',
6    '6': 'mno',
7    '7': 'pqrs',
8    '8': 'tuv',
9    '9': 'wxyz',
10}
11
12def letter_combinations_of_phone_number(digits):
13    def dfs(path, res):
14        if len(path) == len(digits):
15            res.append(''.join(path))
16            return
17
18        next_number = digits[len(path)]
19        for letter in KEYBOARD[next_number]:
20            path.append(letter)
21            dfs(path, res)
22            path.pop()
23
24    res = []
25    dfs([], res)
26    return res
27
1private static final Map<Character, char[]> KEYBOARD = Map.of(
2    '2', "abc".toCharArray(),
3    '3', "def".toCharArray(),
4    '4', "ghi".toCharArray(),
5    '5', "jkl".toCharArray(),
6    '6', "mno".toCharArray(),
7    '7', "pqrs".toCharArray(),
8    '8', "tuv".toCharArray(),
9    '9', "wxyz".toCharArray()
10);
11
12public static List<String> letterCombinationsOfPhoneNumber(String digits) {
13    List<String> res = new ArrayList<>();
14    dfs(new StringBuilder(), res, digits.toCharArray());
15    return res;
16}
17
18private static void dfs(StringBuilder path, List<String> res, char[] digits) {
19    if (path.length() == digits.length) {
20        res.add(path.toString());
21        return;
22    }
23    char next_digit = digits[path.length()];
24    for (char letter : KEYBOARD.get(next_digit)) {
25        path.append(letter);
26        dfs(path, res, digits);
27        path.deleteCharAt(path.length() - 1);
28    }
29}
30
1const KEYBOARD = {
2    '2': 'abc',
3    '3': 'def',
4    '4': 'ghi',
5    '5': 'jkl',
6    '6': 'mno',
7    '7': 'pqrs',
8    '8': 'tuv',
9    '9': 'wxyz',
10}
11
12function letter_combinations_of_phone_number(digits) {
13    let res = [];
14    dfs(digits, [], res);
15    return res;
16}
17
18function dfs(digits, path, res) {
19    if (path.length === digits.length) {
20        res.push(path.join(''));
21        return;
22    }
23    let next_number = digits.charAt(path.length);
24    for (let letter of KEYBOARD[next_number]) {
25        path.push(letter);
26        dfs(digits, path, res);
27        path.pop();
28    }
29}
30

Recommended Readings

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

Load More