Python Bisect Module: Binary Search and Insertion
The Python bisect module provides specialized tools for
maintaining lists in sorted order without having to repeatedly resort
them after every insertion. By implementing standard binary search
algorithms, the module locates positions in logarithmic time, enabling
fast lookups and sequence manipulations. This article explains how
bisect functions internally, the distinction between its
left and right search variants, how insertion works, and the performance
characteristics associated with standard Python lists.
How Binary Search Operates in Bisect
At its core, the bisect module implements a bisection
algorithm that operates on pre-sorted sequences. Instead of checking
elements sequentially from left to right (which takes linear time, \(O(n)\)), it compares the target value
against the middle element of the search window.
If the target is smaller than the midpoint, the algorithm discards the upper half; if larger, it discards the lower half. It repeats this division until it narrows down the exact insertion index, completing the search in \(O(\log n)\) time.
Finding
Insertion Points: bisect_left vs.
bisect_right
The module provides two primary inspection functions to locate where an element should be placed to preserve order:
bisect_left(a, x): Returns an insertion index such that ifxalready exists ina, the returned index will be to the left of (before) any existing entries ofx.bisect_right(a, x)(or the aliasbisect): Returns an insertion index such that ifxalready exists ina, the returned index will be to the right of (after) any existing entries ofx.
import bisect
data = [10, 20, 20, 20, 30]
# Locate insertion point for an existing value
idx_left = bisect.bisect_left(data, 20) # Returns 1 (before existing 20s)
idx_right = bisect.bisect_right(data, 20) # Returns 4 (after existing 20s)Both functions accept optional lo and hi
arguments to restrict the search to a specific slice of the list.
Inserting
Elements: insort_left and insort_right
Finding an index is only the first step; modifying the sequence
requires inserting the value. The insort functions combine
the search and insertion steps:
insort_left(a, x): Finds the index usingbisect_leftand executesa.insert(index, x).insort_right(a, x)(orinsort): Finds the index usingbisect_rightand executesa.insert(index, x).
import bisect
numbers = [1, 3, 4, 7]
bisect.insort(numbers, 5)
# Output: [1, 3, 4, 5, 7]
print(numbers)Computational Complexity and Limitations
While the search phase is efficient at \(O(\log n)\), the actual insertion step is bounded by the underlying data structure.
Python lists are dynamic arrays stored in contiguous memory blocks.
Inserting an element into an arbitrary index requires shifting every
subsequent element to the right by one position. Consequently,
insort runs in \(O(n)\)
time due to the list insertion step, even though locating the insertion
point takes only \(O(\log n)\)
comparisons.
Using the key
Parameter
Starting in Python 3.10, all functions in the bisect
module support a key argument. This allows binary searching
and inserting on lists of complex objects sorted by a specific attribute
or transformation function:
import bisect
records = [("Alice", 85), ("Bob", 90), ("Charlie", 95)]
# Find insertion point based on the score (the second tuple element)
idx = bisect.bisect_left(records, 90, key=lambda x: x[1]) # Returns 1The key function is applied lazily to elements during
comparisons, maintaining the \(O(\log
n)\) search efficiency without requiring full list
transformations.