intermediate9 min read·Updated October 6, 2026

Binary Search First Occurrence Explained: Finding 3 in [1,3,3,3,5]

Master the binary search lower bound pattern by tracing the first occurrence of 3 in [1,3,3,3,5] and the insert position for 2 in [1,3,5].

By Learnisim AI·Published October 6, 2026
LEARNING ARCSetup → Model in the example → Full trace → Twist → Edge cases → Apply
PREREQUISITES
  • Basic binary search on unique sorted arrays
  • Understanding of pointers or index bounds (low and high)
Binary Search Lower Bound: First Occurrence & Insert Position Invariant: nums[mid] >= target (pull hi down to mid, or lo = mid + 1) Part A: First Occurrence in Duplicates nums = [1, 3, 3, 3, 5], target = 3 | Find first index 1 idx 0 3* idx 1 (Ans) 3 idx 2 3 idx 3 5 idx 4 • Init: lo=0, hi=5. mid=(0+5)//2 = 2 (nums[2]=3) • nums[2] >= 3 => hi = mid (hi becomes 2) • Converges to lo=1, hi=1 => Return index 1 Part B: Insert Position of Missing Target nums = [1, 3, 5], target = 2 | Find insert spot 1 idx 0 3 idx 1 (Insert) 5 idx 2 • Init: lo=0, hi=3. mid=(0+3)//2 = 1 (nums[1]=3) • nums[1] >= 2 => hi = mid (hi becomes 1) • Terminates at lo=1 => Correct insert index for 2 The Lower Bound Invariant (Why it Never Misses the Left Boundary) 1. Half-Open Range [lo, hi) • Set hi = n (not n - 1) • Allows target larger than all elements to insert at index n. Loop runs while lo < hi. 2. Mid Decision Logic • If nums[mid] >= target: hi = mid (do NOT skip mid) • If nums[mid] < target: lo = mid + 1 (too small) 3. Guaranteed Termination • When lo == hi, pointers converge precisely on the first valid lower bound. Return lo (O(log n) time).
First index of 3 in [1, 3, 3, 3, 5]; insert position of 2 in [1, 3, 5] overview diagram
Why

Why Standard Binary Search Fails on Duplicates

Phase 1: The Duplicate Trap

Imagine you are handed a sorted list of numbers containing duplicates: [1, 3, 3, 3, 5], and your mission is to find the first occurrence of the number 3. If you reach for a standard binary search, you calculate a midpoint, check if it equals 3, and happily return its index. But standard binary search has no loyalty to boundaries; it might land on index 2 or index 3 and stop immediately, completely oblivious to whether an earlier 3 is hiding to the left.
Now consider a second scenario: what if the target is 2, which does not even exist in our sorted list [1, 3, 5]? A standard binary search will eventually report failure (-1 or not found). Yet, in many practical systems—like database indexing or language runtimes—we do not just want to know if 2 is present. We need its exact insert position, the index where 2 belongs to keep the sequence sorted. Without a specialized approach, you are forced to scan the array linearly, throwing away the speed advantage you bought by keeping the data sorted in the first place.
Why Standard Binary Search Fails on Duplicates Standard search stops arbitrarily on any match. Lower bound finds the exact first occurrence & insert position. 1. The Duplicate Trap: Target = 3 Array: [1, 3, 3, 3, 5] 1 idx 0 3 idx 1 (First) 3 idx 2 (Mid) 3 idx 3 5 idx 4 Standard Search Behavior: 1. Computes mid = index 2 (value 3). 2. Matches target! Instantly returns index 2. ✗ Blind to index 1 (the TRUE first occurrence)! Lower Bound Solution: • When arr[mid] == target, do NOT stop. • Save position as candidate, then search LEFT: right = mid - 1 Result: Correctly locks onto index 1. 2. The Missing Target: Insert Position of 2 Array: [1, 3, 5] | Target: 2 (absent) 1 idx 0 3 idx 1 5 idx 2 Insert Index 1 Standard Search Behavior: 1. Compares 2 against midpoints (1, 3, 5). 2. Target 2 is never found in the array. ✗ Returns -1 or fails without position info. Lower Bound & Insert Guarantee: • Treats condition: arr[mid] >= target • When arr[mid] = 3 >= 2, moves left (right = mid - 1). • Exhaustion leaves low pointer at correct spot. Result: Finds index 1 in $O(\log n)$ time!
Why Standard Binary Search Fails on Duplicates diagram
Model

Building the Lower Bound Search Space and Invariant

Phase 2: The Core Model

Standard binary search stops as soon as it hits any matching value. When our sorted nums is and target is 3, a naive search might land on index 2 and declare success. But our goal for a lower bound is much stricter: we want the first occurrence at index 1, or for the missing target 2 in , the exact insertion index 1 where 2 belongs without breaking order.
To achieve this, we redefine our search space boundaries. Instead of setting hi = n - 1, we maintain our high pointer at hi = n. This allows our pointer to validly point to the index after the last element, which is essential if our target is larger than all elements in the array. Our search window is always a half-open range .
When we compute , we compare against our target. If , we know and everything to its left is strictly too small. We safely advance . However, if , itself could be our answer or our first occurrence might still lie further to the left. Therefore, we do not step past ; instead, we pull our upper bound down by setting .
Try this: Given nums = [1, 3, 5] and target = 2, what are the initial values of lo, hi, and mid on the very first iteration using this lower bound model?
Lower Bound Binary Search: Finding First Occurrence & Insert Position Search Space [lo, hi) with hi = n | nums = [1, 3, 3, 3, 5], target = 3 Array & Index Structure (n = 5) 1 idx: 0 3 (First) idx: 1 3 idx: 2 3 idx: 3 5 idx: 4 [ ] hi=5 (out) Pointer State Example (lo < hi half-open range) lo = 0 (Start of window) mid = (0+5)//2 = 2 (nums[2]=3) hi = 5 (Allows indexing past end) Core Invariant & Branching Logic If nums[mid] < target: Too small! Safely advance lo = mid + 1 If nums[mid] >= target: Could be first occurrence! Do NOT step past mid; Pull upper bound down: hi = mid Why hi = n & Insert Position Guarantee Target larger than all elements (e.g. target=6) hi = n allows returning index n (end of array) Missing Target Example: nums = [1, 3, 5], target = 2 Initial: lo = 0, hi = 3, mid = 1 (nums[1]=3 >= 2) Terminates at lo = 1 (Exact insert position for 2)
Building the Lower Bound Search Space and Invariant diagram
Worked example

Tracing Lower Bound Execution on Duplicates and Missing Elements

Phase 3:

To see how the lower bound invariant operates in practice, let us trace our locked example step by step. We have a sorted array nums = [1, 3, 3, 3, 5] and a target of 3. We want to locate its first occurrence. We initialize our search pointers across the full valid index range: lo = 0, hi = 5 (where ).
Given: nums = [1, 3, 3, 3, 5], target = 3
Steps:
- Iteration 1: lo = 0, hi = 5. Compute mid = (0 + 5) // 2 = 2. Inspect nums[2], which is 3. Since nums[2] >= target (), we must look to the left to see if an earlier 3 exists. We set hi = mid ().
- Iteration 2: lo = 0, hi = 2. Compute mid = (0 + 2) // 2 = 1. Inspect nums[1], which is 3. Again, nums[1] >= target, so we set hi = mid ().
- Iteration 3: lo = 0, hi = 1. Compute mid = (0 + 1) // 2 = 0. Inspect nums[0], which is 1. Since 1 < 3, nums[0] is too small. We advance the lower bound: lo = mid + 1 ().
- Termination: Now lo = 1 and hi = 1. The loop condition lo < hi fails ( is false). We terminate and return lo.
Result: Index 1, which is the exact first position of 3.
Now let us trace the second part of our working example: finding the insert position of target = 2 in nums = [1, 3, 5].
Given: nums = [1, 3, 5], target = 2, lo = 0, hi = 3 ()
Steps:
- Iteration 1: lo = 0, hi = 3. Compute mid = (0 + 3) // 2 = 1. Inspect nums[1], which is 3. Since 3 >= 2, we set hi = 1.
- Iteration 2: lo = 0, hi = 1. Compute mid = (0 + 1) // 2 = 0. Inspect nums[0], which is 1. Since 1 < 2, we set lo = mid + 1 ().
- Termination: lo = 1 and hi = 1. Loop terminates.
Result: Index 1, which correctly represents the insertion index where 2 belongs between 1 and 3 to keep the array sorted.
Phase 3: Lower Bound Execution Trace Tracing nums = [1, 3, 3, 3, 5] with target = 3 (Finding First Occurrence) idx 0 idx 1 idx 2 idx 3 idx 4 1 3 3 3 5 lo = 1 hi = 1 Step-by-Step Execution Summary Iter 1: lo=0, hi=5 mid = 2 (nums[2] = 3) >= target Action: hi = mid (hi -> 2) Iter 2: lo=0, hi=2 mid = 1 (nums[1] = 3) >= target Action: hi = mid (hi -> 1) Iter 3: lo=0, hi=1 mid = 0 (nums[0] = 1) < target Action: lo = mid + 1 (lo -> 1) Termination: lo = 1, hi = 1 (lo < hi fails) Result: Returns Index 1 — The exact first occurrence of target 3 in the array. Target = 3 Search left on >= target
Tracing Lower Bound Execution on Duplicates and Missing Elements diagram
Practice

Predicting Lower Bound Behavior on Edge Inputs

Phase 4: Practice

Now that you have traced how the invariant shrinks the window for both the first occurrence of 3 in [1, 3, 3, 3, 5] and the missing insertion point of 2 in [1, 3, 5], it is time to test your mental model on a boundary modification of the same data. Recall that the lower bound routine returns the first index where , or if no such element exists.
Consider the original array [1, 3, 3, 3, 5] from our locked example, but change the target value to 0. Your task is to trace or mentally execute the lo and hi pointer updates to determine what index the algorithm will return and why the invariant prevents the pointers from breaking.
python
nums = [1, 3, 3, 3, 5]
target = 0
# What does binary_search_lower_bound(nums, target) return?
Apply

Transferring Lower Bound Logic to Range Queries and Beyond

Phase 5: Applying the Lower Bound Pattern

Now that you have traced the lower bound logic for [1, 3, 3, 3, 5] and handled the missing element 2 in [1, 3, 5], it is time to deploy this exact structural invariant to a new situation. Many problems that appear distinct—such as finding the frequency of a repeated element or locating the first bad version in a sequence of software builds—are actually direct disguises of the lower bound pattern.
For instance, suppose you need to count how many times the value 3 appears in the locked array [1, 3, 3, 3, 5]. Instead of scanning linearly, you can combine two binary searches: find the lower bound of 3 to lock down the starting index, and find the lower bound of (or an upper bound) to find the cutoff index. The difference between these two pointers yields the exact count in time.
The core transfer skill is recognizing when a problem asks for a boundary transition from false to true (or less-than to greater-than-or-equal). Whenever you spot a sorted domain where you need the first transition point rather than any arbitrary match, throw away standard equality checking and instantiate the range with hi = n and else hi = mid.

The Final Challenge

Consider a sorted array of timestamps representing successful and failed system checks, where [0, 0, 0, 1, 1] denotes 0 for pass and 1 for fail. You need to find the exact index of the first system failure (1). State how you would adapt the lower bound template variables (target = 1, comparison operator, and pointer updates) to solve this without modifying the core invariant.
python
def find_first_failure(checks):
    lo, hi = 0, len(checks)
    while lo < hi:
        mid = (lo + hi) // 2
        # Adapt the condition below:
        if checks[mid] < 1:
            lo = mid + 1
        else:
            hi = mid
    return lo

FAQ

What is the lower bound of 3 in [1, 3, 3, 3, 5]?
The lower bound is index 1, which points to the very first occurrence of the number 3 in the array.
How does lower bound handle a missing target like 2 in [1, 3, 5]?
When the target is missing, the lower bound returns the index where the target would be inserted to maintain sorted order, which is index 1 (between 1 and 3).
Why does standard binary search fail when duplicates are present?
Standard binary search stops immediately upon finding any matching element, which could be any middle duplicate rather than the earliest one.
What is the time complexity of a lower bound binary search?
It operates in O(log n) time complexity because the search space is halved at each step, just like standard binary search.

Keep learning