Understanding upper bound and lower bound in C++ is a fundamental milestone for any programmer working with the Standard Template Library (STL). These two functions, defined in the <algorithm> header, are the backbone of efficient searching in sorted ranges. While std::binary_search merely tells you if an element exists, lower_bound and upper_bound tell you where it is or where it should be. Mastering them unlocks the ability to write cleaner, faster, and more reliable code for competitive programming, system design, and general application development No workaround needed..
What Are Lower Bound and Upper Bound?
At their core, both functions perform a binary search on a sorted range (ascending order by default). They return an iterator pointing to a specific position within that range. The critical difference lies in how they handle duplicate values and the exact boundary they define Less friction, more output..
std::lower_bound
The lower_bound function returns an iterator pointing to the first element in the range that is not less than (i.e., greater than or equal to) the given value.
- If the value exists: It points to the first occurrence of that value.
- If the value does not exist: It points to the position where the value could be inserted without violating the sorting order (the first element strictly greater than the value).
- If all elements are smaller: It returns the
end()iterator.
std::upper_bound
The upper_bound function returns an iterator pointing to the first element in the range that is greater than the given value.
- If the value exists: It points to the element immediately after the last occurrence of that value.
- If the value does not exist: It points to the same position as
lower_bound(the first element strictly greater than the value). - If all elements are smaller or equal: It returns the
end()iterator.
Syntax and Parameters
Both functions share identical signatures, offering flexibility for different use cases.
// Default version (uses operator<)
template
ForwardIt lower_bound(ForwardIt first, ForwardIt last, const T& value);
template
ForwardIt upper_bound(ForwardIt first, ForwardIt last, const T& value);
// Custom comparator version
template
ForwardIt lower_bound(ForwardIt first, ForwardIt last, const T& value, Compare comp);
template
ForwardIt upper_bound(ForwardIt first, ForwardIt last, const T& value, Compare comp);
Parameters:
first,last: The range of elements to examine. The range must be sorted (or at least partitioned with respect tovalue).value: The value to compare against.comp(Optional): A binary predicate (function object or lambda) returningtrueif the first argument is less than the second. This allows searching in descending order or custom structs.
Return Value: An iterator to the found position Less friction, more output..
Complexity: Logarithmic — O(log N) comparisons (where N = last - first). For non-random-access iterators (like std::list), the number of iterator increments is linear, though comparisons remain logarithmic.
Visualizing the Difference
Imagine a sorted vector: v = {10, 20, 20, 20, 30, 40}
| Target Value | lower_bound Result (Index/Value) |
upper_bound Result (Index/Value) |
Explanation |
|---|---|---|---|
| 20 | Index 2 (Value 20) | Index 5 (Value 30) | lower finds first 20. |
| 50 | Index 6 (end()) |
Index 6 (end()) |
Larger than all. upper finds first element > 20. That's why |
| 5 | Index 0 (Value 10) | Index 0 (Value 10) | Smaller than all. Still, both point to beginning. |
| 25 | Index 5 (Value 30) | Index 5 (Value 30) | 25 not found. Both point to insertion point (30). Both point to end. |
The range [lower_bound(val), upper_bound(val)) represents the equal range — all elements exactly equal to val. This is exactly what std::equal_range returns.
Practical Code Examples
1. Basic Usage with std::vector
#include
#include
#include // Required for lower_bound, upper_bound
int main() {
std::vector data = {2, 5, 5, 5, 8, 10, 12};
int target = 5;
// lower_bound: First element >= 5
auto low = std::lower_bound(data.begin(), data.end(), target);
// upper_bound: First element > 5
auto up = std::upper_bound(data.begin(), data.
std::cout << "Data: ";
for(int x : data) std::cout << x << " ";
std::cout << "\nTarget: " << target << "\n";
if (low != data.Which means end())
std::cout << "lower_bound at index: " << (low - data. begin()) << " (Value: " << *low << ")\n";
if (up != data.end())
std::cout << "upper_bound at index: " << (up - data.
// Count occurrences of target
std::cout << "Count of " << target << ": " << (up - low) << "\n";
return 0;
}
Output:
Data: 2 5 5 5 8 10 12
Target: 5
lower_bound at index: 1 (Value: 5)
upper_bound at index: 4 (Value: 8)
Count of 5: 3
2. Using Custom Comparators (Descending Order)
By default, STL assumes ascending order (std::less). If your container is sorted in descending order, you must provide std::greater<>() or a custom lambda Took long enough..
#include
#include
#include
#include // for std::greater
int main() {
// Sorted in DESCENDING order
std::vector data = {50, 40, 40, 30, 20, 10};
int target = 40;
// Note: std::greater() means "a > b" defines the sorting logic.
// lower_bound finds first element NOT less than target (in terms of the comparator).
Worth adding: // With greater: "not (element > target)" -> "element <= target". Consider this: // So it finds the first element <= 40. Day to day, auto low = std::lower_bound(data. So begin(), data. Here's the thing — end(), target, std::greater());
// upper_bound finds first element GREATER than target (in terms of comparator). // With greater: "target > element" -> "40 > element".
In practice, // So it finds the first element < 40. auto up = std::upper_bound(data.begin(), data.
std::cout << "lower_bound (first <= 40): Index " << (low - data.begin()) << " Value: " << *low << "\n";
std::cout << "upper_bound (first <
40): Index " << (up - data.begin()) << " Value: " << *up << "\n";
return 0;
}
Output:
lower_bound (first <= 40): Index 2 Value: 40
upper_bound (first < 40): Index 4 Value: 30
Notice how the logic flips compared to ascending order. With std::greater<int>(), lower_bound no longer searches for the first element ≥ target — it finds the first element ≤ target. Similarly, upper_bound locates the first element < target rather than >. This inversion is critical when working with reverse-sorted containers or custom ordering criteria Practical, not theoretical..
And yeah — that's actually more nuanced than it sounds.
3. Counting Occurrences Efficiently
A standout most common practical applications of lower_bound and upper_bound together is counting how many times a value appears in a sorted container — all in O(log n) time.
#include
#include
#include
int main() {
std::vector scores = {10, 20, 20, 20, 30, 40, 40, 50};
int query = 20;
auto [low, high] = std::equal_range(scores.begin(), scores.end(), query);
int count = high - low;
std::cout << "Score " << query << " appears " << count << " times.\n";
// Print all matching elements
std::cout << "Matching elements: ";
for (auto it = low; it != high; ++it)
std::cout << *it << " ";
std::cout << "\n";
return 0;
}
Output:
Score 20 appears 3 times.
Matching elements: 20 20 20
Here, std::equal_range internally calls lower_bound and upper_bound as a single optimized operation, returning a std::pair of iterators that bracket every occurrence of query Simple, but easy to overlook. Simple as that..
4. Using lower_bound with User-Defined Types
When working with structs or classes, you can pass a custom comparator that defines how elements are ordered.
#include
#include
#include
struct Student {
std::string name;
int grade;
};
int main() {
std::vector students = {
{"Alice", 70}, {"Bob", 75}, {"Charlie", 80}, {"Diana", 85}
};
int targetGrade = 80;
// Find first student with grade >= 80
auto it = std::lower_bound(students.begin(), students.end(), targetGrade,
{
return s.
if (it != students.end())
std::cout << "First student with grade >= " << targetGrade
<< ": " << it->name << " (" << it->grade << ")\n";
return 0;
}
Output:
First student with grade >= 80: Charlie (80)
The lambda comparator tells lower_bound how to compare a Student object against an int, enabling heterogeneous lookups without needing to construct a full Student object for the search key That's the part that actually makes a difference..
5. std::binary_search — Existence Check
A close relative is std::binary_search, which returns only a bool indicating whether a value exists in the sorted range. It uses the same logarithmic divide-and-conquer strategy but discards positional information And that's really what it comes down to..
#include
#include
#include
int main() {
std::vector data = {1, 3, 5, 7, 9};
bool found = std::binary_search(data.begin(), data.end(), 5);
std::cout << "Is 5 present? " << (found ?
found
```cpp
found = std::binary_search(data.begin(), data.end(), 4);
std::cout << "Is 4 present? " << (found ? "Yes" : "No") << "\n";
return 0;
}
Output:
Is 5 present? Yes
Is 4 present? No
While binary_search is ideal for simple existence checks, remember that it abstracts away the position of the element. If you need the actual element or its range, lower_bound or equal_range are more appropriate Most people skip this — try not to..
6. Performance Comparison and Practical Implications
All these algorithms—std::lower_bound, std::upper_bound, std::equal_range, and std::binary_search—operate in O(log n) time on random-access iterators (like those from std::vector). On the flip side, they require the range to be partitioned with respect to the comparison criterion, which is typically achieved by sorting. The slight performance differences among them are negligible in most cases, as they all perform a similar number of comparisons And that's really what it comes down to..
This is the bit that actually matters in practice.
- Existence only:
std::binary_search - First occurrence (or insertion point):
std::lower_bound - One past the last occurrence:
std::upper_bound - Range of equal elements:
std::equal_range
7. Handling Multiset-Like Behavior with std::multiset
For those who need a container that maintains sorted order and allows duplicates out of the box, std::multiset provides built-in support for these operations. Its member functions lower_bound, upper_bound, and equal_range offer the same logarithmic complexity but are optimized for the container's internal structure The details matter here..
#include
#include
int main() {
std::multiset ms = {10, 20, 20, 30, 40, 40, 50};
auto range = ms.first; it !equal_range(20);
std::cout << "Count of 20 in multiset: ";
for (auto it = range.= range.
return 0;
}
Output:
Count of 20 in multiset: 20 20
Conclusion
The C++ Standard Library provides a powerful set of binary search algorithms that bring the efficiency of divide-and-conquer to your code. By mastering std::lower_bound, std::upper_bound, std::equal_range, and std::binary_search, you gain precise control over how you query sorted data. Whether you're checking existence, finding insertion points, or extracting ranges of equal elements, these tools ensure your operations remain optimal at O(log n) complexity. Remember that their effectiveness hinges on sorted data, and for custom types, a well-defined comparator is your key to unlocking their full potential. Embrace these algorithms, and you'll handle the complexities of sorted collections with confidence and efficiency.