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:
-
When searching for element
nums[i]
, any element to its left that is chosen as pivot must be smaller thannums[i]
(so the algorithm correctly removes the left portion and keepsnums[i]
) -
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 keepsnums[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 (element2
to its right is smaller, so if2
is chosen as pivot when searching for3
, the algorithm incorrectly removes3
)2
is NOT guaranteed (element3
to its left is larger, so if3
is chosen as pivot when searching for2
, the algorithm incorrectly removes2
)
The answer would be 1
.
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 thannums[i]
, the algorithm thinksnums[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, includingnums[i]
itself - causing a miss! - Similarly, if a pivot from the right of
nums[i]
is chosen and it's smaller thannums[i]
, the algorithm thinksnums[i]
must be to the right. It will discard everything to the left, includingnums[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:
-
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
, markok[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)
- If
- Initialize
-
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
, markok[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)
- If
- Initialize
-
Count Result:
- Return
sum(ok)
- the count of elements that remain marked as 1
- Return
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 EvaluatorExample 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
: Is2 < -1000000
? No, so keepok[0]=1
. Updatemx=2
i=1, nums[1]=1
: Is1 < 2
? Yes, so setok[1]=0
. Keepmx=2
i=2, nums[2]=3
: Is3 < 2
? No, so keepok[2]=1
. Updatemx=3
i=3, nums[3]=5
: Is5 < 3
? No, so keepok[3]=1
. Updatemx=5
i=4, nums[4]=4
: Is4 < 5
? Yes, so setok[4]=0
. Keepmx=5
i=5, nums[5]=6
: Is6 < 5
? No, so keepok[5]=1
. Updatemx=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
: Is6 > 1000000
? No, so keepok[5]=1
. Updatemi=6
i=4, nums[4]=4
: Is4 > 6
? No, so keepok[4]=0
. Updatemi=4
i=3, nums[3]=5
: Is5 > 4
? Yes, so setok[3]=0
. Keepmi=4
i=2, nums[2]=3
: Is3 > 4
? No, so keepok[2]=1
. Updatemi=3
i=1, nums[1]=1
: Is1 > 3
? No, so keepok[1]=0
. Updatemi=1
i=0, nums[0]=2
: Is2 > 1
? Yes, so setok[0]=0
. Keepmi=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
or1
) correctly keeps the right portion containing3
, and any right pivot (5
,4
, or6
) correctly keeps the left portion containing3
- When searching for
6
, any left pivot correctly keeps the right portion containing6
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 takesO(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 takesO(n)
time. - Final summation:
sum(ok)
iterates through theok
array once, takingO(n)
time.
Total time complexity: O(n) + O(n) + O(n) = O(n)
Space Complexity: O(n)
The algorithm uses:
ok
array: Storesn
elements to track which numbers are binary searchable, requiringO(n)
space.- Variables
mx
,mi
,i
,x
: These are constant space variables, requiringO(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.
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
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!