DSA sheet · Binary Search · Lower & upper bound pattern

Count Occurrences in a Sorted Array

Counting how many times a number appears sounds like a one-line loop, and it is. The teacher's point is that the question says the array is sorted, and every extra fact in a question is there for a reason. Sorted means binary search, and binary search means O(log n) instead of O(n). The trick: all copies of the target sit next to each other, so if we know where the block starts and where it ends, its length is the count. Finding the start and end is exactly the previous problem (First & Last Position).

Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember

Part 0 · Before starting

Why binary search works on a sorted array

Look at the middle value. If it's smaller than the target, everything on its left is even smaller, so that whole half can't contain the target and is thrown away. If it's bigger, the right half goes. Each look cuts the search area in half.

The yes/no view: for target 2 in [1, 1, 2, 2, 2, 2, 3], ask "is arr[i] >= 2?" The answers are no no yes yes yes yes yes. In a sorted array they switch only once, and binary search finds that switch point fast.

left, right, mid

Why mid = left + (right - left) // 2?

In Java/C++, left + right can go past the biggest int (about 2.1 × 10⁹) and wrap to a negative number: that's overflow. left + (right - left) // 2 gives the same middle without ever making a number bigger than right. Python integers never overflow, so here it's just a good habit, and the teacher always uses it.

Lower bound vs upper bound, and why counting is just their difference

For arr = [1, 1, 2, 2, 2, 2, 3] and x = 2:

index0123456
value1122223
bounds↑LB↑UBLB = 2, UB = 6
  index:  0  1  2  3  4  5  6
  value:  1  1 [2  2  2  2] 3
                ^           ^
     lower bound = 2        upper bound = 6
     (first 2)              (first value after the 2s)

  first = LB = 2,  last = UB - 1 = 5
  count = last - first + 1 = 5 - 2 + 1 = 4
        = UB - LB             = 6 - 2     = 4

For a missing value like x = 0, both bounds land on index 0, so UB − LB = 0.

value1122223
x = 0↑LB ↑UBcount 0

Part A · Brute force: walk and count

GeeksforGeeks · Number of occurrence

1The question in simple words

You get a sorted array arr and a number target. Return how many times the target appears. If it doesn't appear, return 0.

index0123456
value1122223target 2 → 4

2What the constraints tell us

3Intuition

Walk through the array with a counter. Every time you see the target, add 1. The teacher mentions two easy ways: a simple count variable, or putting every number into a hash map (dictionary) of number → frequency and reading the target's entry.

4Building the logic

There's nothing tricky here. The real lesson is the question she asks right after: if the array is sorted and I scan it linearly anyway, what was the point of telling me it's sorted? A linear scan works on any array, sorted or not. When a question hands you a sorted array, the interviewer expects you to use that fact.

Doubt: since it's sorted, can I at least stop the scan early?
→ Yes. Once you pass a value bigger than the target, no more copies can appear, so you can break. That helps a little, but in the worst case (target near the end) it's still O(n).

5Approach steps

  1. count = 0.
  2. For each value: if it equals the target, count += 1. If it's bigger, stop.
  3. Return count.

6Code (Python)

brute force: O(n)
class Solution:
    def countFreq(self, arr, target):
        count = 0
        for x in arr:
            if x == target:
                count += 1
            elif x > target:      # sorted: no more copies after this
                break
        return count

The hash-map version is collections.Counter(arr)[target]. It's also O(n), and it uses O(n) extra space.

7Code line by line

linewhat it means
count = 0No copies seen yet.
if x == target: count += 1One more copy.
elif x > target: breakWe've walked past the target's block, and nothing later can match.
return count0 if the target never showed up.

8Dry run

i0123456
arr[i]1122223
count after0012343 > 2 → break

Answer 4.

9Complexity & remember

RememberBrute force works and passes, but it ignores "sorted". Say it out loud in an interview, then move to binary search.

Part B · Optimal: first + last with binary search

1The question

Same question: count the target in a sorted array, but in O(log n).

2Constraints

n up to 10⁶ → log₂(10⁶) ≈ 20 steps per binary search. No empty array, so right = n - 1 is always a real index.

3Intuition: the copies form one block

Because the array is sorted, all copies of the target sit side by side in one block. If you know the index where the block starts (the first occurrence) and where it ends (the last occurrence), the count is the block's length:

  index:  0  1  2  3  4  5  6
  value:  1  1 [2  2  2  2] 3
                ^        ^
             first=2   last=5      length = 5 - 2 + 1 = 4
Doubt: why "+ 1"? 5 − 2 is only 3.
→ Subtraction counts the gaps between indexes, not the cells. From index 2 to index 5 there are 3 steps but 4 cells (2, 3, 4, 5). The teacher's own check: if the target appears just once, first = last, say both are 3, and 3 − 3 + 1 = 1 ✓. Without the +1 you'd get 0.

Finding the first and last occurrence with binary search is exactly the previous problem. The teacher says to watch that video first if you haven't, and here she only reuses the code. These notes recap it fully, so this page stands alone.

4Building the conditions from examples

Her example: [1, 1, 2, 2, 2, 2, 3], target 2. left = 0, right = 6, so mid = 3, and arr[3] = 2, a match. There are three cases to handle:

Case 1: arr[mid] == target

Why save before moving? If the side we move into has no more 2's, we haven't lost this one. It's already in ans.

Case 2: arr[mid] < target (e.g. mid lands on a 1)

1 is too small, and so is everything left of it. The 2's are on the right, so left = mid + 1. Why not left = mid? Because mid is a 1. It can never be counted, so there's no reason to keep it.

Case 3: arr[mid] > target (e.g. mid lands on a 3)

3 is too big. The 2's are on the left, so right = mid - 1. Mid is a 3, so we don't keep it either.

Doubt (a fix the video doesn't mention): what if the target isn't in the array at all?
→ Both helpers return −1. Then last - first + 1 = -1 - (-1) + 1 = 1, which is wrong: the answer should be 0. So check first: if first == -1: return 0. (If first is −1, last is −1 too, because they look for the same value.)

5Approach steps

  1. first = binary search that, on a match, saves and goes left.
  2. If first == -1 → the target is absent → return 0.
  3. last = binary search that, on a match, saves and goes right.
  4. Return last - first + 1.

6Code (Python)

optimal: two binary searches
class Solution:
    def countFreq(self, arr, target):
        first = self.findFirst(arr, target)
        if first == -1:                 # target not present
            return 0
        last = self.findLast(arr, target)
        return last - first + 1         # length of the block

    def findFirst(self, arr, target):
        left, right = 0, len(arr) - 1
        ans = -1
        while left <= right:
            mid = left + (right - left) // 2
            if arr[mid] == target:
                ans = mid
                right = mid - 1         # block may start earlier: go left
            elif arr[mid] < target:
                left = mid + 1
            else:
                right = mid - 1
        return ans

    def findLast(self, arr, target):
        left, right = 0, len(arr) - 1
        ans = -1
        while left <= right:
            mid = left + (right - left) // 2
            if arr[mid] == target:
                ans = mid
                left = mid + 1          # block may end later: go right
            elif arr[mid] < target:
                left = mid + 1
            else:
                right = mid - 1
        return ans

7Code line by line

linewhat it means
first = self.findFirst(arr, target)Index where the target's block starts, or −1.
if first == -1: return 0Not present → zero copies. (This guards against the "−1 − (−1) + 1 = 1" bug.)
last = self.findLast(arr, target)Index where the block ends.
return last - first + 1Number of cells from first to last, both included.
ans = mid; right = mid - 1(findFirst) Remember this match, keep hunting left.
ans = mid; left = mid + 1(findLast) Remember this match, keep hunting right.
elif arr[mid] < target: left = mid + 1Too small → keep the right half (mid excluded).
else: right = mid - 1Too big → keep the left half (mid excluded).

8Dry run (hand tables)

arr = [1, 1, 2, 2, 2, 2, 3], target 2.

findFirst

stepleftrightmidarr[mid]decisionansthrown away
10632match → save, right = 233–6
202111 < 2 → left = 230–1
32222match → save, right = 122
end21crossed → first = 2
step 11122223match at 3 → go left
step 211222231 too small → go right
step 31122223match at 2 → ans = 2

findLast

stepleftrightmidarr[mid]decisionansthrown away
10632match → save, left = 430–3
24652match → save, left = 654–5
366633 > 2 → right = 556
end65crossed → last = 5
step 21122223match at 5 → go right
step 311222233 too big → stop

count = 5 − 2 + 1 = 4 ✓. Target 4 on the same array: findFirst never matches → −1 → return 0.

9Complexity & remember

Remembercount = last − first + 1, where first and last come from two binary searches that "save and keep going". If first is −1, return 0.

Part C · Her other style: keep mid in range

1What she says

In this video the teacher also describes another way to handle a match. Instead of saving mid and jumping past it, keep mid inside the search area: right = mid for the first occurrence, left = mid for the last. Her reason: we don't know if another 2 exists beyond mid, so we must not jump over this one. Both styles are valid. Her final code uses the "save in ans" style from Part B.

2Constraints

Same. The array is non-empty, so arr[left] is always safe at the end.

3Intuition

If mid is never thrown away, the pointers squeeze toward it. The loop must then be while left < right (stop when they meet). With <=, right = mid could repeat forever on the same single cell. When they meet, that cell is the candidate. Check it once at the end.

4The trap: left = mid needs the UPPER middle

Doubt (a fix the video doesn't mention): try findLast with left = mid on [2, 2].
→ left=0, right=1 → mid = 0 + (1−0)//2 = 0 → match → left = mid = 0. Nothing moved, so it's an infinite loop. The normal formula rounds down, so with two cells it always picks the left one. Fix: when a branch does left = mid, round up: mid = left + (right - left + 1) // 2. Then mid = 1, left becomes 1, and the loop ends.

5Approach steps

  1. First: while left < right, lower mid; if arr[mid] < target → left = mid + 1, else right = mid.
  2. Last: while left < right, upper mid; if arr[mid] > target → right = mid - 1, else left = mid.
  3. If arr[first] != target → return 0, else return last - first + 1.

6Code (Python)

keep-mid style
class Solution:
    def countFreq(self, arr, target):
        left, right = 0, len(arr) - 1
        while left < right:                       # first occurrence
            mid = left + (right - left) // 2
            if arr[mid] < target:
                left = mid + 1
            else:
                right = mid                       # mid might be the first: keep it
        if arr[left] != target:
            return 0
        first = left

        right = len(arr) - 1                      # last occurrence (left = first is fine)
        while left < right:
            mid = left + (right - left + 1) // 2  # round UP because of left = mid
            if arr[mid] > target:
                right = mid - 1
            else:
                left = mid                        # mid might be the last: keep it
        return left - first + 1

7Line by line (only the new parts)

linewhat it means
while left < right:Stop when one cell is left. That cell is the candidate.
else: right = midarr[mid] ≥ target: mid could be the first copy, so keep it.
if arr[left] != target: return 0The candidate isn't the target → it's absent.
mid = left + (right - left + 1) // 2Upper middle, so left = mid always moves forward.
return left - first + 1Block length, as before.

8Dry run on [1, 1, 2, 2, 2, 2, 3], target 2

phaseleftrightmidarr[mid]decision
first0632not < 2 → right = 3
first03111 < 2 → left = 2
first2322right = 2 → meet at 2
last2642not > 2 → left = 4
last4652left = 5
last56633 > 2 → right = 5 → meet at 5

5 − 2 + 1 = 4 ✓.

9Complexity & remember

O(log n) time, O(1) space. Starting the second search from left = first is a tiny speed-up. Starting from 0 also works.

RememberKeep-mid style → while left < right, and round mid up whenever you write left = mid. When in doubt, use the "save in ans, then mid ± 1" style.

Part D · One line with lower_bound / upper_bound

Not shown in the video. This is added to tie the code to the pattern's name.

From Part 0: count = upper bound − lower bound. If the target is missing, both bounds land on the same index, and the answer is 0 automatically. No −1 special case needed.

index0123456
value1122223
bounds↑LB↑UB6 − 2 = 4
count = upper_bound − lower_bound
def lower_bound(arr, x):           # first index with arr[i] >= x
    left, right = 0, len(arr)      # note: right = n, half-open range [left, right)
    while left < right:
        mid = left + (right - left) // 2
        if arr[mid] >= x:
            right = mid
        else:
            left = mid + 1
    return left

def upper_bound(arr, x):           # first index with arr[i] > x
    left, right = 0, len(arr)
    while left < right:
        mid = left + (right - left) // 2
        if arr[mid] > x:
            right = mid
        else:
            left = mid + 1
    return left

def count_freq(arr, target):
    return upper_bound(arr, target) - lower_bound(arr, target)

The two bound functions differ only in >= vs >. In Python, bisect.bisect_right(arr, t) - bisect.bisect_left(arr, t) gives the same count, which is useful for checking your own code.

Here right starts at n (one past the end) because the bound can be n. The range is "left included, right excluded", so the loop is left < right and right = mid is safe (mid rounds down, so it always shrinks the range).


Part E · Revision page

brute forcefirst + last (teacher)upper − lower
ideascan and countblock length = last − first + 1block length = UB − LB
uses "sorted"?barelyyesyes
missing targetcount stays 0must check first == -1 → 0UB = LB → 0 automatically
timeO(n)2 log n = O(log n)O(log n)
spaceO(1) (hash map: O(n))O(1)O(1)
lower boundupper bound
definitionfirst index with value ≥ xfirst index with value > x
[1,1,2,2,2,2,3], x = 22 (first 2)6 (the 3)
same array, x = 000 → count 0
relation= first occurrence= last occurrence + 1
If you remember only 5 lines 1. Sorted array → equal values form one block.
2. Count = length of the block = last − first + 1.
3. first/last = binary search that saves the match and keeps going left/right.
4. Target missing → return 0 (don't let −1 − (−1) + 1 give 1).
5. Two searches → 2 log n → O(log n).
Mistakes to avoid ✗ stopping at the first match (that finds a copy, not the block edges)
✗ forgetting the +1 in last − first + 1
✗ returning 1 for a missing target
✗ left = mid with the round-down mid → infinite loop
✗ settling for O(n) when the array is sorted
test it yourself (paste under the Part B solution)
s = Solution()
print(s.countFreq([1, 1, 2, 2, 2, 2, 3], 2))   # 4
print(s.countFreq([1, 1, 2, 2, 2, 2, 3], 4))   # 0
print(s.countFreq([8, 9, 10, 12, 12, 12], 12)) # 3
print(s.countFreq([5], 5))                     # 1
print(s.countFreq([7, 7], 7))                  # 2

Based on this video: Count Occurrences in a Sorted Array