Skip to the content
Software Made Clear Diagrams that show the mechanism About

Binary search

ANSWER

It answers where a value belongs, not whether it is there. That framing explains the two variants every library ships, why duplicates need a decision, and why the logarithmic search is often the cheapest part of the operation around it.

IN PLAIN TERMS

You find a word in a dictionary like this: open it in the middle, see whether your word comes before or after, and throw half the book away. Do that a dozen times and a hundred thousand pages are down to one.

Binary search is taught as a way to find a value quickly, and that description quietly hides the thing that makes it useful. What the algorithm computes is a position — the place in a sorted sequence where a value belongs. Whether something is already sitting at that place is a separate question, and keeping the two apart explains most of the design decisions in any library that ships it.

It finds a position, not a value#

The mechanism is a repeated halving. Look at the middle of the range; if the target sorts before it, the answer lies in the left half, and if after, the right. Either way half the remaining candidates are eliminated by a single comparison, and the range keeps halving until one position is left. Twenty comparisons cover a million elements, thirty cover a billion, and the growth is so slow that the size of the collection almost stops being a consideration. A balanced search tree makes the same halving structural rather than arithmetic, which is why walking one in order comes out sorted.

Python’s bisect module makes the framing explicit rather than leaving it as an implementation detail. Its documentation notes that, unlike other tools that search for a specific value, these functions are designed to locate an insertion point — and draws the consequence that surprises people: the functions never call eq to determine whether a value has been found, only lt. The search is conducted entirely in terms of ordering. Equality never enters into it, so an object whose ordering and equality disagree will be placed by the first and not consulted about the second.

Left and right are a decision about duplicates#

Once the answer is a position, duplicates stop being an edge case and become a question the caller has to answer. If a sorted list already holds three copies of a value, there are four positions where a fourth could go, and two of them are interesting: immediately before the existing run, or immediately after it.

That is why the module ships both. bisect_left returns an insertion point before any existing entries equal to the value; bisect_right returns one after them. On a list with no duplicates they are identical, which is precisely the trap — code written and tested against distinct values behaves the same either way, and starts differing on the day real data arrives with repeats in it. Choosing between them is choosing whether a new equal element is treated as arriving before or after the ones already there, and that is a decision about your data, not about the algorithm.

The search is rarely the cost#

The logarithmic search is so cheap that it usually stops being what the operation costs, and Python’s documentation puts the point bluntly where it matters most — for insertion, the O(log n) search is dominated by the slow O(n) insertion step. Finding where an element belongs in a sorted array takes a handful of comparisons. Putting it there means shifting everything after it along by one, and that is proportional to the size of the array. Maintaining a sorted list by repeated insertion is therefore a linear operation wearing a logarithmic label.

There is a second cost that is easy to miss, and it decides whether binary search is available at all: the sequence has to be sorted, and it has to support jumping straight to the middle. Both are real constraints. Sorting an unsorted collection to run one search is worse than scanning it, and a structure with no cheap way to reach its midpoint cannot be searched this way at all — which is why a linked list gets no benefit from being in order. When the question is simply whether a specific value is present, the documentation offers the plainer advice: for locating specific values, dictionaries are more performant.

IF YOU REMEMBER ONE THING

Binary search returns a position. Everything else — the two variants, the indifference to equality, the linear insert that follows — comes from taking that literally.

Questions people also ask

3 QUESTIONS
Why do libraries offer two versions of binary search?

Because a sorted list with duplicates has two defensible answers and the caller has to pick one. Python's bisect_left returns an insertion point before any existing entries equal to the value; bisect_right returns one after them. Neither is more correct — they differ only where equal elements already sit, which is exactly the case people forget to test.

Does binary search use equality at all?

Python's does not, and the documentation is explicit about it: the functions never call __eq__ to determine whether a value has been found, only __lt__. It is worth knowing when your objects define the two inconsistently — the search will still return a position, decided entirely by the ordering, whatever equality would have said.

If binary search is O(log n), why is my code still slow?

Usually because the search is not what you are paying for. Python's documentation says it directly about insertion: the O(log n) search is dominated by the slow O(n) insertion step. Finding the position in a sorted array is nearly free; making room at that position is not, because everything after it has to move.