Python Linear and Binary Search

Implement linear and binary search, keep binary search's interval correct, and use the bisect module for sorted data.

Lesson 82 of 107Advanced13 min read

Searching means finding whether, and where, a value appears in a collection. The two classic algorithms are:

  • Linear search: check each item in turn. Works on any list; O(n).
  • Binary search: repeatedly halve a sorted list, discarding the half that can't contain the target. O(log n): about 20 checks for a million items.
Python
from bisect import bisect_left
photos = [11, 18, 24, 39, 52]          # sorted photo ids
i = bisect_left(photos, 24)
print(i, photos[i] == 24)              # 2 True

This lesson implements both, explains the invariant that keeps binary search correct, and shows how Python's bisect module does the work for you. The examples look up photo ids in a camera club archive. Complexity terms come from time and space complexity.

Sorted prerequisite

Binary search compares the target with the middle item. Because the list is sorted, everything to the left is smaller and everything to the right is larger, so one comparison eliminates half the remaining items:

Text
search 39 in [11, 18, 24, 39, 52]
middle 24 < 39  → discard 11, 18, 24 → [39, 52]
middle 39 == 39 → found

The ordering is what makes discarding safe. On unsorted data, the target could be in the half that was thrown away, so binary search can miss items that are present, without any error to warn you.

The list must be sorted by the same key you search with. A list of photos sorted by date can't be binary-searched by id. If data changes often, keeping it sorted costs time on every insert, so binary search pays off when you search much more often than you modify.

Interval invariant

A correct binary search maintains an invariant: if the target is present, it lies within the current interval. The half-open convention [low, high), which includes low and excludes high, is the easiest to get right:

Python
def binary_search(items, target):
    low, high = 0, len(items)            # interval [low, high)
    while low < high:                    # non-empty interval
        mid = (low + high) // 2
        if items[mid] < target:
            low = mid + 1                # target is right of mid
        elif items[mid] > target:
            high = mid                   # target is left of mid
        else:
            return mid
    return -1

Each branch keeps the invariant: when items[mid] < target, mid and everything before it can't be the answer, so low moves past it. The loop ends when the interval is empty.

Most binary search bugs come from mixing conventions: using high = len(items) - 1 (inclusive) with while low < high, or high = mid - 1 with a half-open interval. The result is an infinite loop or a missed first or last element. Test empty lists, one-element lists, and targets at both ends and absent.

Insertion positions

The bisect module implements binary search in C. Its functions return insertion positions: where a value would go to keep the list sorted.

Python
from bisect import bisect_left, bisect_right, insort
photos = [11, 18, 24, 24, 39]
print(bisect_left(photos, 24))    # 2: before existing 24s
print(bisect_right(photos, 24))   # 4: after existing 24s
print(bisect_left(photos, 30))    # 4: where 30 would go
insort(photos, 30)                # insert keeping order

An insertion position isn't proof the value exists: bisect_left(photos, 30) returns 4 even though 30 wasn't there. Always check: i < len(photos) and photos[i] == target.

bisect_right - bisect_left counts how many times a value appears, and the pair finds ranges, such as every photo id between 20 and 40. Since Python 3.10, the functions accept key= to search records by one field, provided the list is sorted by that same key.

Independent practice and a worked check

Task: implement a linear search and a binary search and test empty input, absent values and end positions.

Python
def linear_search(items, target):
    for i, item in enumerate(items):
        if item == target:
            return i
    return -1

def binary_search(items, target):
    low, high = 0, len(items)
    while low < high:
        mid = (low + high) // 2
        if items[mid] < target:
            low = mid + 1
        elif items[mid] > target:
            high = mid
        else:
            return mid
    return -1

cases = [([], 5, -1), ([7], 7, 0), ([3, 8, 12, 20], 3, 0),
         ([3, 8, 12, 20], 20, 3), ([3, 8, 12, 20], 10, -1)]
for items, target, expected in cases:
    assert linear_search(items, target) == binary_search(items, target) == expected
print("all", len(cases), "cases pass")

Both functions agree on every edge case, including the empty list and targets at each end. For the worked check, binary search requires a suitably sorted search space, ordered by the same key you search with.

Reference and next practice

The bisect module documentation includes recipes for searching sorted lists. Continue with sorting algorithms and hash tables. Practise with the Python exercises and Python quiz.

Linear and Binary Search Summary

  • Linear search: A linear search examines candidates in order until it finds a match or finishes.
  • Sorted prerequisite: Binary search relies on ordering that allows part of the interval to be discarded.
  • Interval invariant: Maintain a search interval that still contains every possible matching position.
  • Insertion positions: bisect_left returns where a value could be inserted while preserving order.

Linear and Binary Search Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Linear and Binary Search in a small program

Python
from bisect import bisect_left
photos = [11, 18, 24, 39, 52]
target = 24
position = bisect_left(photos, target)
found = position < len(photos) and photos[position] == target
print(position, found)
Output
2 True
Line-by-line explanation
  1. Line 4bisect_left returns the position where 24 is, or would be inserted.
  2. Line 5Checking both the bounds and the value turns the position into a reliable "found".
Example 2

Comparing the two searches step by step

Python
def linear_steps(items, target):
    for steps, item in enumerate(items, start=1):
        if item == target:
            return steps
    return len(items)

def binary_steps(items, target):
    low, high, steps = 0, len(items), 0
    while low < high:
        steps += 1
        mid = (low + high) // 2
        if items[mid] < target:
            low = mid + 1
        elif items[mid] > target:
            high = mid
        else:
            return steps
    return steps

archive = list(range(1000, 1_001_000, 10))     # 100,000 sorted photo ids
for target in [1000, 500_000, 1_000_990, 7]:
    print(f"{target:>9}: linear {linear_steps(archive, target):>6}  binary {binary_steps(archive, target)}")
Output
     1000: linear      1  binary 17
   500000: linear  49901  binary 15
  1000990: linear 100000  binary 16
        7: linear 100000  binary 17
Line-by-line explanation
  1. Lines 1 to 5Linear search is unbeatable when the target is first, and checks everything when it's last or missing.
  2. Lines 7 to 18Binary search needs at most about log₂(100,000) ≈ 17 steps for any target.
  3. Line 21The last target is absent: both searches must exhaust their possibilities, but binary search's "everything" is 17 steps.

Common Linear and Binary Search Mistakes

Treating an insertion position as a match

Don't
i = bisect_left(photos, 30)
print("found at", i)                  # 30 isn't in the list
Do
i = bisect_left(photos, 30)
if i < len(photos) and photos[i] == 30:
    print("found at", i)
else:
    print("not found; would insert at", i)

Binary searching unsorted data

Don't
photos = [52, 11, 39, 24, 18]
bisect_left(photos, 24)               # meaningless result
Do
photos = sorted([52, 11, 39, 24, 18])
bisect_left(photos, 24)               # 2

Linear and Binary Search in Real Projects

Searching in practice

Binary search is everywhere sorted data lives: database indexes (B-trees generalise the idea), looking up a timestamp in sorted logs, finding the right price band or tax bracket with bisect, and git bisect, which binary-searches commit history to find the change that introduced a bug.

In application code, the usual choice is a dictionary or set for exact lookups by key, bisect for range queries and nearest values in sorted data, and a simple linear scan for small or one-off searches. Writing binary search by hand is mostly for learning and interviews, where getting the boundaries right is the point of the exercise.

Linear and Binary Search FAQ

What is the difference between linear and binary search?

Linear search checks items one by one and works on any data in O(n) time. Binary search halves a sorted list at each step and runs in O(log n) time, but only works when the data is sorted by the search key.

Does Python have a built-in binary search?

Yes, the bisect module. bisect_left(items, x) returns the leftmost insertion position; check items[i] == x to confirm a match. insort() inserts while keeping a list sorted.

Why is binary search O(log n)?

Each comparison halves the remaining interval, so after k steps there are n / 2ᵏ candidates. The search ends when one or none remains, after about log₂ n steps: around 20 for a million items.

Is binary search faster than using a set?

For exact membership tests, a set's O(1) average lookup is faster than binary search's O(log n). Binary search wins when you need order-related queries, such as ranges, the nearest value or the number of items below a threshold.

Should I write binary search recursively or iteratively?

Iteratively in Python. It avoids recursion depth limits and function-call overhead, and the loop makes the interval invariant easy to see. A recursive version is fine for learning.

Linear and Binary Search Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choiceeasy

What prerequisite does binary search require?

Multiple choicemedium

Which review concern is valid for linear and binary search?

Linear and Binary Search Quiz

2 questions to check what you learned.

  1. What prerequisite does binary search require?

    • A random input order
    • A suitably sorted search space
    • Python ignores the distinction
  2. Which statement correctly describes interval invariant?

    • Maintain a search interval that still contains every possible matching position.
    • Only the example output matters, not the rule
    • Every operation has the same behavior