You are doing what is often called a three-way binary search, as in each iteration you have three possible results: smaller, larger or equal. This may seem advantageous, because if you find the item early, you may be done with as little as one recursive call. The alternative is to never check for equality, and keep dividing the array in half until you are left with a single item. This has the advantage that, in each iteration, you only do one comparison instead of two, but you will always have to do the full \$\log n\$ recursive calls.
On average you will come out ahead if you do the full \$\log n\$ iterations every time. The way I learned to implement this, is to use an index to the first item to search, and an index past the last item to search for:
def binsearch(haystack, needle, lo=0, hi=None):
if hi is None:
hi = len(haystack)
if hi - lo <= 1:
if haystack[lo] == needle:
return lo
else:
raise ValueError('needle {} not in haystack'.format(needle))
mid = lo + (hi - lo) // 2
if haystack[mid] < needle:
lo = mid + 1
else:
hi = mid
return binsearch(haystack, needle, lo, hi)
If instead of raising an error you return lo
without checking if the needle
is indeed in the haystack
, things start getting interesting:
def binsearch_left(haystack, needle, lo=0, hi=None):
if hi is None:
hi = len(haystack)
if hi - lo <= 1:
return lo
mid = lo + (hi - lo) // 2
if haystack[mid] < needle:
lo = mid + 1
else:
hi = mid
return binsearch_left(haystack, needle, lo, hi)
This function returns the first position in which you could insert needle
, and keep the haystack
sorted, which will also be the first occurrence of needle
in haystack
if there are repeated values. The neat twist is that, by replacing a single <
with a <=
you get the following:
def binsearch_right(haystack, needle, lo=0, hi=None):
if hi is None:
hi = len(haystack)
if hi - lo <= 1:
return lo
mid = lo + (hi - lo) // 2
if haystack[mid] <= needle: # Have replaced < with <=
lo = mid + 1
else:
hi = mid
return binsearch_right(haystack, needle, lo, hi)
This functions returns the last position in which needle
could be inserted in haystack
and still keep it sorted, which corresponds to one past the last occurrence of needle
in haystack if it indeed is there.
Not only is this approach often times faster than the three-way binary search approach, it also gives you much better defined returns if the haystack
has repeated entries: either the first or the last, not an undefined one.
It is no surprise that Python's standard library's bisect
package implements bisect_left
and bisect_right
following these ideas.
Note that writing this functions iteratively is extremely simple, and a much, much better option:
def binsearch_left(haystack, needle, lo=0, hi=None):
if hi is None:
hi = len(haystack)
while hi - lo > 1:
mid = lo + (hi - lo) // 2
if haystack[mid] < needle:
lo = mid + 1
else:
hi = mid
return lo
Also, note that I am computing mid
as lo + (hi - lo) // 2
, not (lo + hi) // 2
. This is not relevant in Python, because integers never overflow. But in other languages, the form I have used will never overflow if lo
and hi
areboth positive values. This is a more or less famous bug, that was shipped with standard libraries for years, until someone figured it out.