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.
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 TrueThis 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.
Linear search
Linear search walks through the items from the start until it finds the target or runs out:
def linear_search(items, target):
for index, item in enumerate(items):
if item == target:
return index
return -1
photos = [52, 11, 39, 24, 18] # unsorted is fine
print(linear_search(photos, 24)) # 3
print(linear_search(photos, 99)) # -1It needs no preparation and works on unsorted data, any iterable, and with any condition, such as "first photo taken after 6 pm". Its cost grows with the list: the worst case, a missing target, checks every item.
Python's built-ins are linear searches: target in items, items.index(target) (which raises ValueError if missing) and next((x for x in items if condition), None). For a single search on a small or unsorted list, linear search is the right choice. For many searches, consider sorting once and using binary search, or putting the values in a set or dictionary for O(1) average lookups.
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:
search 39 in [11, 18, 24, 39, 52]
middle 24 < 39 → discard 11, 18, 24 → [39, 52]
middle 39 == 39 → foundThe 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:
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 -1Each 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.
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 orderAn 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.
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.
Linear and Binary Search in a small program
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)2 TrueLine-by-line explanation
- Line 4
bisect_leftreturns the position where 24 is, or would be inserted. - Line 5Checking both the bounds and the value turns the position into a reliable "found".
Comparing the two searches step by step
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)}") 1000: linear 1 binary 17
500000: linear 49901 binary 15
1000990: linear 100000 binary 16
7: linear 100000 binary 17Line-by-line explanation
- Lines 1 to 5Linear search is unbeatable when the target is first, and checks everything when it's last or missing.
- Lines 7 to 18Binary search needs at most about log₂(100,000) ≈ 17 steps for any target.
- 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
i = bisect_left(photos, 30)
print("found at", i) # 30 isn't in the listi = 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
photos = [52, 11, 39, 24, 18]
bisect_left(photos, 24) # meaningless resultphotos = sorted([52, 11, 39, 24, 18])
bisect_left(photos, 24) # 2Linear 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.
What prerequisite does binary search require?
Which review concern is valid for linear and binary search?
Linear and Binary Search Quiz
2 questions to check what you learned.
What prerequisite does binary search require?
- A random input order
- A suitably sorted search space
- Python ignores the distinction
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