2774. Array Upper Bound 🔒
Problem Description
This problem asks you to extend JavaScript's Array prototype with a custom method called upperBound()
. The method should work on sorted arrays of numbers (in ascending order) and find the last occurrence of a given target number.
The key requirements are:
-
Input: The array is sorted in ascending order and may contain duplicate values. The method takes a
target
number as its parameter. -
Output:
- If the
target
exists in the array, return the index of its last occurrence - If the
target
doesn't exist in the array, return-1
- If the
-
Implementation: The method should be added to all arrays through prototype extension, allowing you to call it directly on any array instance like
[3,4,5].upperBound(5)
.
Examples:
[3,4,5].upperBound(5)
returns2
(the last index where 5 appears)[1,4,5].upperBound(2)
returns-1
(2 is not in the array)[3,4,6,6,6,6,7].upperBound(6)
returns5
(the last index where 6 appears, even though 6 appears multiple times)
The solution uses a binary search approach to efficiently find the position just after the last occurrence of the target, then checks if the element at the previous position equals the target to determine the result.
Intuition
Since we need to find the last occurrence of a target in a sorted array, we can think about this problem as finding the first element that is greater than our target. Once we find that position, the element just before it (if it exists and equals the target) would be our answer.
The key insight is that in a sorted array with duplicates, all occurrences of a number are grouped together. So if we can find where the next larger number starts, we can step back one position to find the last occurrence of our target.
Consider the array [3,4,6,6,6,6,7]
and target 6
:
- Instead of searching for the last
6
directly, we search for the position where elements become greater than6
- Binary search will lead us to index
6
(where7
is located) - Step back one position to index
5
, and we find the last6
This approach naturally handles the case where the target doesn't exist. If we find the position where elements become greater than the target, but the previous element isn't equal to the target, then the target was never in the array.
The binary search modification is subtle but important:
- When
this[mid] > target
, we moveright = mid
(this position might be our answer for "first greater") - When
this[mid] <= target
, we moveleft = mid + 1
(we need to go further right to find something greater)
This ensures we always converge to the first position where an element is greater than the target. The final check this[left - 1] == target
confirms whether the target actually exists in the array.
Solution Approach
The implementation extends the Array prototype to add the upperBound
method using a modified binary search algorithm.
Step-by-step implementation:
-
Initialize binary search boundaries:
let left = 0; let right = this.length;
We set
right
to the array length (notlength - 1
) to handle the case where all elements are less than or equal to the target. -
Binary search loop:
while (left < right) { const mid = (left + right) >> 1; if (this[mid] > target) { right = mid; } else { left = mid + 1; } }
- Calculate
mid
using bit shift>> 1
(equivalent toMath.floor((left + right) / 2)
) - If
this[mid] > target
: Moveright
tomid
because we found an element greater than target - If
this[mid] <= target
: Moveleft
tomid + 1
to continue searching for the first element greater than target
- Calculate
-
Determine the result:
return left > 0 && this[left - 1] == target ? left - 1 : -1;
After the loop,
left
points to the first position where an element is greater than the target (or the array's end if no such element exists).- Check if
left > 0
to ensure we don't access an invalid index - Check if
this[left - 1] == target
to verify the target exists - If both conditions are true, return
left - 1
(the last occurrence index) - Otherwise, return
-1
- Check if
Time Complexity: O(log n)
where n is the array length, due to binary search.
Space Complexity: O(1)
as we only use a constant amount of extra space.
The beauty of this approach is that it leverages the sorted nature of the array to find the boundary between elements that are <= target
and elements that are > target
, making it efficient even for arrays with many duplicates.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through the algorithm with array [2, 5, 5, 5, 8, 9]
and target 5
.
Goal: Find the last occurrence of 5
(which should be index 3
).
Initial State:
- Array:
[2, 5, 5, 5, 8, 9]
left = 0
,right = 6
(array length)- Target:
5
Iteration 1:
mid = (0 + 6) >> 1 = 3
this[3] = 5
, which equals target- Since
5 <= 5
, setleft = mid + 1 = 4
- New range:
left = 4
,right = 6
Iteration 2:
mid = (4 + 6) >> 1 = 5
this[5] = 9
, which is greater than target- Since
9 > 5
, setright = mid = 5
- New range:
left = 4
,right = 5
Iteration 3:
mid = (4 + 5) >> 1 = 4
this[4] = 8
, which is greater than target- Since
8 > 5
, setright = mid = 4
- New range:
left = 4
,right = 4
Loop ends (left == right)
Final Check:
left = 4
(pointing to first element greater than5
)- Check:
left > 0
✓ andthis[left - 1] = this[3] = 5
equals target ✓ - Return
left - 1 = 3
The algorithm correctly identifies index 3
as the last occurrence of 5
.
Edge Case Example: Array [1, 3, 7]
with target 5
- Binary search converges to
left = 2
(position where7 > 5
) - Check:
this[left - 1] = this[1] = 3
≠5
- Return
-1
(target not found)
Solution Implementation
1def upperBound(arr, target):
2 """
3 Binary search implementation to find the upper bound of a target value.
4 Returns the index of the last occurrence of the target value if found,
5 otherwise returns -1.
6
7 Args:
8 arr: The sorted array to search in
9 target: The value to search for in the array
10
11 Returns:
12 The index of the last occurrence of target, or -1 if not found
13
14 Time Complexity: O(log n)
15 Space Complexity: O(1)
16 """
17 # Initialize binary search boundaries
18 left = 0
19 right = len(arr)
20
21 # Binary search to find the first element greater than target
22 while left < right:
23 # Calculate middle index using integer division
24 mid = (left + right) // 2
25
26 # If middle element is greater than target, search in left half
27 if arr[mid] > target:
28 right = mid
29 else:
30 # Otherwise, search in right half
31 left = mid + 1
32
33 # Check if the element before the found position equals target
34 # If yes, return its index (last occurrence), otherwise return -1
35 return left - 1 if left > 0 and arr[left - 1] == target else -1
36
37
38# Example usage:
39# upperBound([3, 4, 5], 5) # Returns 2 (index of last occurrence of 5)
40# upperBound([1, 4, 5], 2) # Returns -1 (2 not found in array)
41# upperBound([3, 4, 6, 6, 6, 6, 7], 6) # Returns 5 (index of last occurrence of 6)
42
1/**
2 * Binary search implementation to find the upper bound of a target value.
3 * Returns the index of the last occurrence of the target value if found,
4 * otherwise returns -1.
5 *
6 * Time Complexity: O(log n)
7 * Space Complexity: O(1)
8 */
9public class Solution {
10
11 /**
12 * Finds the upper bound (last occurrence) of a target value in a sorted array.
13 *
14 * @param arr - The sorted array to search in
15 * @param target - The value to search for in the array
16 * @return The index of the last occurrence of target, or -1 if not found
17 */
18 public int upperBound(int[] arr, int target) {
19 // Initialize binary search boundaries
20 int left = 0;
21 int right = arr.length;
22
23 // Binary search to find the first element greater than target
24 while (left < right) {
25 // Calculate middle index using bit shift for efficiency
26 int mid = (left + right) >> 1;
27
28 // If middle element is greater than target, search in left half
29 if (arr[mid] > target) {
30 right = mid;
31 } else {
32 // Otherwise, search in right half
33 left = mid + 1;
34 }
35 }
36
37 // Check if the element before the found position equals target
38 // If yes, return its index (last occurrence), otherwise return -1
39 return left > 0 && arr[left - 1] == target ? left - 1 : -1;
40 }
41
42 /**
43 * Example usage:
44 * Solution solution = new Solution();
45 * int[] arr1 = {3, 4, 5};
46 * solution.upperBound(arr1, 5); // Returns 2 (index of last occurrence of 5)
47 *
48 * int[] arr2 = {1, 4, 5};
49 * solution.upperBound(arr2, 2); // Returns -1 (2 not found in array)
50 *
51 * int[] arr3 = {3, 4, 6, 6, 6, 6, 7};
52 * solution.upperBound(arr3, 6); // Returns 5 (index of last occurrence of 6)
53 */
54}
55
1#include <vector>
2#include <algorithm>
3
4class Solution {
5public:
6 /**
7 * Binary search implementation to find the upper bound of a target value.
8 * Returns the index of the last occurrence of the target value if found,
9 * otherwise returns -1.
10 *
11 * @param nums - The sorted array to search in
12 * @param target - The value to search for in the array
13 * @return The index of the last occurrence of target, or -1 if not found
14 *
15 * Time Complexity: O(log n)
16 * Space Complexity: O(1)
17 */
18 int upperBound(std::vector<int>& nums, int target) {
19 // Initialize binary search boundaries
20 int left = 0;
21 int right = nums.size();
22
23 // Binary search to find the first element greater than target
24 while (left < right) {
25 // Calculate middle index, using right shift for efficiency
26 // Equivalent to (left + right) / 2 but avoids potential overflow
27 int mid = left + ((right - left) >> 1);
28
29 // If middle element is greater than target, search in left half
30 if (nums[mid] > target) {
31 right = mid;
32 } else {
33 // Otherwise, search in right half
34 left = mid + 1;
35 }
36 }
37
38 // Check if the element before the found position equals target
39 // If yes, return its index (last occurrence), otherwise return -1
40 return (left > 0 && nums[left - 1] == target) ? left - 1 : -1;
41 }
42};
43
44// Example usage:
45// Solution sol;
46// std::vector<int> nums1 = {3, 4, 5};
47// sol.upperBound(nums1, 5); // Returns 2 (index of last occurrence of 5)
48//
49// std::vector<int> nums2 = {1, 4, 5};
50// sol.upperBound(nums2, 2); // Returns -1 (2 not found in array)
51//
52// std::vector<int> nums3 = {3, 4, 6, 6, 6, 6, 7};
53// sol.upperBound(nums3, 6); // Returns 5 (index of last occurrence of 6)
54
1// Extend the global Array interface to include the upperBound method
2declare global {
3 interface Array<T> {
4 upperBound(target: number): number;
5 }
6}
7
8/**
9 * Binary search implementation to find the upper bound of a target value.
10 * Returns the index of the last occurrence of the target value if found,
11 * otherwise returns -1.
12 *
13 * @param target - The value to search for in the array
14 * @returns The index of the last occurrence of target, or -1 if not found
15 *
16 * Time Complexity: O(log n)
17 * Space Complexity: O(1)
18 */
19Array.prototype.upperBound = function (target: number): number {
20 // Initialize binary search boundaries
21 let left: number = 0;
22 let right: number = this.length;
23
24 // Binary search to find the first element greater than target
25 while (left < right) {
26 // Calculate middle index using bit shift for efficiency
27 const mid: number = (left + right) >> 1;
28
29 // If middle element is greater than target, search in left half
30 if (this[mid] > target) {
31 right = mid;
32 } else {
33 // Otherwise, search in right half
34 left = mid + 1;
35 }
36 }
37
38 // Check if the element before the found position equals target
39 // If yes, return its index (last occurrence), otherwise return -1
40 return left > 0 && this[left - 1] === target ? left - 1 : -1;
41};
42
43// Example usage:
44// [3,4,5].upperBound(5); // Returns 2 (index of last occurrence of 5)
45// [1,4,5].upperBound(2); // Returns -1 (2 not found in array)
46// [3,4,6,6,6,6,7].upperBound(6); // Returns 5 (index of last occurrence of 6)
47
Time and Space Complexity
Time Complexity: O(log n)
where n
is the length of the array. The algorithm uses binary search to find the position. In each iteration, the search space is halved by comparing the middle element with the target. The while loop runs at most log₂(n)
times since we divide the search space by 2 in each iteration.
Space Complexity: O(1)
- The algorithm only uses a constant amount of extra space for the variables left
, right
, and mid
. No additional data structures are created that scale with the input size. The modification is done to the Array prototype itself, but this doesn't count toward the space complexity of the algorithm execution.
Common Pitfalls
1. Off-by-One Error in Binary Search Boundary
Pitfall: A common mistake is initializing right = len(arr) - 1
instead of right = len(arr)
. This can cause the algorithm to fail when all elements in the array are less than or equal to the target.
Example of the issue:
# Incorrect implementation
def upperBound_wrong(arr, target):
left = 0
right = len(arr) - 1 # WRONG: should be len(arr)
while left < right:
mid = (left + right) // 2
if arr[mid] > target:
right = mid
else:
left = mid + 1
return left - 1 if left > 0 and arr[left - 1] == target else -1
# This fails for: upperBound_wrong([1, 2, 3], 3)
# Returns -1 instead of 2
Solution: Always initialize right = len(arr)
to properly handle the case where the target is greater than or equal to all elements.
2. Integer Overflow in Mid Calculation
Pitfall: In languages with fixed-size integers, calculating mid = (left + right) // 2
can cause integer overflow when left
and right
are very large.
Solution: Use mid = left + (right - left) // 2
to avoid overflow:
def upperBound_safe(arr, target):
left = 0
right = len(arr)
while left < right:
# Overflow-safe mid calculation
mid = left + (right - left) // 2
if arr[mid] > target:
right = mid
else:
left = mid + 1
return left - 1 if left > 0 and arr[left - 1] == target else -1
3. Incorrect Loop Condition
Pitfall: Using left <= right
instead of left < right
can cause infinite loops or incorrect results.
Example of the issue:
# Incorrect loop condition
def upperBound_infinite(arr, target):
left = 0
right = len(arr)
while left <= right: # WRONG: can cause infinite loop
mid = (left + right) // 2
if arr[mid] > target:
right = mid # When left == right == mid, this creates infinite loop
else:
left = mid + 1
return left - 1 if left > 0 and arr[left - 1] == target else -1
Solution: Always use left < right
as the loop condition when right
is initialized to len(arr)
.
4. Forgetting to Check Array Bounds
Pitfall: Not checking if left > 0
before accessing arr[left - 1]
can cause an IndexError when the target is smaller than all elements.
Example of the issue:
# Missing bounds check
def upperBound_no_check(arr, target):
left = 0
right = len(arr)
while left < right:
mid = (left + right) // 2
if arr[mid] > target:
right = mid
else:
left = mid + 1
# WRONG: Missing left > 0 check
return left - 1 if arr[left - 1] == target else -1
# This crashes when target < arr[0], as left becomes 0
Solution: Always verify left > 0
before accessing arr[left - 1]
to prevent index out of bounds errors.
What's the output of running the following function using input 56
?
1KEYBOARD = {
2 '2': 'abc',
3 '3': 'def',
4 '4': 'ghi',
5 '5': 'jkl',
6 '6': 'mno',
7 '7': 'pqrs',
8 '8': 'tuv',
9 '9': 'wxyz',
10}
11
12def letter_combinations_of_phone_number(digits):
13 def dfs(path, res):
14 if len(path) == len(digits):
15 res.append(''.join(path))
16 return
17
18 next_number = digits[len(path)]
19 for letter in KEYBOARD[next_number]:
20 path.append(letter)
21 dfs(path, res)
22 path.pop()
23
24 res = []
25 dfs([], res)
26 return res
27
1private static final Map<Character, char[]> KEYBOARD = Map.of(
2 '2', "abc".toCharArray(),
3 '3', "def".toCharArray(),
4 '4', "ghi".toCharArray(),
5 '5', "jkl".toCharArray(),
6 '6', "mno".toCharArray(),
7 '7', "pqrs".toCharArray(),
8 '8', "tuv".toCharArray(),
9 '9', "wxyz".toCharArray()
10);
11
12public static List<String> letterCombinationsOfPhoneNumber(String digits) {
13 List<String> res = new ArrayList<>();
14 dfs(new StringBuilder(), res, digits.toCharArray());
15 return res;
16}
17
18private static void dfs(StringBuilder path, List<String> res, char[] digits) {
19 if (path.length() == digits.length) {
20 res.add(path.toString());
21 return;
22 }
23 char next_digit = digits[path.length()];
24 for (char letter : KEYBOARD.get(next_digit)) {
25 path.append(letter);
26 dfs(path, res, digits);
27 path.deleteCharAt(path.length() - 1);
28 }
29}
30
1const KEYBOARD = {
2 '2': 'abc',
3 '3': 'def',
4 '4': 'ghi',
5 '5': 'jkl',
6 '6': 'mno',
7 '7': 'pqrs',
8 '8': 'tuv',
9 '9': 'wxyz',
10}
11
12function letter_combinations_of_phone_number(digits) {
13 let res = [];
14 dfs(digits, [], res);
15 return res;
16}
17
18function dfs(digits, path, res) {
19 if (path.length === digits.length) {
20 res.push(path.join(''));
21 return;
22 }
23 let next_number = digits.charAt(path.length);
24 for (let letter of KEYBOARD[next_number]) {
25 path.push(letter);
26 dfs(digits, path, res);
27 path.pop();
28 }
29}
30
Recommended Readings
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!