Quiz 2

Binary Search — Divide & Conquer Search

2701 words
14 min read
Python Week 1: the first filter for runtime behavior
Visual companion
Python
Type and operator map

Python Week 1: the first filter for runtime behavior

View
Revision summary

What this note is really saying

Short form

# Binary Search — Divide & Conquer Search > **Why read this?** Imagine searching for a friend's name in a phone book by starting at page 1 and reading every single name. That's linear search — simple but painfully slow.

Binary Search — Divide & Conquer Search

Why read this? Imagine searching for a friend's name in a phone book by starting at page 1 and reading every single name. That's linear search — simple but painfully slow. Binary search is the "open to the middle" strategy: each comparison eliminates half the remaining data. Searching a sorted list of 1 million items takes just 20 comparisons instead of up to 1 million. This exponential speedup is one of the most important ideas in computer science.

🎯 Learning Objectives

By the end of this topic, you will be able to:
  1. Explain how binary search works (divide and conquer)
  2. Implement binary search iteratively (while loop)
  3. Implement binary search recursively
  4. Calculate time complexity: O(log n)
  5. Understand the requirement that data must be sorted
  6. Apply binary search to real-world problems

📋 Prerequisites

  • Recursion — For the recursive implementation.
  • Lists — Searching through sorted lists.
  • While Loops — For the iterative implementation.

📖 Core Content

24.1 What Problem Does Binary Search Solve?

Intuition: Think of the "guess the number" game. I pick a number between 1 and 100. You guess 50. I say "too high." Now you know it's 1-49. You guess 25. "Too low." Now 26-49. Each guess cuts the range in half. That's binary search. The naive approach (linear search) checks every element one by one — O(n). Binary search checks the middle, eliminates half, and repeats — O(log n). For n=1,000,000, linear search checks up to 1M items; binary search checks ~20.

24.2 The Algorithm — Step by Step

(Diagram) Step-by-step trace for finding 23 in [2, 5, 8, 12, 16, 23, 38, 45, 56, 72]:
pseudo
Iteration 1: low=0, high=9, mid=(0+9)//2=4, list[4]=16
  16 < 23, so search right: low=5
Iteration 2: low=5, high=9, mid=(5+9)//2=7, list[7]=45
  45 > 23, so search left: high=6
Iteration 3: low=5, high=6, mid=(5+6)//2=5, list[5]=23
  23 == 23 → FOUND at index 5! Return 5.
python
# runnable
def binary_search_iterative(lst, target):
    """
    Search for target in sorted list.
    Returns index if found, -1 if not found.
    """
    low = 0
    high = len(lst) - 1
    while low <= high:
        mid = (low + high) // 2  # integer division
        mid_val = lst[mid]
        if mid_val == target:
            return mid          # Found!
        elif mid_val < target:
            low = mid + 1       # Search right half
        else:
            high = mid - 1      # Search left half
    return -1  # Not found
# Test
data = [2, 5, 8, 12, 16, 23, 38, 45, 56, 72]
tests = [23, 1, 72, 100]
for t in tests:
    result = binary_search_iterative(data, t)
    if result != -1:
        print(f"{t} found at index {result}")
    else:
        print(f"{t} not found")
Output:
pseudo
23 found at index 5
1 not found
72 found at index 9
100 not found
python
# runnable
def binary_search_recursive(lst, target, low, high):
    """
    Recursive binary search.
    Base case: low > high (empty range) → not found.
    """
    # Base case: empty search range
    if low > high:
        return -1
    mid = (low + high) // 2
    if lst[mid] == target:
        return mid
    elif lst[mid] < target:
        # Search right half
        return binary_search_recursive(lst, target, mid + 1, high)
    else:
        # Search left half
        return binary_search_recursive(lst, target, low, mid - 1)
# Wrapper function (easier to call)
def binary_search(lst, target):
    return binary_search_recursive(lst, target, 0, len(lst) - 1)
# Test
data = [2, 5, 8, 12, 16, 23, 38, 45, 56, 72]
print(binary_search(data, 23))   # 5
print(binary_search(data, 1))    # -1
print(binary_search(data, 72))   # 9
Output:
pseudo
5
-1
9

24.5 Time Complexity Analysis

AlgorithmBest CaseAverage CaseWorst Case
Linear SearchO(1) — first elementO(n)O(n) — last element
Binary SearchO(1) — middle elementO(log n)O(log n)
Why O(log n)? Each iteration halves the search space. After k iterations, the search space is n/2^k. When n/2^k = 1 (one element left), k = log₂(n). So binary search takes at most log₂(n) steps. Examples:
  • n = 10 → log₂(10) ≈ 4 steps max
  • n = 1,000 → log₂(1000) ≈ 10 steps max
  • n = 1,000,000 → log₂(1,000,000) ≈ 20 steps max
  • n = 1,000,000,000 → log₂(1B) ≈ 30 steps max
python
# runnable
import math
sizes = [10, 100, 1000, 10000, 100000, 1000000, 1000000000]
print("n             | log2(n) | Linear worst")
print("-" * 45)
for n in sizes:
    log_n = int(math.ceil(math.log2(n)))
    print(f"{n:12,} | {log_n:7} | {n:13,}")
Output:
pseudo
n             | log2(n) | Linear worst
---------------------------------------------
          10 |       4 |            10
         100 |       7 |           100
       1,000 |      10 |         1,000
      10,000 |      14 |        10,000
     100,000 |      17 |       100,000
   1,000,000 |      20 |     1,000,000
1,000,000,000 |      30 | 1,000,000,000

24.6 Worked Example 1: First Occurrence (with Duplicates)

python
# runnable
def binary_search_first(lst, target):
    """Find FIRST occurrence of target (handles duplicates)."""
    low, high = 0, len(lst) - 1
    result = -1
    while low <= high:
        mid = (low + high) // 2
        if lst[mid] == target:
            result = mid        # Record this position
            high = mid - 1      # Keep searching LEFT for earlier occurrence
        elif lst[mid] < target:
            low = mid + 1
        else:
            high = mid - 1
    return result
# Test with duplicates
data = [1, 2, 3, 3, 3, 4, 5, 5, 6, 7]
print(f"First 3 at index: {binary_search_first(data, 3)}")  # 2
print(f"First 5 at index: {binary_search_first(data, 5)}")  # 6
Output:
pseudo
First 3 at index: 2
First 5 at index: 6

24.7 Worked Example 2: Square Root Approximation

Use binary search to compute square root without math.sqrt():
python
# runnable
def sqrt_binary_search(n, precision=0.0001):
    """Compute square root using binary search."""
    if n < 0:
        return None
    if n == 0 or n == 1:
        return n
    low, high = 0, n
    # For numbers less than 1, high should be 1
    if n < 1:
        high = 1
    while high - low > precision:
        mid = (low + high) / 2
        mid_sq = mid * mid
        if mid_sq == n:
            return mid
        elif mid_sq < n:
            low = mid
        else:
            high = mid
    return (low + high) / 2
# Test
for n in [4, 9, 16, 2, 100, 0.25]:
    result = sqrt_binary_search(n)
    print(f"√{n} ≈ {result:.4f}")
Output:
pseudo
√4 ≈ 2.0000
√9 ≈ 3.0000
√16 ≈ 4.0000
√2 ≈ 1.4142
√100 ≈ 10.0000
√0.25 ≈ 0.5000

24.8 Worked Example 3: Dictionary Word Lookup

python
# runnable
# Simulated dictionary
dictionary = [
    "apple", "banana", "cherry", "date", "elderberry",
    "fig", "grape", "honeydew", "kiwi", "lemon",
    "mango", "nectarine", "orange", "papaya", "quince"
]
def lookup_word(words, target):
    """Check if target word exists in dictionary."""
    index = binary_search_iterative(words, target)
    if index != -1:
        return f"'{target}' found at position {index + 1}"
    else:
        return f"'{target}' not found"
print(lookup_word(dictionary, "kiwi"))
print(lookup_word(dictionary, "grape"))
print(lookup_word(dictionary, "watermelon"))
Output:
pseudo
'kiwi' found at position 9
'grape' found at position 7
'watermelon' not found

24.9 Worked Example 4: Find Insert Position

Find the index where a target should be inserted to maintain sorted order:
python
# runnable
def find_insert_position(lst, target):
    """Return the index where target should be inserted."""
    low, high = 0, len(lst)
    while low < high:
        mid = (low + high) // 2
        if lst[mid] < target:
            low = mid + 1
        else:
            high = mid
    return low
data = [1, 3, 5, 7, 9, 11]
for val in [0, 4, 6, 12]:
    pos = find_insert_position(data, val)
    print(f"Insert {val:2} at index {pos}: {data[:pos] + [val] + data[pos:]}")
Output:
pseudo
Insert  0 at index 0: [0, 1, 3, 5, 7, 9, 11]
Insert  4 at index 2: [1, 3, 4, 5, 7, 9, 11]
Insert  6 at index 3: [1, 3, 5, 6, 7, 9, 11]
Insert 12 at index 6: [1, 3, 5, 7, 9, 11, 12]

24.10 Worked Example 5: Peak Element in Mountain Array

A mountain array is one that increases then decreases. Find the peak:
python
# runnable
def find_peak(arr):
    """Find peak element in a mountain array."""
    low, high = 0, len(arr) - 1
    while low < high:
        mid = (low + high) // 2
        if arr[mid] < arr[mid + 1]:
            low = mid + 1  # Ascending, peak is to the right
        else:
            high = mid      # Descending, peak is at mid or left
    return low  # Peak index
mountain = [1, 3, 5, 7, 9, 8, 6, 4, 2]
peak = find_peak(mountain)
print(f"Mountain: {mountain}")
print(f"Peak at index {peak}: value = {mountain[peak]}")
Output:
pseudo
Mountain: [1, 3, 5, 7, 9, 8, 6, 4, 2]
Peak at index 4: value = 9

📐 Key Concepts Reference

ConceptDescriptionFormula/Code
Binary searchDivide sorted list in half repeatedlymid = (low+high)//2
PreconditionList must be SORTEDsorted(lst)
Time complexityO(log n)log₂(n) comparisons
Space (iterative)O(1)No extra memory
Space (recursive)O(log n)Call stack frames
Mid formulaAvoids integer overflowmid = low + (high-low)//2
FoundTarget equals middlelst[mid] == target
Not foundlow > highreturn -1

⚠️ Common Pitfalls

Pitfall 1: Data Not Sorted

The mistake: Running binary search on unsorted data. It may return wrong results or miss the target entirely. Why: Binary search relies on the sorted property to know which half to discard. If the data isn't sorted, discarding half may incorrectly eliminate the target. Fix: Always ensure data is sorted: data.sort() before binary search.

Pitfall 2: Off-by-One Errors in Range

The mistake: Using while low < high (missing the =), causing the final element to never be checked. Fix: Use while low <= high for the inclusive range [low, high]. Trace of the bug: If list = [5] and we search for 5: low=0, high=0. With low < high, the loop never executes because 0 < 0 is False. So 5 is never found.

Pitfall 3: Integer Overflow in Mid Calculation

The mistake: mid = (low + high) // 2 can overflow in languages with fixed-width integers. Python has arbitrary-precision integers, so this isn't an issue here, but the safer formula is mid = low + (high - low) // 2. Also: For very large arrays approaching Python's memory limits, the simple formula works fine in Python.

Pitfall 4: Infinite Loop in Recursive Version

The mistake: Not properly updating mid + 1 or mid - 1 in recursive calls, causing infinite recursion. Example: binary_search_recursive(lst, target, low, mid) instead of low, mid - 1. Fix: Ensure each recursive call narrows the range: mid + 1 for right half, mid - 1 for left half.

Pitfall 5: Forgetting the Wrapper Function for Recursive Version

The mistake: Making the user pass low and high every time. Fix: Create a wrapper function: def search(lst, target): return binary_search_recursive(lst, target, 0, len(lst)-1)

📝 Practice Questions

Q1: How many steps does binary search take for a sorted list of 1024 elements in the worst case?
Answer: log₂(1024) = 10 steps maximum. After 10 halvings, the search range is reduced to 1 element. Q2: Why must the list be sorted for binary search to work?
Answer: Binary search decides which half to search next by comparing the target to the middle element. If the list is not sorted, this comparison doesn't guarantee which half contains the target — the target could be in either half regardless of the comparison result. Q3: Trace binary search for target=7 in [1, 3, 5, 7, 9, 11].
Answer:
pseudo
low=0, high=5, mid=2, list[2]=5 < 7 → search right: low=3
low=3, high=5, mid=4, list[4]=9 > 7 → search left: high=3
low=3, high=3, mid=3, list[3]=7 == 7 → FOUND at index 3
Q4: Write a function that uses binary search to find a target in a list of strings (case-insensitive).
Answer:
python
# runnable
def binary_search_case_insensitive(lst, target):
    """Binary search ignoring case. List must be sorted case-insensitively."""
    target_lower = target.lower()
    low, high = 0, len(lst) - 1

    while low <= high:
        mid = (low + high) // 2
        mid_lower = lst[mid].lower()

        if mid_lower == target_lower:
            return mid
        elif mid_lower < target_lower:
            low = mid + 1
        else:
            high = mid - 1
    return -1

# Test
words = sorted(["Apple", "Banana", "Cherry", "Date"], key=str.lower)
print(binary_search_case_insensitive(words, "banana"))  # 1
print(binary_search_case_insensitive(words, "BANANA"))  # 1
print(binary_search_case_insensitive(words, "grape"))   # -1
Q5: What's the worst-case time complexity of binary search compared to linear search?
Answer:
  • Binary search: O(log n) — logarithmic
  • Linear search: O(n) — linear For n=1,000,000: binary search takes ~20 comparisons, linear search takes up to 1,000,000. Q6: Write binary search that finds the LAST occurrence of a target (handling duplicates).
Answer:
python
# runnable
def binary_search_last(lst, target):
    low, high = 0, len(lst) - 1
    result = -1
    while low <= high:
        mid = (low + high) // 2
        if lst[mid] == target:
            result = mid
            low = mid + 1  # Search right for later occurrence
        elif lst[mid] < target:
            low = mid + 1
        else:
            high = mid - 1
    return result

data = [1, 2, 3, 3, 3, 4, 5]
print(binary_search_last(data, 3))  # 4
Q7: What happens if you run binary search on [3, 1, 4, 1, 5, 9] (unsorted) looking for 9?
Answer: It might fail to find 9. The unsorted array means the assumption "if mid < target, target is in the right half" is invalid. The search might discard the half containing 9 and return -1 (not found) even though 9 is present. Q8: Write a program that uses binary search to check if a word is in a dictionary file.
Answer:
python
# runnable
# Simulate reading a dictionary file
dictionary_text = """apple
banana
cherry
date
elderberry
fig
grape
honeydew"""

words = dictionary_text.strip().split("\n")
words.sort()  # ensure sorted

def dictionary_lookup(word):
    idx = binary_search_iterative(words, word.lower())
    return idx != -1

# Test
print(dictionary_lookup("Cherry"))  # True
print(dictionary_lookup("kiwi"))    # False
Q9: Why does the recursive binary search have O(log n) space complexity?
Answer: Each recursive call adds a stack frame. In the worst case, the recursion depth is log₂(n) (the number of times we halve the array before reaching the base case). So we use O(log n) additional memory for the call stack. The iterative version only uses O(1) space. Q10: Implement a function that finds the smallest element in a rotated sorted array (e.g., [4,5,6,7,0,1,2] → 0) using a binary-search-like approach.
Answer:
python
# runnable
def find_min_rotated(arr):
    """Find minimum in rotated sorted array using binary search."""
    low, high = 0, len(arr) - 1

    while low < high:
        mid = (low + high) // 2

        if arr[mid] > arr[high]:
            low = mid + 1  # Min is in right half
        else:
            high = mid      # Min is at mid or left

    return arr[low]

rotated = [4, 5, 6, 7, 0, 1, 2]
print(f"Minimum in {rotated}: {find_min_rotated(rotated)}")  # 0

rotated2 = [3, 4, 5, 1, 2]
print(f"Minimum in {rotated2}: {find_min_rotated(rotated2)}")  # 1

🔗 Cross-References

  • Next Topic: File Operations — Reading data from files to search through.
  • Previous Topic: Recursion — Recursive binary search uses the same principles.
  • BSCS1001 Computational Thinking: Binary search is the classic "divide and conquer" algorithm.
  • BSCS2002 PDSA: Binary search is essential for efficient searching in data structures. Later courses explore binary search trees and balanced trees.
  • Reference: Python for Everybody, Chapter 9 (Section 9.5) — "Searching" mentions linear search; binary search is covered in more depth in PDSA.
  • Video: L81: Binary search implementation, L83: Binary search recursion way, L85: Warm up for binary search, L86: Introduction to binary search Join Discord Previous23. RecursionNext25. File Operations
Document outline

Keep your place and jump directly to a heading.

Table of Contents
System Normal // Awaiting Context

Intelligence Hub

Navigate the knowledge graph to generate context. The Hub adapts dynamically to surface backlinks, related notes, and metadata insights.