Upper Bound¶
Question¶
Given a sorted array nums and an integer x, find the upper bound of x. The upper bound is the index of the first element in the array that is strictly greater than \(x\) (nums[i] > x). If no such element exists, return the size of the array.
Solution¶
Pattern¶
Binary Search for Boundary (Predicate Search) Instead of stopping when nums[mid] == x, we evaluate a predicate: nums[mid] > x. If true, we record mid as a potential answer and continue searching the left half to see if an earlier valid index exists. If false, we discard the left half and search the right half.
How to Identify¶
- The array is explicitly stated to be sorted.
- The prompt asks for the "first", "last", "smallest index", or "largest index" satisfying a condition.
- You need to find where an element should be inserted to maintain sorted order, specifically after all existing duplicates of that element.
Description¶
Step-by-step explanation:
- Initialize
left = 0andright = nums.length. We initializerightto the length of the array (out of bounds) because if \(x\) is greater than or equal to every element, the correct insertion point/upper bound is the very end of the array. - Loop while
left < right. We use<instead of<=because our search space is defined as[left, right). - Calculate
mid = left + (right - left) / 2to prevent integer overflow. - Evaluate the condition:
nums[mid] > x. - If TRUE: The element at
midis strictly greater than \(x\). This meansmidcould be the upper bound. But there might be valid elements to its left. Therefore, we shrink our window by settingright = mid. We do not domid - 1becausemiditself is still a valid candidate. - If FALSE: The element at
midis less than or equal to \(x\). It cannot possibly be the upper bound, nor can anything to its left. We definitively discard it by settingleft = mid + 1. - When the loop terminates,
leftwill equalright, pointing exactly to the first element \(> x\), ornums.lengthif all elements were \(\le x\). Returnleft.
The Intuition¶
Think of binary search as a boundary-finding tool rather than a value-finding tool. Imagine painting all elements in the array: Elements less than or equal to \(x\) are painted RED. Elements strictly greater than \(x\) are painted GREEN. Because the array is sorted, the colors will look like this: [RED, RED, RED, GREEN, GREEN]. The upper bound is simply asking: "Find the index of the very first GREEN element." If nums[mid] is GREEN, the first GREEN element must be at mid or somewhere to its left. If nums[mid] is RED, the first GREEN element must be strictly to its right. By repeatedly halving the space, the left and right pointers eventually squeeze together onto the exact boundary where RED turns to GREEN.
Complexity¶
| Label | Worst | Average |
|---|---|---|
| Time Complexity | \(O(\log N)\) | \(O(\log N)\) |
| Space Complexity | \(O(1)\) | \(O(1)\) |
Time Complexity¶
The search space is halved in every iteration of the while loop. For an array of size \(N\), this takes at most \(\log_2(N)\) iterations.
Space Complexity¶
The algorithm uses only three integer variables (left, right, mid), operating entirely in place. Therefore, the auxiliary space is strictly \(O(1)\).
Code¶
class Solution {
public int upperBound(int[] nums, int x) {
if (nums == null) return 0;
// Search space is [0, n).
// We initialize right to n because the answer could be n if x is >= all elements.
int left = 0;
int right = nums.length;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] > x) {
// mid is strictly greater than x. It is a valid candidate for upper bound.
// We keep it in the search space and look left for an earlier one.
right = mid;
} else {
// mid is <= x. It cannot be the upper bound. Discard it.
left = mid + 1;
}
}
return left;
}
}
Caveats¶
- Lower Bound vs Upper Bound: The only difference between Lower Bound and Upper Bound is the predicate.
- Lower Bound:
nums[mid] >= x(First element greater than OR equal to \(x\)) - Upper Bound:
nums[mid] > x(First element strictly greater than \(x\)) - Loop Condition and Shrinking: If you use
while (left <= right)andright = mid - 1(the standard exact-match binary search template), it requires a separately trackedansvariable to work correctly. Mixing the[left, right)boundaries (while(left < right)) withright = mid - 1will result in skipping valid answers or infinite loops. You must memorize which template you are using and stick to its specific boundary rules. left = midInfinite Loop: Notice we never writeleft = mid. In integer division,midbiases towards the left. Ifleftandrightare adjacent,midwill equalleft. If the logic branches toleft = mid, the state doesn't change, causing an infinite loop. The[left, right)template perfectly avoids this by only doingright = midorleft = mid + 1.
Concepts to Think About¶
- Counting Occurrences: You can find the exact number of times a target \(x\) appears in a sorted array by calculating
UpperBound(x) - LowerBound(x). This takes \(O(\log N)\) time, which is much faster than finding one instance and linearly scanning left and right (\(O(N)\) time). - Binary Search on Answer: This exact boundary-finding logic is the core engine for advanced DP/Greedy problems where you aren't searching an array, but rather searching a mathematical range of possible answers (e.g., LeetCode 875: Koko Eating Bananas, LeetCode 1011: Capacity to Ship Packages).
Logical Follow-up¶
Question: Given a sorted array with duplicates, how would you find the index of the last occurrence of \(x\)?
Solution: Find the Upper Bound of \(x\), and then subtract 1. Because the Upper Bound is the first element strictly greater than \(x\), the element immediately preceding it must be the last occurrence of \(x\) (provided \(x\) actually exists in the array, which you verify by checking nums[upperBound - 1] == x). This takes \(O(\log N)\) time.
Question: "Given a sorted array nums, return the count of a specific number \(x\). For example, if nums = [1, 2, 2, 2, 3] and \(x = 2\), the output should be \(3\)."
Solution: The count of \(x\) is simply \(\text{upperBound}(x) - \text{lowerBound}(x)\). 1. Run lowerBound to find the first index of \(x\). 2. Run upperBound to find the first index after the last \(x\). 3. The difference is the total frequency. This is \(O(\log n)\).
Question (Koko Eating Bananas): "There are \(n\) piles of bananas, where the \(i^{th}\) pile has \(piles[i]\) bananas. Guards will be gone for \(H\) hours. Koko can decide her bananas-per-hour eating speed \(k\). Each hour, she chooses a pile and eats \(k\) bananas from it. If the pile has less than \(k\), she eats them all and doesn't eat any more during that hour. Koko wants to finish all bananas within \(H\) hours. Return the minimum integer \(k\) such that she can eat all the bananas within \(H\) hours."
L5 Analysis & Solution: This is a "Binary Search on Answer" problem, where the search space is the possible speed \(k\).
- Search Space: The minimum speed is \(1\) (she must eat something). The maximum speed is \(\max(piles)\) (at this speed, she finishes any pile in 1 hour).
- Monotonicity: If Koko can finish at speed \(k\), she can definitely finish at speed \(k+1\). If she can't finish at speed \(k\), she definitely can't finish at any speed slower than \(k\). This is a sorted predicate
[false, false, true, true]. - Predicate Function: Write a
canFinish(speed, H)helper:- Iterate through piles:
hoursUsed += Math.ceil(piles[i] / speed). - Return
hoursUsed <= H.
- Iterate through piles:
- Binary Search: Use your Lower Bound template on the range \([1, \max(piles)]\) to find the smallest speed that returns
true.