704. Binary Search
Problem Description
You are given a sorted array of integers nums
in ascending order and an integer target
. Your task is to search for the target
value in the array.
The function should:
- Return the index of
target
if it exists in the array - Return
-1
iftarget
is not found in the array
Important constraint: Your algorithm must have O(log n)
runtime complexity, which means you need to use binary search rather than a linear scan through the array.
For example:
- If
nums = [1, 3, 5, 7, 9]
andtarget = 5
, the function should return2
(sincenums[2] = 5
) - If
nums = [1, 3, 5, 7, 9]
andtarget = 6
, the function should return-1
(since 6 is not in the array)
The key insight is that since the array is sorted, you can eliminate half of the search space in each step by comparing the middle element with the target, leading to the required logarithmic time complexity.
Intuition
Since we need O(log n)
complexity and the array is sorted, binary search is the natural choice. The sorted property allows us to make intelligent decisions about which half of the array to search next.
The key idea is to repeatedly divide the search space in half. At each step, we look at the middle element and ask: "Is our target in the left half or the right half?" We can answer this question by comparing the middle element with our target.
The specific implementation uses a slightly different variant of binary search. Instead of having three cases (found, go left, go right), it uses a two-way decision:
- If
nums[mid] >= target
, we include the middle element in our search space by settingr = mid
- If
nums[mid] < target
, we know the target must be to the right, so we exclude mid by settingl = mid + 1
This approach guarantees that if the target exists, it will eventually be at position l
when the loop terminates (when l
equals r
). The reason this works is that we're essentially finding the leftmost position where nums[pos] >= target
. If the element at that position equals our target, we've found it; otherwise, the target doesn't exist in the array.
The bit shift operation (l + r) >> 1
is just an efficient way to calculate the middle point, equivalent to (l + r) // 2
, avoiding potential integer overflow issues in other languages.
Learn more about Binary Search patterns.
Solution Approach
We implement binary search with two pointers to find the target in the sorted array.
Step-by-step implementation:
-
Initialize boundaries: Set
l = 0
(left boundary) andr = len(nums) - 1
(right boundary) to cover the entire array. -
Binary search loop: Continue searching while
l < r
:- Calculate the middle position:
mid = (l + r) >> 1
(equivalent to(l + r) // 2
) - Compare
nums[mid]
withtarget
:- If
nums[mid] >= target
: The target is either atmid
or to its left, so we move the right boundary tomid
by settingr = mid
- If
nums[mid] < target
: The target must be to the right ofmid
, so we move the left boundary tomid + 1
by settingl = mid + 1
- If
- Calculate the middle position:
-
Check result: When the loop ends (when
l == r
), we have converged to a single position:- If
nums[l] == target
, we found the target and returnl
- Otherwise, the target doesn't exist in the array, so we return
-1
- If
Why this approach works:
The algorithm maintains an invariant that if the target exists in the array, it must be within the range [l, r]
. By comparing with the middle element, we can safely eliminate half of the search space in each iteration.
The condition nums[mid] >= target
ensures that we find the leftmost occurrence if there were duplicates (though this problem assumes unique elements). When l
and r
converge, l
points to the smallest index where nums[index] >= target
.
Time Complexity: O(log n)
- We halve the search space in each iteration
Space Complexity: O(1)
- We only use a constant amount of extra space for the pointers
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through the algorithm with nums = [1, 3, 5, 7, 9, 11]
and target = 7
.
Initial Setup:
l = 0
,r = 5
(array indices)- Array:
[1, 3, 5, 7, 9, 11]
- Looking for:
7
Iteration 1:
l = 0
,r = 5
, sol < r
(continue)- Calculate
mid = (0 + 5) >> 1 = 2
- Check
nums[2] = 5
- Compare:
5 < 7
, so target must be to the right - Update:
l = mid + 1 = 3
- New range: indices
[3, 5]
→ values[7, 9, 11]
Iteration 2:
l = 3
,r = 5
, sol < r
(continue)- Calculate
mid = (3 + 5) >> 1 = 4
- Check
nums[4] = 9
- Compare:
9 >= 7
, so target is at mid or to its left - Update:
r = mid = 4
- New range: indices
[3, 4]
→ values[7, 9]
Iteration 3:
l = 3
,r = 4
, sol < r
(continue)- Calculate
mid = (3 + 4) >> 1 = 3
- Check
nums[3] = 7
- Compare:
7 >= 7
, so target is at mid or to its left - Update:
r = mid = 3
- New range: index
[3]
→ value[7]
Termination:
l = 3
,r = 3
, sol == r
(exit loop)- Check:
nums[3] = 7
, which equals our target - Return:
3
Example with target not found:
Let's quickly see what happens with the same array but target = 6
:
- After similar iterations, we converge to
l = r = 3
- Check:
nums[3] = 7
, which is7 ≠ 6
- Return:
-1
The algorithm efficiently narrows down the search space by half in each iteration, examining only 3 elements out of 6 to find the target, demonstrating the logarithmic time complexity.
Solution Implementation
1class Solution:
2 def search(self, nums: List[int], target: int) -> int:
3 # Initialize left and right pointers for binary search
4 left, right = 0, len(nums) - 1
5
6 # Binary search to find the leftmost position where nums[mid] >= target
7 while left < right:
8 # Calculate middle index using bit shift (equivalent to // 2)
9 mid = (left + right) >> 1
10
11 # If middle element is greater than or equal to target,
12 # search in the left half (including mid)
13 if nums[mid] >= target:
14 right = mid
15 else:
16 # Otherwise, search in the right half (excluding mid)
17 left = mid + 1
18
19 # Check if the element at the final position equals target
20 # Return the index if found, otherwise return -1
21 return left if nums[left] == target else -1
22
1class Solution {
2 /**
3 * Binary search to find the target element in a sorted array.
4 * Returns the index of the target if found, otherwise returns -1.
5 *
6 * @param nums sorted array of integers
7 * @param target the value to search for
8 * @return index of target if found, -1 otherwise
9 */
10 public int search(int[] nums, int target) {
11 // Initialize left and right pointers for binary search
12 int left = 0;
13 int right = nums.length - 1;
14
15 // Binary search loop - find leftmost position where nums[i] >= target
16 while (left < right) {
17 // Calculate middle index using bit shift (equivalent to dividing by 2)
18 int middle = (left + right) >> 1;
19
20 // If middle element is greater than or equal to target,
21 // the target (if exists) must be in the left half including middle
22 if (nums[middle] >= target) {
23 right = middle;
24 } else {
25 // Otherwise, target must be in the right half excluding middle
26 left = middle + 1;
27 }
28 }
29
30 // After the loop, left == right, pointing to the leftmost position
31 // where nums[i] >= target. Check if it equals target.
32 return nums[left] == target ? left : -1;
33 }
34}
35
1class Solution {
2public:
3 int search(vector<int>& nums, int target) {
4 // Initialize left and right pointers for binary search
5 int left = 0;
6 int right = nums.size() - 1;
7
8 // Binary search to find the leftmost position where nums[i] >= target
9 while (left < right) {
10 // Calculate middle index using bit shift (equivalent to dividing by 2)
11 int mid = (left + right) >> 1;
12
13 // If middle element is greater than or equal to target,
14 // the target (if exists) must be in the left half including mid
15 if (nums[mid] >= target) {
16 right = mid;
17 }
18 // Otherwise, the target must be in the right half excluding mid
19 else {
20 left = mid + 1;
21 }
22 }
23
24 // After the loop, left == right, pointing to the leftmost position
25 // where nums[i] >= target. Check if it equals target
26 return nums[left] == target ? left : -1;
27 }
28};
29
1/**
2 * Binary search to find the leftmost position of target in a sorted array
3 * @param nums - Sorted array of numbers in ascending order
4 * @param target - The target value to search for
5 * @returns The index of target if found, otherwise -1
6 */
7function search(nums: number[], target: number): number {
8 // Initialize left and right pointers for binary search
9 let left: number = 0;
10 let right: number = nums.length - 1;
11
12 // Continue searching while the search space has more than one element
13 while (left < right) {
14 // Calculate middle index using bit shift for integer division
15 const middle: number = (left + right) >> 1;
16
17 // If middle element is greater than or equal to target,
18 // the target (if exists) must be in the left half including middle
19 if (nums[middle] >= target) {
20 right = middle;
21 } else {
22 // Otherwise, target must be in the right half excluding middle
23 left = middle + 1;
24 }
25 }
26
27 // Check if the element at the final position equals target
28 // left and right converge to the same position after the loop
29 return nums[left] === target ? left : -1;
30}
31
Time and Space Complexity
The time complexity is O(log n)
, where n
is the length of the array nums
. This is because the algorithm uses binary search, which divides the search space in half with each iteration. In the worst case, the while loop runs log₂(n)
times until l
and r
converge.
The space complexity is O(1)
. The algorithm only uses a constant amount of extra space for the 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. Empty Array Handling
The current implementation fails when the input array is empty. Accessing nums[left]
when nums = []
will throw an IndexError
.
Solution: Add an early check for empty arrays:
def search(self, nums: List[int], target: int) -> int:
if not nums: # Handle empty array
return -1
left, right = 0, len(nums) - 1
# ... rest of the code
2. Integer Overflow in Middle Calculation
While Python handles large integers gracefully, in other languages like Java or C++, calculating mid = (left + right) // 2
can cause integer overflow when left + right
exceeds the maximum integer value.
Solution: Use the overflow-safe formula:
mid = left + (right - left) // 2 # Or keep the bit shift version which is also safe: mid = left + ((right - left) >> 1)
3. Incorrect Loop Condition Leading to Infinite Loop
A common mistake is using while left <= right
with the update logic right = mid
, which can cause an infinite loop when left == right
and nums[mid] >= target
.
Solution: Either:
- Keep
while left < right
withright = mid
(as in the current code) - Or use
while left <= right
withright = mid - 1
:
while left <= right: mid = (left + right) >> 1 if nums[mid] == target: return mid elif nums[mid] > target: right = mid - 1 # Note: mid - 1, not mid else: left = mid + 1
4. Confusing Binary Search Variants
The given implementation finds the leftmost position where nums[index] >= target
. If the problem requires finding any occurrence (not necessarily the leftmost), the logic becomes simpler but different.
Solution for finding any occurrence:
def search(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums) - 1
while left <= right:
mid = (left + right) >> 1
if nums[mid] == target:
return mid # Return immediately when found
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
5. Off-by-One Errors in Boundary Updates
Forgetting to use mid + 1
or mid - 1
appropriately can lead to missing elements or infinite loops. The rule of thumb:
- When moving left boundary past mid:
left = mid + 1
- When keeping mid in search space:
right = mid
- When excluding mid from search space:
right = mid - 1
What does the following code do?
1def f(arr1, arr2):
2 i, j = 0, 0
3 new_arr = []
4 while i < len(arr1) and j < len(arr2):
5 if arr1[i] < arr2[j]:
6 new_arr.append(arr1[i])
7 i += 1
8 else:
9 new_arr.append(arr2[j])
10 j += 1
11 new_arr.extend(arr1[i:])
12 new_arr.extend(arr2[j:])
13 return new_arr
14
1public static List<Integer> f(int[] arr1, int[] arr2) {
2 int i = 0, j = 0;
3 List<Integer> newArr = new ArrayList<>();
4
5 while (i < arr1.length && j < arr2.length) {
6 if (arr1[i] < arr2[j]) {
7 newArr.add(arr1[i]);
8 i++;
9 } else {
10 newArr.add(arr2[j]);
11 j++;
12 }
13 }
14
15 while (i < arr1.length) {
16 newArr.add(arr1[i]);
17 i++;
18 }
19
20 while (j < arr2.length) {
21 newArr.add(arr2[j]);
22 j++;
23 }
24
25 return newArr;
26}
27
1function f(arr1, arr2) {
2 let i = 0, j = 0;
3 let newArr = [];
4
5 while (i < arr1.length && j < arr2.length) {
6 if (arr1[i] < arr2[j]) {
7 newArr.push(arr1[i]);
8 i++;
9 } else {
10 newArr.push(arr2[j]);
11 j++;
12 }
13 }
14
15 while (i < arr1.length) {
16 newArr.push(arr1[i]);
17 i++;
18 }
19
20 while (j < arr2.length) {
21 newArr.push(arr2[j]);
22 j++;
23 }
24
25 return newArr;
26}
27
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!