Binary Search — Divide & Conquer Search
2701 words
14 min read
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:
- Explain how binary search works (divide and conquer)
- Implement binary search iteratively (while loop)
- Implement binary search recursively
- Calculate time complexity: O(log n)
- Understand the requirement that data must be sorted
- 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]:
pseudoIteration 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.
24.3 Iterative Binary Search
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:
pseudo23 found at index 5 1 not found 72 found at index 9 100 not found
24.4 Recursive Binary Search
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:
pseudo5 -1 9
24.5 Time Complexity Analysis
| Algorithm | Best Case | Average Case | Worst Case |
|---|---|---|---|
| Linear Search | O(1) — first element | O(n) | O(n) — last element |
| Binary Search | O(1) — middle element | O(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:
pseudon | 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:
pseudoFirst 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:
pseudoInsert 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:
pseudoMountain: [1, 3, 5, 7, 9, 8, 6, 4, 2] Peak at index 4: value = 9
📐 Key Concepts Reference
| Concept | Description | Formula/Code |
|---|---|---|
| Binary search | Divide sorted list in half repeatedly | mid = (low+high)//2 |
| Precondition | List must be SORTED | sorted(lst) |
| Time complexity | O(log n) | log₂(n) comparisons |
| Space (iterative) | O(1) | No extra memory |
| Space (recursive) | O(log n) | Call stack frames |
| Mid formula | Avoids integer overflow | mid = low + (high-low)//2 |
| Found | Target equals middle | lst[mid] == target |
| Not found | low > high | return -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:pseudolow=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 3Q4: 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")) # -1Q5: 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)) # 4Q7: 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")) # FalseQ9: 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