Bisect Module Complexity¶
The bisect module provides binary search operations for sorted lists.
Operations¶
| Operation | Time | Space | Notes |
|---|---|---|---|
bisect_left(a, x) |
O(log n) | O(1) | Find leftmost position |
bisect_right(a, x) |
O(log n) | O(1) | Find rightmost position |
bisect(a, x) |
O(log n) | O(1) | Alias for bisect_right |
insort_left(a, x) |
O(n) | O(1) | O(log n) search + O(n) insert (shifts elements in place) |
insort_right(a, x) |
O(n) | O(1) | O(log n) search + O(n) insert (shifts elements in place) |
insort(a, x) |
O(n) | O(1) | Alias for insort_right |
Space Complexity¶
- Binary search operations: O(1) additional space
- Insert operations: O(1) additional space (shifts elements within the existing list)
Implementation Details¶
Binary Search Guarantee¶
import bisect
# Must be sorted!
sorted_list = [1, 3, 3, 3, 5, 7, 9]
# bisect_left: leftmost insertion point
pos = bisect.bisect_left(sorted_list, 3) # O(log n), pos = 1
# Insert here to keep list sorted (before all 3's)
# bisect_right: rightmost insertion point
pos = bisect.bisect_right(sorted_list, 3) # O(log n), pos = 4
# Insert here to keep list sorted (after all 3's)
# Both halve the search range each step, so a run of equal values costs no
# more than a unique one
Finding Elements¶
import bisect
sorted_list = [1, 3, 5, 7, 9]
# Check if element exists
def exists(sorted_list, x):
pos = bisect.bisect_left(sorted_list, x)
return pos < len(sorted_list) and sorted_list[pos] == x
exists(sorted_list, 5) # True - O(log n)
exists(sorted_list, 4) # False - O(log n)
Common Use Cases¶
Sorted Insert¶
import bisect
sorted_list = [1, 3, 5, 7]
# Insert while maintaining order - O(n) overall
# (O(log n) search + O(n) shift)
bisect.insort(sorted_list, 4) # [1, 3, 4, 5, 7]
# Better for many insertions: use list, then sort
# Multiple inserts: O(n log n) with sort
# vs O(n²) with repeated insort
Finding Ranges¶
import bisect
# Find all equal elements
sorted_list = [1, 3, 3, 3, 5, 7, 9]
target = 3
left = bisect.bisect_left(sorted_list, target)
right = bisect.bisect_right(sorted_list, target)
equals = sorted_list[left:right] # All 3's - O(log n) search
Finding Insertion Point for Range¶
import bisect
# Find where range [a, b] fits in sorted list
sorted_list = [1, 5, 10, 15, 20]
target_range = (7, 12)
# Position to insert start of range
start_pos = bisect.bisect_right(sorted_list, target_range[0]) # O(log n)
# Position to insert end of range
end_pos = bisect.bisect_left(sorted_list, target_range[1]) # O(log n)
# Two independent searches: O(log n) total, not O(n)
print(f"Insert range {target_range} at positions {start_pos}-{end_pos}")
Performance Comparison¶
Searching in Sorted Data¶
import bisect
data = sorted(range(1000000))
# Bad: Linear search - O(n)
found = 500000 in data # Scans linearly
# Good: Binary search - O(log n)
pos = bisect.bisect_left(data, 500000) # Much faster!
found = pos < len(data) and data[pos] == 500000
Maintaining Sorted Lists¶
import bisect
# Many insertions scenario
sorted_list = [1, 3, 5, 7, 9]
# Bad: Multiple insort - O(n²)
for item in [2, 4, 6, 8]:
bisect.insort(sorted_list, item) # O(n) each
# Better: Collect, sort once - O(n log n)
sorted_list.extend([2, 4, 6, 8])
sorted_list.sort() # Single O(n log n) operation
Detailed Examples¶
Grade Ranges¶
import bisect
# Map scores to grades
grade_breaks = [60, 70, 80, 90]
grades = ['F', 'D', 'C', 'B', 'A']
def get_grade(score):
i = bisect.bisect(grade_breaks, score)
return grades[i]
print(get_grade(85)) # 'B' - O(log n)
print(get_grade(95)) # 'A' - O(log n)
Timestamp Lookup¶
import bisect
from datetime import datetime, timedelta
# Find events in a time range
events = [
(datetime(2024, 1, 1, 10), 'event1'),
(datetime(2024, 1, 1, 12), 'event2'),
(datetime(2024, 1, 1, 15), 'event3'),
(datetime(2024, 1, 1, 18), 'event4'),
]
timestamps = [e[0] for e in events]
# Find events after specific time
target = datetime(2024, 1, 1, 14)
idx = bisect.bisect_right(timestamps, target)
later_events = events[idx:] # O(log n) search
print(later_events) # Events at 3pm and 6pm
Advanced: Custom Key Functions¶
import bisect
from bisect import bisect_right
# Custom objects - compare by second element
data = [('a', 1), ('b', 3), ('c', 5)]
keys = [x[1] for x in data] # O(n) - building the key list dominates
# Find position for ('d', 4)
pos = bisect_right(keys, 4) # O(log n)
data.insert(pos, ('d', 4)) # O(n) - shifts the tail
# Rebuilding keys per search makes the whole thing O(n); keep it alongside
# data instead
Since Python 3.10 every function in the module takes a key argument, which
removes the parallel list:
import bisect
data = [('a', 1), ('b', 3), ('c', 5)]
# key runs once per probe, so no parallel list is needed
pos = bisect.bisect_right(data, 4, key=lambda item: item[1]) # O(log n) key calls
data.insert(pos, ('d', 4)) # O(n) - shifts the tail
Which to prefer depends on how many searches share the keys: a parallel list
costs O(n) once and no calls per search, while key costs nothing up front and
one call per probe.
Important Notes¶
Sorted Data Requirement
The input list MUST be sorted for binary search to work correctly.
import bisect
# Wrong: Data not sorted
unsorted = [3, 1, 4, 1, 5]
pos = bisect.bisect(unsorted, 2) # Incorrect result!
Amortized Efficiency
For many insertions:
- Multiple
insort()calls: O(n²) overall - Collect then single
sort(): O(n log n) overall
Choose based on your access patterns.
Version Notes¶
- Python 3.10+:
keyparameter added to every function in the module