34. Find First and Last Position of Element in Sorted Array
Problem Description
You are given an array of integers nums
that is sorted in non-decreasing order (ascending order, allowing duplicates). Your task is to find the starting and ending positions of a given target
value within this array.
The function should return an array of two integers [start, end]
where:
start
is the index of the first occurrence oftarget
in the arrayend
is the index of the last occurrence oftarget
in the array
If the target
value is not found anywhere in the array, return [-1, -1]
.
The key constraint is that your algorithm must achieve O(log n)
runtime complexity, which means you need to use a binary search approach rather than a linear scan.
For example:
- If
nums = [5,7,7,8,8,10]
andtarget = 8
, the output should be[3,4]
because 8 appears at indices 3 and 4 - If
nums = [5,7,7,8,8,10]
andtarget = 6
, the output should be[-1,-1]
because 6 is not in the array - If
nums = [1]
andtarget = 1
, the output should be[0,0]
because the single element at index 0 matches the target
The solution uses Python's bisect_left
function twice:
- First to find the leftmost position where
target
could be inserted (which gives us the starting position iftarget
exists) - Second to find the leftmost position where
target + 1
could be inserted (which gives us one position after the last occurrence oftarget
)
If these two positions are the same (l == r
), it means target
doesn't exist in the array, so we return [-1, -1]
. Otherwise, we return [l, r-1]
as the range.
Intuition
Since the array is sorted and we need O(log n)
complexity, binary search is the natural choice. But the challenge is that we need to find both the first and last occurrences of the target, not just any occurrence.
The key insight is that we can transform this problem into finding two specific boundaries:
- The leftmost position where
target
appears (or would appear if inserted) - The position right after the last occurrence of
target
Think about it this way: if we have an array like [5,7,7,8,8,8,10]
and target = 8
, we want to find where the "8 section" begins and ends.
For the left boundary, we need to find the first position where a value is greater than or equal to target
. This is exactly what bisect_left(nums, target)
does - it finds the leftmost valid insertion point for target
.
For the right boundary, here's the clever part: instead of directly searching for the last occurrence of target
, we search for where target + 1
would be inserted. Why? Because bisect_left(nums, target + 1)
will give us the first position where a value is greater than or equal to target + 1
, which is exactly one position after the last occurrence of target
.
For example, with [5,7,7,8,8,8,10]
and target = 8
:
bisect_left(nums, 8)
returns 3 (first position of 8)bisect_left(nums, 9)
returns 6 (first position where we could insert 9, which is right after the last 8)- So the range is
[3, 6-1] = [3, 5]
If the target doesn't exist, both bisect_left(nums, target)
and bisect_left(nums, target + 1)
will return the same position (where target
would be inserted if it existed), making l == r
, which tells us to return [-1, -1]
.
This approach elegantly solves the problem with just two binary searches, maintaining the required O(log n)
complexity.
Learn more about Binary Search patterns.
Solution Approach
The solution uses binary search to find both boundaries of the target range efficiently. Here's how the implementation works:
Step 1: Find the left boundary
l = bisect_left(nums, target)
The bisect_left
function performs a binary search to find the leftmost position where target
should be inserted to maintain sorted order. This gives us:
- If
target
exists in the array, this returns the index of its first occurrence - If
target
doesn't exist, this returns the position where it would be inserted
Step 2: Find the position after the right boundary
r = bisect_left(nums, target + 1)
By searching for target + 1
, we find the position where the next larger value would be inserted. This cleverly gives us:
- If
target
exists, this returns the index right after its last occurrence - If
target
doesn't exist, this returns the same position asl
Step 3: Check if target exists and return the result
return [-1, -1] if l == r else [l, r - 1]
- If
l == r
, it means there's no space between wheretarget
would start and wheretarget + 1
would start, indicatingtarget
doesn't exist in the array - If
l != r
, the target exists, and the range is[l, r-1]
wherer-1
is the last occurrence
Binary Search Details:
The bisect_left
function internally implements binary search with the following logic:
- Maintains two pointers:
left = 0
andright = len(nums)
- While
left < right
:- Calculate
mid = (left + right) // 2
- If
nums[mid] < target
, search right half:left = mid + 1
- Otherwise, search left half:
right = mid
- Calculate
- Returns
left
as the insertion position
Time Complexity: O(log n)
- Two binary searches, each taking O(log n)
time
Space Complexity: O(1)
- Only using a constant amount of extra space for variables
Example Walkthrough:
For nums = [5,7,7,8,8,10]
and target = 8
:
bisect_left(nums, 8)
returns3
(first index of 8)bisect_left(nums, 9)
returns5
(position after last 8)- Since
3 != 5
, return[3, 5-1] = [3, 4]
For nums = [5,7,7,8,8,10]
and target = 6
:
bisect_left(nums, 6)
returns1
(where 6 would be inserted)bisect_left(nums, 7)
returns1
(where 7 already exists)- Since
1 == 1
, return[-1, -1]
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 walk through a detailed example with nums = [2,2,3,4,4,4,5]
and target = 4
:
Step 1: Find the left boundary (first occurrence of 4)
Using bisect_left(nums, 4)
:
- Initial:
left = 0
,right = 7
- Iteration 1:
mid = 3
,nums[3] = 4
, not less than target, search left half →right = 3
- Iteration 2:
mid = 1
,nums[1] = 2
, less than target, search right half →left = 2
- Iteration 3:
mid = 2
,nums[2] = 3
, less than target, search right half →left = 3
- Terminates with
left = 3
So l = 3
(first occurrence of 4 is at index 3)
Step 2: Find the position after the right boundary
Using bisect_left(nums, 5)
(searching for target + 1):
- Initial:
left = 0
,right = 7
- Iteration 1:
mid = 3
,nums[3] = 4
, less than 5, search right half →left = 4
- Iteration 2:
mid = 5
,nums[5] = 4
, less than 5, search right half →left = 6
- Iteration 3:
mid = 6
,nums[6] = 5
, not less than 5, search left half →right = 6
- Terminates with
left = 6
So r = 6
(position after the last 4)
Step 3: Determine the result
Since l = 3
and r = 6
, we have l != r
, meaning target exists.
Return [l, r-1] = [3, 5]
Verification: In [2,2,3,4,4,4,5]
, the value 4 appears at indices 3, 4, and 5, so [3,5]
is correct!
Edge Case Example:
For the same array with target = 1
(not in array):
bisect_left(nums, 1)
returns0
(would insert at beginning)bisect_left(nums, 2)
returns0
(where 2 already exists)- Since
0 == 0
, return[-1, -1]
Solution Implementation
1from typing import List
2from bisect import bisect_left
3
4class Solution:
5 def searchRange(self, nums: List[int], target: int) -> List[int]:
6 # Find the leftmost position where target could be inserted
7 left_index = bisect_left(nums, target)
8
9 # Find the leftmost position where (target + 1) could be inserted
10 # This gives us the position right after the last occurrence of target
11 right_index = bisect_left(nums, target + 1)
12
13 # If left_index equals right_index, target doesn't exist in the array
14 # Otherwise, return the range [left_index, right_index - 1]
15 if left_index == right_index:
16 return [-1, -1]
17 else:
18 return [left_index, right_index - 1]
19
1class Solution {
2 /**
3 * Find the starting and ending position of a given target value in a sorted array.
4 * @param nums - sorted array of integers
5 * @param target - target value to search for
6 * @return array containing [start_index, end_index] of target, or [-1, -1] if not found
7 */
8 public int[] searchRange(int[] nums, int target) {
9 // Find the leftmost position where target could be inserted
10 int leftBoundary = findLeftBoundary(nums, target);
11
12 // Find the leftmost position where (target + 1) could be inserted
13 // This gives us the position right after the last occurrence of target
14 int rightBoundaryPlusOne = findLeftBoundary(nums, target + 1);
15
16 // If both boundaries are the same, target doesn't exist in the array
17 if (leftBoundary == rightBoundaryPlusOne) {
18 return new int[] {-1, -1};
19 }
20
21 // Return the range: [leftBoundary, rightBoundaryPlusOne - 1]
22 return new int[] {leftBoundary, rightBoundaryPlusOne - 1};
23 }
24
25 /**
26 * Binary search to find the leftmost position where targetValue could be inserted.
27 * This finds the first occurrence of values >= targetValue.
28 * @param nums - sorted array of integers
29 * @param targetValue - value to search for
30 * @return index of the leftmost position where targetValue should be inserted
31 */
32 private int findLeftBoundary(int[] nums, int targetValue) {
33 int left = 0;
34 int right = nums.length;
35
36 // Binary search for the leftmost position
37 while (left < right) {
38 // Calculate middle index (using unsigned right shift to avoid overflow)
39 int mid = (left + right) >>> 1;
40
41 // If mid element is greater than or equal to targetValue,
42 // the answer lies in the left half (including mid)
43 if (nums[mid] >= targetValue) {
44 right = mid;
45 } else {
46 // Otherwise, the answer lies in the right half (excluding mid)
47 left = mid + 1;
48 }
49 }
50
51 return left;
52 }
53}
54
1class Solution {
2public:
3 vector<int> searchRange(vector<int>& nums, int target) {
4 // Find the leftmost position where target could be inserted (first occurrence)
5 int leftBound = lower_bound(nums.begin(), nums.end(), target) - nums.begin();
6
7 // Find the leftmost position where (target + 1) could be inserted
8 // This gives us the position right after the last occurrence of target
9 int rightBoundNext = lower_bound(nums.begin(), nums.end(), target + 1) - nums.begin();
10
11 // If leftBound equals rightBoundNext, target doesn't exist in the array
12 if (leftBound == rightBoundNext) {
13 return {-1, -1};
14 }
15
16 // Return the range: [first occurrence, last occurrence]
17 // Last occurrence is rightBoundNext - 1
18 return {leftBound, rightBoundNext - 1};
19 }
20};
21
1/**
2 * Finds the first and last position of a target value in a sorted array.
3 * @param nums - The sorted array of numbers to search in
4 * @param target - The target value to find
5 * @returns An array containing [startIndex, endIndex] of the target, or [-1, -1] if not found
6 */
7function searchRange(nums: number[], target: number): number[] {
8 /**
9 * Binary search to find the leftmost position where value >= searchValue
10 * @param searchValue - The value to search for
11 * @returns The index of the leftmost position where nums[index] >= searchValue
12 */
13 const findLeftmostPosition = (searchValue: number): number => {
14 let left: number = 0;
15 let right: number = nums.length;
16
17 // Binary search for the leftmost position
18 while (left < right) {
19 // Calculate middle index using bit shift for efficiency
20 const middle: number = (left + right) >> 1;
21
22 if (nums[middle] >= searchValue) {
23 // Target might be at middle or to the left
24 right = middle;
25 } else {
26 // Target is definitely to the right
27 left = middle + 1;
28 }
29 }
30
31 return left;
32 };
33
34 // Find the leftmost position of the target
35 const leftBoundary: number = findLeftmostPosition(target);
36
37 // Find the leftmost position of (target + 1), which gives us the position after the last target
38 const rightBoundaryPlusOne: number = findLeftmostPosition(target + 1);
39
40 // If both boundaries are the same, target doesn't exist in the array
41 // Otherwise, return the range [leftBoundary, rightBoundaryPlusOne - 1]
42 return leftBoundary === rightBoundaryPlusOne ? [-1, -1] : [leftBoundary, rightBoundaryPlusOne - 1];
43}
44
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 two binary search operations via bisect_left()
. Each bisect_left()
call performs a binary search on the sorted array, which takes O(log n)
time. Since we perform two binary searches sequentially (one for target
and one for target + 1
), the total time complexity is O(log n) + O(log n) = O(log n)
.
The space complexity is O(1)
. The algorithm only uses a constant amount of extra space to store the two variables l
and r
, regardless of the input size. The bisect_left()
function itself uses iterative binary search internally, which doesn't require additional space proportional to the input size.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Integer Overflow When Computing target + 1
The Pitfall:
When using bisect_left(nums, target + 1)
to find the right boundary, if target
equals the maximum integer value (e.g., 2^31 - 1
in Python), adding 1 would cause integer overflow in languages with fixed integer sizes. While Python handles arbitrary precision integers, this approach could fail in other languages or when porting the solution.
Example Scenario:
nums = [1, 2, 2147483647, 2147483647] target = 2147483647 # Maximum 32-bit integer # target + 1 would overflow in many languages
Solution:
Instead of searching for target + 1
, use bisect_right(nums, target)
which directly finds the rightmost position where target
could be inserted:
from bisect import bisect_left, bisect_right
def searchRange(self, nums: List[int], target: int) -> List[int]:
left_index = bisect_left(nums, target)
right_index = bisect_right(nums, target)
if left_index == right_index:
return [-1, -1]
else:
return [left_index, right_index - 1]
2. Assuming the Array is Non-Empty
The Pitfall:
The original solution doesn't explicitly handle empty arrays. While bisect_left
works correctly with empty lists (returning 0), it's a common mistake to forget this edge case when implementing custom binary search.
Example Scenario:
nums = [] target = 5 # Should return [-1, -1]
Solution: Add an explicit check for empty arrays if implementing custom binary search:
def searchRange(self, nums: List[int], target: int) -> List[int]:
if not nums: # Handle empty array explicitly
return [-1, -1]
left_index = bisect_left(nums, target)
right_index = bisect_left(nums, target + 1)
if left_index == right_index:
return [-1, -1]
else:
return [left_index, right_index - 1]
3. Misunderstanding bisect_left
Return Value
The Pitfall:
A common misconception is that bisect_left(nums, target)
returns -1
when the target is not found. In reality, it returns the insertion position, which could be any valid index from 0 to len(nums)
.
Example Scenario:
nums = [1, 3, 5, 7] target = 4 # bisect_left returns 2 (not -1), which is where 4 would be inserted # Without proper checking, you might incorrectly access nums[2] thinking it's the target
Solution: Always verify that the found position contains the target before using it:
def searchRange(self, nums: List[int], target: int) -> List[int]:
left_index = bisect_left(nums, target)
# Verify that the target actually exists at the found position
if left_index >= len(nums) or nums[left_index] != target:
return [-1, -1]
right_index = bisect_left(nums, target + 1)
return [left_index, right_index - 1]
4. Implementing Custom Binary Search Incorrectly
The Pitfall:
When implementing binary search manually instead of using bisect
, common mistakes include:
- Using
right = len(nums) - 1
when the logic expectsright = len(nums)
- Incorrect mid calculation causing infinite loops
- Wrong comparison operators leading to off-by-one errors
Solution: Stick to a consistent binary search template:
def find_left_boundary(nums, target):
left, right = 0, len(nums)
while left < right:
mid = left + (right - left) // 2 # Prevents overflow
if nums[mid] < target:
left = mid + 1
else:
right = mid
return left
def find_right_boundary(nums, target):
left, right = 0, len(nums)
while left < right:
mid = left + (right - left) // 2
if nums[mid] <= target: # Note the <= for right boundary
left = mid + 1
else:
right = mid
return left
What is the best way of checking if an element exists in an unsorted array once in terms of time complexity? Select the best that applies.
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!