Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 4

Binary Search for Insertion and Range Finding

Recommended resources

Hello! Welcome to your fourth lesson.

In our last session, we built a robust, bug-free implementation of binary search for finding an exact target. You learned why details like loop conditions (left <= right) and pointer updates (mid + 1 / mid - 1) are critical for correctness.

Today, we'll adapt that core algorithm to solve more nuanced problems. Instead of just asking "Is the target here?", we will ask "Where should the target be?" and "Where does the range of this target begin and end?". This lesson directly addresses the learning outcome: Apply binary search to find insertion positions and search ranges in sorted arrays. Mastering these variations is a key step in leveling up your problem-solving skills for coding interviews.

1. The Lower Bound: Finding the Insertion Point

Our standard binary search stops as soon as it finds a match. But what if we need to find the first possible position for a target? This is useful for two scenarios:

  1. Finding the index to insert a new element while maintaining sorted order.
  2. Finding the starting index of a target that might have duplicates.

This position is formally known as the lower bound.

Lower Bound: The smallest index i in a sorted array such that nums[i] >= target.

Lower Bound and Upper Bound in a Sorted Array
This diagram illustrates the concepts of lower bound and upper bound. For a target of 3, the lower bound is the index of the first '3', and the upper bound is the index of the first element greater than '3'. The range of all '3's is called the equal range.

To find the lower bound, we modify our binary search logic: even when we find an element nums[mid] that is >= target, we don't stop. We record this mid as a potential answer and then try to find an even smaller index to its left.

Let's watch a video that explains this concept and walks through the implementation logic.

BS-2. Implement Lower Bound and Upper Bound | Search Insert Position | Floor and Ceil

This video from 'take U forward' provides an excellent, detailed explanation of the lower bound concept and how to implement it using a modified binary search.

Please watch from 00:00 to 02:40 to understand the definition, and then from 03:35 to 13:41 for a detailed dry-run and explanation of the code template. Focus on how the ans variable is updated and why the search continues to the left even after a potential answer is found.

Here is a Python implementation of the lower_bound function based on the logic from the video:

def lower_bound(nums, target):
    """
    Finds the first index i such that nums[i] >= target.
    Returns len(nums) if all elements are smaller than target.
    """
    left, right = 0, len(nums) - 1
    ans = len(nums)  # Default answer if no element is >= target

    while left <= right:
        mid = left + (right - left) // 2
        
        if nums[mid] >= target:
            # This is a potential answer. Store it and look for a better one (smaller index) to the left.
            ans = mid
            right = mid - 1
        else: # nums[mid] < target
            # The answer must be in the right half.
            left = mid + 1
            
    return ans

This exact logic solves the LeetCode problem "Search Insert Position" (LC 35). If the target is in the array, lower_bound will find its first occurrence. If it's not, it will find the index where it should be inserted. The video confirms this connection in the segment starting at 19:36.


2. Finding a Search Range: First and Last Position

A very common interview problem (LeetCode 34: Find First and Last Position of Element in Sorted Array) asks you to find the starting and ending index of a target value that may appear multiple times.

For example, in [5, 7, 7, 8, 8, 10], for target = 8, the answer is [3, 4].

We can solve this by performing two modified binary searches:

  1. Find the first occurrence: This is simply the lower_bound of the target.
  2. Find the last occurrence: We can do this by finding the lower_bound of target + 1 and subtracting 1 from the result. This clever trick finds the first element greater than our target, and the element right before it must be the last occurrence of our target.

This "upper bound" concept is the natural counterpart to the lower bound.

Upper Bound: The smallest index i in a sorted array such that nums[i] > target.

The implementation is nearly identical to lower_bound, with only a change from >= to >. The video you watched earlier also covers this.

BS-2. Implement Lower Bound and Upper Bound | Search Insert Position | Floor and Ceil

Let's revisit the 'take U forward' video to see how the upper bound is defined and implemented. This is the second piece of the puzzle for finding a search range.

Watch from 15:34 to 19:14. Notice the single-character change in the condition (> instead of >=) to implement the upper bound search.

Now we can combine these two functions to solve the search range problem.

def search_range(nums, target):
    # Find the first occurrence of the target
    start = lower_bound(nums, target)
    
    # If the start index is out of bounds or the element at start is not the target,
    # then the target is not in the array.
    if start == len(nums) or nums[start] != target:
        return [-1, -1]
        
    # Find the first occurrence of an element > target
    # This is the upper_bound of the target.
    # The last occurrence of the target will be the index right before it.
    end_index_exclusive = lower_bound(nums, target + 1)
    end = end_index_exclusive - 1
    
    return [start, end]

# lower_bound function as defined before
def lower_bound(nums, target):
    left, right = 0, len(nums) - 1
    ans = len(nums)
    while left <= right:
        mid = left + (right - left) // 2
        if nums[mid] >= target:
            ans = mid
            right = mid - 1
        else:
            left = mid + 1
    return ans

# Example:
nums = [5, 7, 7, 8, 8, 10]
target = 8
print(search_range(nums, target)) # Output: [3, 4]

target = 6
print(search_range(nums, target)) # Output: [-1, -1]

For a comprehensive text-based walkthrough of this problem, including the intuition, pitfalls, and code in multiple languages (Python, Java, C++), the following resource is excellent.

34. Find First and Last Position of Element in Sorted Array

This AlgoMonster article provides a deep dive into LeetCode 34. It's a great supplement to the video explanations.

Read the sections 'Intuition', 'Solution Approach', 'Example Walkthrough', and 'Common Pitfalls'. This will solidify your understanding of the two-search strategy and help you avoid common mistakes.

Test your understanding!

Given the array nums = [1, 2, 4, 4, 4, 4, 9, 11] and target = 4, trace the search_range function.

  1. What will lower_bound(nums, 4) return?
  2. What will lower_bound(nums, 5) return?
  3. What is the final result?
Show answer
  1. lower_bound(nums, 4) will search for the first index i where nums[i] >= 4. It will find index 2. So, start = 2.
  2. lower_bound(nums, 5) will search for the first index i where nums[i] >= 5. It will find index 6 (where the value is 9). So, end_index_exclusive = 6.
  3. The end index is end_index_exclusive - 1, which is 6 - 1 = 5. The final result is [start, end], which is [2, 5]. This correctly identifies the range of all 4s.

Conclusion

You have now extended your binary search skills beyond simple lookups. By making small but crucial modifications to the comparison logic, you can find boundaries and ranges efficiently.

Key Takeaways:

  • Lower Bound: lower_bound(target) finds the first index i where nums[i] >= target. This is the key to solving "Search Insert Position".
  • Upper Bound: upper_bound(target) finds the first index i where nums[i] > target.
  • Search Range Strategy: The range of a target can be found by [lower_bound(target), upper_bound(target) - 1], after checking that the target exists.
  • The Pattern: The core pattern is to not stop on a match, but to record it as a potential answer and continue shrinking the search space to find an even better (leftmost or rightmost) solution.

In our next lesson, we will push this pattern further by applying binary search to a more complex data structure: a rotated sorted array. This will challenge you to adapt the boundary conditions in a new way.