Internalizing Python bisect_right()
and bisect_left()
Published:
Binary search is a popular topic in coding interviews. I was struggling with finding the right template for implementing binary search as it’s annoying to get the equal sign or less than equal sign correctly in the right place. Eventually I decided to use a template similar to bisect_right()
and bisect_left()
in Python bisect
library.
Here I’m going to share how I internalize bisect_right()
and bisect_left()
so that I no longer need to memorize it.
Of course, if your interviewer gives you a green light to use Python’s built-in libraries, I’d recommend just using it so that you don’t need to reinvent the wheel. And, it also showcases your familiarity with the Python language. Or, you can implement binary search yourself first and then tell your interviewer that there’s actaully Python built-in library that you can use.
Python’s bisect_right and bisect_left
bisect_right
(also known as bisect
) and bisect_left
perform binary search operations on sorted lists, returning the index where an element needs to be inserted to maintain the sorted order.
bisect_right
: Given a sorted lista
, find the right most insertion position (i.e. index) for valuex
to maintain sorted order. Ifx
already appears in the list, the insertion point is after (to the right of) any existing entries. In other words, the returned indexi
partitions the list into two halves such that, all elements less than or equal tox
are ina[:i]
and all elements greater thanx
are ina[i:]
.bisect_left
: Given a sorted lista
, find the left most insertion position (i.e. index) for valuex
to maintain sorted order. This function works similarly tobisect_right
, but ifx
already appears in the list, the insertion point returned is the first (or leftmost) occurrence ofx
. Therefore, the returned indexi
partitions the list into two halves such that, all elements less thanx
are ina[:i]
and all elements greater than or equal tox
are ina[i:]
.
Here are the links to their implementations: bisect_right
and bisect_left
Internalizing bisect_right and bisect_left
To truly understand these functions, I’ll start by creating simplified versions of them and then gradually refine them to match the Python library implementations.
Let’s start with this code for bisect_right
that does the same thing but is easier to understand in my opinion:
def bisect_right(a: List[int], target: int) -> int:
# Assuming `a` is a sorted list
left = 0
# It's possible to insert a new element at (last index of a) + 1 if the new element is greater than all existing elements
right = len(a)
while left < right:
middle = (left + right) // 2
if target > a[middle]:
# The insertion position for `target` must be greater than `middle`
left = middle + 1
elif target < a[middle]:
# The insertion position for `target` must be less than or equal to `middle`
right = middle
else:
# If `target == a[middle]`, the right most insertion index must be at least `middle + 1`
# since all elements <= `target` should be to the left of insertion position.
left = middle + 1
return left
If we refactor the above code a little bit, we have what’s close to the bisect_right
in Python’s bisect
library:
def bisect_right(a: List[int], target: int) -> int:
left = 0
right = len(a)
while left < right:
middle = (left + right) // 2
if target < a[middle]:
right = middle
else:
left = middle + 1
return left
Similarly, we can understand bisect_left
by looking at a simpler version:
# Assuming `a` is a sorted list
def bisect_left(a: List[int], target: int) -> int:
left, right = 0, len(a)
while left < right:
middle = (left + right) // 2
if target > a[middle]:
# The insertion position for `target` must be greater than `middle`
left = middle + 1
elif target < a[middle]:
# The insertion position for `target` must be less than or equal to `middle`
right = middle
else:
# If `target == a[middle]`, there are two cases to consider:
# 1. If there is only one element whose value equal to target in the list. Then the
# insertion position is equal to `middle`
# 2. If there are more than one element whose value equal to target in the list, then
# we want to keep searching left till we find the first occurrence of `target`. So
# the insertion position must be less than or equal to `middle`.
right = middle
return left
And again, if we refactor the above code a little bit, we have what’s close to bisect_left
in Python bisect
library:
def bisect_left(a: List[int], target: int) -> int:
left, right = 0, len(a)
while left < right:
middle = (left + right) // 2
if target > a[middle]:
left = middle + 1
else:
right = middle
return left
So now I alway start with writing the simpler version first because it’s easy to just write it out without memorizing it. And then I’ll merge conditions that do the same thing and finally get to the implementation in Python’s bisect
library.
Finding the Leftmost or Rightmost Occurrence of an Element
In many cases, what we want is not the leftmost or rightmost insertion position for a target element in a sorted list. Instead, we want to find the leftmost or rightmost occurrence of that element. We can modify the return value of the above code to achieve this. E.g. if we want to find the rightmost element that is less than or equal to target, we will return left - 1
instead in bisect_right
. If left - 1
< 0, this means all elements in the list are greater than target.
A note on integer overflow
In other programming languages (e.g. C++) where there’s a limit on the size of integer, you might want to calculate middle
by middle = left + (right - left) // 2
to avoid integer overflow.
Python doesn’t have a limit on integer value so it’s ok to do middle = (left + right) // 2