Find First and Last Position of Element in Sorted Array | Leetcode 34
Welcome to our deep dive on Find First and Last Position of Element in Sorted Array (Leetcode 34). This is the quintessential problem for testing your absolute mastery of Binary Search. It forces you to write two specialized implementations to hunt for borders rather than just an exact match.
Problem Statement
Given an array of integers nums sorted in non-decreasing order, find the starting and ending position of a given target value.
If target is not found in the array, return [-1, -1].
Constraint: You must write an algorithm with O(log N) runtime complexity.
Example:
nums = [5, 7, 7, 8, 8, 10], target = 8 => Output: [3, 4]
Approach 1: Dual Binary Search O(log N)
We are dealing with a sorted array, and the complexity constraint screams O(log N). This guarantees we need to implement a Binary Search.
The conventional binary search stops executing right when nums[mid] == target. But in this problem, there might be multiple 8s scattered. Finding the middle 8 doesn't tell us where the sequence starts or ends!
To find the absolute first position (Left Bound):
- We execute a standard binary search.
- If we hit
nums[mid] == target, we log that index as a potential answer. - However, we do not stop. We aggressively pinch our
rightpointer tomid - 1to force the search into the left half to see if an even earlier occurrence exists!
To find the absolute last position (Right Bound):
- If we hit
nums[mid] == target, we log that index. - We aggressively squeeze our
leftpointer tomid + 1to force the search into the right half to map out the final trailing8.
graph TD
A["nums = [5, 7, 7, 8, 8, 10] | Target: 8"]
B("Search Left Bound") --> C("Hit index 4 (8). Update ans=4")
C --> D("Squeeze right to mid-1. Search [5, 7, 7, 8]")
D --> E("Hit index 3 (8). Update ans=3")
F("Search Right Bound") --> G("Hit index 3 (8). Update ans=3")
G --> H("Squeeze left to mid+1. Search [8, 10]")
H --> I("Hit index 4 (8). Update ans=4")
style E fill:#FF9800,stroke:#fff,stroke-width:2px,color:#fff
style I fill:#2196F3,stroke:#fff,stroke-width:2px,color:#fff
class Solution:
def searchRange(self, nums: List[int], target: int) -> List[int]:
left_bound = self.binary_search(nums, target, True)
right_bound = self.binary_search(nums, target, False)
return [left_bound, right_bound]
def binary_search(self, nums, target, is_searching_left):
left, right = 0, len(nums) - 1
pos = -1
while left <= right:
mid = (left + right) // 2
if nums[mid] > target:
right = mid - 1
elif nums[mid] < target:
left = mid + 1
else:
pos = mid
if is_searching_left:
# Target hit! But aggressively map the left chunk
right = mid - 1
else:
# Target hit! But aggressively map the right chunk
left = mid + 1
return pos
Complexity Analysis
| Metric | Complexity | Explanation |
|---|---|---|
| Time | O(log N) | We strictly run two separate and fundamentally isolated continuous O(log N) binary searches. Mathematically 2 * log(N), which collapses into pure O(log N) asymptotic time. |
| Space | O(1) | Pure iterative array-pointer tracking natively consumes memory equivalent to two static integers left and right. |