Opening the reading…
Opening the reading…
BINARY SEARCH › UPPER BOUND AND LOWER BOUND
Open — the attempt gate is not wired up yet
This editorial is meant to unlock after you have run the problem at least once, with the worked solution behind one further deliberate click. That needs per-learner unlock state nothing stores today, so for now the whole article is open.
Try it yourself first →A normal binary search can stop as soon as it sees target because any occurrence is acceptable. This problem asks for a specific occurrence: the leftmost target and the rightmost target. When nums[mid] equals target, the answer may still lie farther left or farther right, so equality cannot end the search.
For the first position, treat target as a dividing line. Values smaller than target must stay to the left, while values equal to or greater than target may still contain the first occurrence, so you keep searching left after a match. For the last position, reverse that decision: values greater than target move right, while values less than or equal to target may still contain the last occurrence.
| MEASURE | BOUND | WHY |
|---|---|---|
| Time | O(log n) | Each binary search at least halves its remaining interval, so each performs O(log n) iterations. The searches run one after the other rather than nested, and 2 log n is still O(log n). |
| Space | O(1) extra | The searches keep only a fixed number of integer variables, so working memory does not grow with n. The returned two-element vector is required output and is excluded from the extra-space bound. |
#include <vector>
using namespace std;
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
int left = 0;
int right = static_cast<int>(nums.size()) - 1;
int first = -1;
int last = -1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] >= target) {
right = mid - 1;
} else {
left = mid + 1;
}
if (nums[mid] == target) {
first = mid;
}
}
if (first == -1) {
return {-1, -1};
}
left = 0;
right = static_cast<int>(nums.size()) - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] <= target) {
left = mid + 1;
} else {
right = mid - 1;
}
if (nums[mid] == target) {
last = mid;
}
}
return {first, last};
}
};The order inside each loop matters. The comparison moves the search boundary, while the equality check saves the current midpoint before that candidate disappears from the next interval. In the first loop, right moves left on equality; in the second, left moves right on equality. Those opposite movements are the entire difference between first and last.
A competent alternative is to use lower_bound for the first position and upper_bound for the position immediately after the last one. This is an optimisation in code length, not in asymptotic performance: both library searches are logarithmic and the extra space remains constant. It buys a compact implementation, while the explicit loops make the equality decisions easier to inspect when you are learning or debugging binary search.
#include <algorithm>
#include <vector>
using namespace std;
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
auto firstIt = lower_bound(nums.begin(), nums.end(), target);
if (firstIt == nums.end() || *firstIt != target) {
return {-1, -1};
}
auto afterLastIt = upper_bound(nums.begin(), nums.end(), target);
int first = static_cast<int>(firstIt - nums.begin());
int last = static_cast<int>(afterLastIt - nums.begin()) - 1;
return {first, last};
}
};lower_bound returns the first iterator whose value is not less than target, so it lands on the first target if one exists. upper_bound returns the first iterator whose value is greater than target, so subtracting one gives the last target. The explicit existence check is essential because lower_bound can legally return end when every value is smaller than target.