Facebook Pixel

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:

  1. Input: The array is sorted in ascending order and may contain duplicate values. The method takes a target number as its parameter.

  2. 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
  3. 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) returns 2 (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) returns 5 (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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 than 6
  • Binary search will lead us to index 6 (where 7 is located)
  • Step back one position to index 5, and we find the last 6

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 move right = mid (this position might be our answer for "first greater")
  • When this[mid] <= target, we move left = 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:

  1. Initialize binary search boundaries:

    let left = 0;
    let right = this.length;

    We set right to the array length (not length - 1) to handle the case where all elements are less than or equal to the target.

  2. 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 to Math.floor((left + right) / 2))
    • If this[mid] > target: Move right to mid because we found an element greater than target
    • If this[mid] <= target: Move left to mid + 1 to continue searching for the first element greater than target
  3. 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

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 Evaluator

Example 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, set left = 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, set right = 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, set right = mid = 4
  • New range: left = 4, right = 4

Loop ends (left == right)

Final Check:

  • left = 4 (pointing to first element greater than 5)
  • Check: left > 0 ✓ and this[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 where 7 > 5)
  • Check: this[left - 1] = this[1] = 35
  • 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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More