Topics › Binary Search › BS on 1D Arrays
Upper Bound
Java source ↗ Edit this article ↗
Jump to code:
Problem
You are given an array arr sorted in non-decreasing order (duplicates allowed) and an integer x. The upper bound of x is the smallest index i such that arr[i] > x, strictly greater. If no element is greater than x, return n, the length of the array. Equivalently, the upper bound is where x would be inserted to keep the array sorted while landing after every existing copy of x.
Constraints: 0 ≤ n ≤ 10^5, values and x fit in a 32-bit signed integer.
Example 1
arr = [1, 2, 2, 3, 3, 5], x = 2
answer = 3 // the 2s occupy indices 1..2; arr[3] = 3 is the first value > 2
Example 2
arr = [1, 2, 2, 3, 3, 5], x = 4
answer = 5 // 4 is absent; arr[5] = 5 is the first value > 4
Example 3
arr = [1, 2, 2, 3, 3, 5], x = 5
answer = 6 // nothing is > 5, so return n
Upper bound and lower bound (first index with arr[i] >= x) agree whenever x is absent; when x is present the lower bound points at its first copy and the upper bound points just past its last copy. The gap between them is therefore the number of copies, which is why both functions appear together so often.
Approach 1: Brute force, linear scan
Intuition. Scan from the left and stop at the first element that is strictly greater than x. Sorted order guarantees this is the smallest such index. Fall off the end, and the answer is n.
Algorithm.
- For every index
ifrom0ton - 1: ifarr[i] > x, returni. - Return
n.
static int bruteForce(int[] arr, int x) {
for (int i = 0; i < arr.length; i++) {
if (arr[i] > x) return i;
}
return arr.length;
}
Complexity. Time O(n) in the worst case, space O(1).
Approach 2: Optimal, binary search with a running answer
Intuition. The predicate arr[i] > x is false for some prefix of the sorted array and true afterwards: F F ... F T T ... T. We want the first T. Probing the middle tells us which side the boundary is on: a T at mid means the first T is at mid or to its left; an F means it is strictly to the right. We record every T we see in ans and keep moving left, so that the last recorded ans is the leftmost T. If we never see a T, ans keeps its initial value n.
The only difference from the lower bound is the comparison: > instead of >=. Everything else, including the proof of correctness, is identical.
Algorithm.
- Set
lo = 0,hi = n - 1,ans = n. - While
lo <= hi:mid = lo + (hi - lo) / 2.- If
arr[mid] > x: setans = mid,hi = mid - 1. - Else set
lo = mid + 1.
- Return
ans.
static int optimal(int[] arr, int x) {
int lo = 0, hi = arr.length - 1, ans = arr.length;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2;
if (arr[mid] > x) {
ans = mid; // mid qualifies; a smaller qualifying index may exist on the left
hi = mid - 1;
} else {
lo = mid + 1; // arr[mid] <= x, so every index <= mid is ruled out
}
}
return ans;
}
Dry run on arr = [1, 2, 2, 3, 3, 5], x = 2:
| lo | hi | mid | arr[mid] | arr[mid] > 2? | action |
|---|---|---|---|---|---|
| 0 | 5 | 2 | 2 | no | lo = 3 |
| 3 | 5 | 4 | 3 | yes | ans = 4, hi = 3 |
| 3 | 3 | 3 | 3 | yes | ans = 3, hi = 2 |
| 3 | 2 | lo > hi | return 3 |
The first probe landed on a copy of x itself. With >= (lower bound) that would have counted as a hit; with > it correctly counts as “not yet”, and the search moves right past all the 2s.
Complexity. Time O(log n), space O(1).
Approach 3: Optimal, reuse lower bound on x + 1
Intuition. For integers, “strictly greater than x” is the same as “greater than or equal to x + 1”. So upperBound(arr, x) == lowerBound(arr, x + 1). If you already have a well-tested lower bound, you get the upper bound for free, and you only need to maintain one binary search.
There is one trap: when x == Integer.MAX_VALUE, x + 1 overflows to Integer.MIN_VALUE, and the lower bound of the smallest possible integer is 0, which is wrong. But no int can be greater than Integer.MAX_VALUE, so the correct answer in that case is simply n. Handle it up front.
Algorithm.
- If
x == Integer.MAX_VALUE, returnn. - Otherwise return
lowerBound(arr, x + 1), wherelowerBoundis the standard half-open binary search for the first index witharr[i] >= value.
static int viaLowerBound(int[] arr, int x) {
if (x == Integer.MAX_VALUE) return arr.length; // no int is greater than x, and x + 1 would overflow
return lowerBound(arr, x + 1);
}
private static int lowerBound(int[] arr, int x) { // first index with arr[i] >= x, or n
int lo = 0, hi = arr.length;
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (arr[mid] >= x) hi = mid;
else lo = mid + 1;
}
return lo;
}
Complexity. Time O(log n), space O(1).
This trick only works for discrete types. For doubles there is no “next value after x” that is convenient to write, so use the direct > comparison from Approach 2 there.
Edge cases and pitfalls
xis the maximum element. Nothing is strictly greater, so the answer isn, even thoughxis present. Compare Example 3.xsmaller than every element. The first element already qualifies: answer0.- Empty array. Returns
0, which equalsn. - Duplicates. The upper bound skips over all copies of
x. The test[1, 2, 2, 3, 3, 5],x = 2returns3, not1or2. - Mixing up
>and>=. This is the single most common bug. Use>for upper bound,>=for lower bound, and check with an input wherexis present: the two answers must differ by the number of copies. - Overflow in
x + 1. Only relevant for Approach 3; guardInteger.MAX_VALUEexplicitly. The Java file tests[Integer.MIN_VALUE, Integer.MAX_VALUE]withx = Integer.MAX_VALUEand expects2. - Overflow in
mid. Uselo + (hi - lo) / 2.
Related problems
- Lower Bound is the non-strict twin; read it first if the “first
T” argument is new to you. - First and last occurrence uses
upperBound(x) - 1as the index of the last copy ofx. - Count Occurrences in a Sorted Array is
upperBound(x) - lowerBound(x). - Search X in sorted array is the plain binary search both bounds are built from.