Binary Search, Heaps, and Intervals
Binary search
Binary search works on sorted arrays or monotonic functions (condition flips from false → true).
- Repeatedly halve the search space using mid.
- Check the condition at mid to decide whether to search left or right. Very common in interview problems where you “search for an answer” rather than an exact value.
1. Standard binary search (Exact match)
Idea: Search for a target in a sorted array.
def binary_search(arr, target):
l, r = 0, len(arr) - 1
while l <= r:
mid = (l + r) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
l = mid + 1
else:
r = mid - 1
return -12. Lower bound/upper bound (Search insert position)
Idea: Find first/last position where a condition is true. Lower bound: first index ≥ target Upper bound: first index > target
def lower_bound(arr, target):
l, r = 0, len(arr)
while l < r:
mid = (l + r) // 2
if arr[mid] >= target:
r = mid
else:
l = mid + 1
return l3. Binary search on answer (Parametric search)
Idea: Use binary search when the answer lies in a range and condition is monotonic.
When solving problems with binary search on the answer, you need to choose a search range [low, high].
This range comes from the domain of the answer (time, speed, capacity, distance — not array indices).
Step 1: Pick a trivial lower bound (low)
Choose the smallest possible answer:
- Often
0or1. - Sometimes
max(item)(e.g., if each machine must handle at least the biggest task).
Step 2: Pick an obviously feasible upper bound (high)
Choose a value that definitely works, even if it’s inefficient:
sum(items)→ one worker/machine does everything.- The full span of coordinates.
- A very large sentinel like
1e18.
Step 3: Make sure feasible(x) is monotonic
Your feasibility check should behave like this:
- If
feasible(x)is true, thenfeasible(x + 1)should also be true. - If
feasible(x)is false, thenfeasible(x - 1)should also be false.
👉 In short:
- Low = smallest possible valid answer.
- High = a value that definitely works.
Then binary search between them.
def binary_search_answer(low, high):
while low < high:
mid = (low + high) // 2
if condition(mid): # can we achieve with mid?
high = mid
else:
low = mid + 1
return lowTime complexity: O(log n) per search
Space complexity: O(1)
Common pitfalls
- Infinite loop
(while l < r vs while l <= r) - Forgetting to update both
landr→ stuck loop - Off-by-one errors when returning (insert pos vs found index)
- Array must be sorted / condition monotonic
- Mid calculation safe in Python, but in other languages may need
l + (r-l)//2

Practice problems
Heap
A heap is a specialized binary tree–based data structure that supports efficient retrieval of the min or max element.
- Min-Heap: Parent ≤ children → root is the smallest.
- Max-Heap: Parent ≥ children → root is the largest.
Python’s heapq implements a min-heap by default. To simulate a max-heap, push negatives or wrap elements.
1. Priority queue
- Idea: Always access/remove the element with highest priority (lowest or highest value).
- When to use: Task scheduling, Dijkstra’s shortest path, A* search, event simulation.
import heapq
pq = []
heapq.heappush(pq, (priority, item))
while pq:
pr, x = heapq.heappop(pq)
# process x with smallest priority2. Top-K problems
- Idea: Maintain a heap of size K.
- Use a min-heap to track largest K elements.
- Use a max-heap (negated values) to track smallest K elements.
- When to use: Kth largest element, streaming median, “recommend top N items.”
import heapq
pq = []
for x in arr:
heapq.heappush(pq, x)
if len(pq) > K:
heapq.heappop(pq)
# pq[0] is Kth largest3. Heapify/range queries
- Idea: Convert list → heap in
O(n).- Useful for repeatedly pulling min/max in sorted order.
- When to use: Heap sort, scheduling jobs, merging k-sorted lists.
import heapq
heapq.heapify(arr) # in-place, O(n)
while arr:
smallest = heapq.heappop(arr)Time complexity
- Push/Pop:
O(log n) - Heapify:
O(n) - Peek (min/max):
O(1) - Building a Top-K heap:
O(n log k)
Space complexity: Heap storage: O(n)
Common pitfalls
- Forgetting max-heap trick: Python only has min-heap → negate values or use PriorityQueue.
- Memory blowup: Don’t keep all n elements if only k needed.
- Tuple ordering:
heapqcompares by first element, then second, etc. Be explicit with priority. - Inefficient removal: Removing arbitrary elements is not
O(log n). Consider lazy deletion or other DS.

Practice problems
Intervals
An interval represents a continuous range, usually expressed as [start, end]. Many problems involve merging, overlapping, or comparing intervals. Interval problems often require sorting + greedy scanning + merging logic.
Intervals are especially common in scheduling, CPU jobs, meeting rooms, calendar events, and range-based computations.
1. Merge intervals
- Idea: Sort by start time, then merge overlapping ranges into one.
- When to use: Merging meeting times, collapsing ranges, removing overlaps.
intervals.sort(key=lambda x: x[0])
merged = []
for start, end in intervals:
if not merged or merged[-1][1] < start:
merged.append([start, end])
else:
merged[-1][1] = max(merged[-1][1], end)2. Interval scheduling (greedy)
- Idea: Pick the interval with the earliest finishing time that doesn’t conflict with previously chosen ones.
- When to use: Maximum number of non-overlapping intervals (e.g., meeting scheduling).
intervals.sort(key=lambda x: x[1]) # sort by end
count, prev_end = 0, float("-inf")
for start, end in intervals:
if start >= prev_end:
count += 1
prev_end = end3. Sweep line (Line sweep/events)
- Idea: Break intervals into events (+1 for start, -1 for end), sort by time, and sweep through.
- When to use: Maximum overlap, meeting rooms required, busiest time, tracking active intervals.
events = []
for s, e in intervals:
events.append((s, 1))
events.append((e, -1))
events.sort()
active, max\_active = 0, 0
for \_, delta in events:
active += delta
max\_active = max(max\_active, active)Time complexity
- Sorting dominates:
O(n log n) - Merging/sweeping:
O(n)
Space complexity Storage for merged list or events: O(n)
Common pitfalls
- Not sorting first: Many interval problems require sorted input.
- Boundary conditions: Decide whether intervals are inclusive/exclusive ([start, end) vs [start, end]).
- Greedy missteps: Picking earliest start vs earliest end leads to wrong solutions.
- Modifying while iterating: Use a new list for merges, don’t overwrite mid-scan.
