Interpolation search
Search a sorted list of evenly spread numbers by guessing where the target should be from its value, like opening a phone book near the right letter.
O(log log n) on evenly spread dataSpace O(1)2 practice problemsStep-through pictureThe idea
Binary search always checks the middle. But if you look up "Zhang" in a phone book, you don't open it in the middle. You open it near the end, because Z is near the end of the alphabet. Interpolation search does the same with numbers.
In a sorted list from items[low] to items[high], if the values are spread evenly, the target should sit about (target - items[low]) / (items[high] - items[low]) of the way along. That fraction times the number of positions gives a guess, pos. If the guess is wrong, the search moves low or high past it and guesses again in the smaller range.
On evenly spread data, the guesses are so good that the search needs about log(log n) steps, which is only 5 for a billion items. On badly spread data, such as [1, 2, 3, 4, 1000], the guesses can be poor and the search can take n steps.
An everyday example
Opening a dictionary near the right page
To find the word "melon" in a dictionary, you open it a bit before the middle, because M is about halfway through the alphabet. You land on "magnet", a little early, so you look a bit further on. To find "apple" you open it near the front. You use what the word is to guess where it is, instead of always opening to the exact middle.
| In the example | In the code |
|---|---|
| Where M falls in the alphabet | (target - items[low]) / (items[high] - items[low]) |
| The page you open to | pos |
| Landing a little early: look later | low = pos + 1 |
| Landing a little late: look earlier | high = pos - 1 |
From the slow way to interpolation search
- Binary search always checks the middle, whatever the values are. Looking for
45in a list that runs from5to88, it doesn't use the fact that45is about halfway between those two values. The values themselves hint at where the target sits, and binary search ignores that hint. - If the values rose by the same amount at every step, you could compute the position exactly. With values from
items[low]toitems[high]spread evenly overhigh - lowsteps, the target sits the fraction(target - items[low]) / (items[high] - items[low])of the way along. Multiply that byhigh - low, round down, and addlow: that is the guesspos. - Real data is rarely perfectly even, so the guess may miss. But a miss still tells you which side to keep, just like a binary search check: if
items[pos] < target, the target is afterpos. Then compute a new guess from the new ends, which sit closer to the target and usually give a better line. - Two guards keep it safe: stop when the target is outside
items[low]toitems[high], which also keepsposinside the range, and handle the case where both ends are equal, which would divide by zero. Guess from the values, check, keep the side that can hold the target: that loop is interpolation search.
How it works
- Set
low = 0andhigh = items.length - 1. - While the target is between
items[low]anditems[high], estimate its position:pos = low + Math.floor((target - items[low]) * (high - low) / (items[high] - items[low])). - If
items[pos]is the target, returnpos. - If
items[pos]is smaller, the target is to the right:low = pos + 1. - If it is bigger, the target is to the left:
high = pos - 1. - If all values in the range are equal, avoid dividing by zero: just compare
items[low]with the target.
Watch it run
This picture runs the code below on a real input. Press Step to move one step at a time, or Play to watch. The highlighted line is the one that just ran. Change the input to see other cases.
See every step at onceThe same run, laid out side by side
The code
Point at a note to light up the lines it explains.
function interpolationSearch(items, target) {let low = 0;let high = items.length - 1;while (low <= high && target >= items[low] && target <= items[high]) {if (items[high] === items[low]) {return items[low] === target ? low : -1;}const pos = low + Math.floor(((target - items[low]) * (high - low)) / (items[high] - items[low]));if (items[pos] === target) return pos;if (items[pos] < target) {low = pos + 1;} else {high = pos - 1;}}return -1;}
How fast it is
| Time | O(log log n) on evenly spread data |
|---|---|
| Extra memory | O(1) |
On evenly spread data, each guess lands very close to the target, so the range shrinks extremely fast. When a few huge or tiny values skew the line, each guess may only move one step.
- Best case
O(1)The values are perfectly even, such as
[12, 24, 36, 48, ...], so the first guess is exact. - Average case
O(log log n)The values are spread fairly evenly, like random numbers drawn from one range. On a sorted list of a million random numbers, a test run needed about 4 guesses on average, against about 20 checks for binary search.
- Worst case
O(n)A few huge values bend the line, such as the numbers
0to999,998followed by10^15. Finding999,998in that list took 999,999 guesses in a test run, one step at a time.
Common mistakes
- Dividing by
items[high] - items[low]when it is0. Handle the "all equal" range first. - Dropping the
target >= items[low] && target <= items[high]check. A target outside the range makespospoint outside the list. - Computing the fraction with integer-style rounding too early, such as
Math.floor((target - items[low]) / (items[high] - items[low])) * (high - low). The fraction rounds to0and every guess islow. Multiply first, then divide and round. - Using it on strings or on data with a few huge outliers. Binary search is safer there.
When to use it
Look for these clues in a problem:
- The list is sorted and its values are spread fairly evenly, such as IDs, timestamps at a steady rate, or positions on a grid.
- The list is very large and you want fewer checks than binary search.
- You can compute how far apart two values are, so they are numbers, not just comparable items.
Check yourself
Answer each one in your head first, then open it.
In [10, 20, 30, 40, 50], where does the first guess for 40 land?
pos = 0 + Math.floor((40 - 10) * 4 / (50 - 10)) = 3, and items[3] is 40, so one guess is enough.
Why must the loop check target >= items[low] && target <= items[high]?
It keeps the fraction between 0 and 1, so pos stays inside the range. It also stops the search as soon as the target can't be in the range.
Why does one huge value at the end slow it down so much?
It stretches the gap items[high] - items[low], so every fraction is tiny and every guess lands near low. Each guess then drops only a few items.
What does O(log log n) mean for a billion items?
log₂ of a billion is about 30, and log₂ 30 is about 5. So on even data it needs about 5 guesses where binary search needs about 30.
Practice problems
Stuck on one? Tuto is in the playground too, and gives you hints, never the answer.
0 of 2 solved. Solve one to earn the bronze medal, half for silver, and all of them for gold.
Start practicingGo further
You don't need these to start practicing. Open one when you want more.
Why it works
The reasoning that makes it always right
The promise is the one from binary search: if target is in the list, it is somewhere from low to high. The loop also checks that target is between items[low] and items[high]. If it isn't, the sorted order means it can't be in the range, so returning -1 is right.
That check also keeps the guess inside the range. The fraction (target - items[low]) / (items[high] - items[low]) is between 0 and 1, so pos is between low and high. From there each step works like binary search: if items[pos] < target, everything up to pos is too small, so low = pos + 1 keeps the promise, and the other side works the same way.
The loop ends because each step leaves pos out, so the range shrinks by at least one. The speed depends on the data, but the correctness doesn't: even the worst guess drops at least one item and never drops the target. That is why skewed data makes it slow, but never wrong.
Going deeper
3 more ideas
Where the guess comes from
Draw a straight line from the point (low, items[low]) to the point (high, items[high]). If the values are spread evenly, every item sits on that line. The guess pos is where the line reaches the height target. That is what "interpolation" means: estimating a point between two known points.
When it gets slow
Find 9 in [1, 2, 3, 4, 5, 6, 7, 8, 9, 1000]. The huge last value drags every guess to the left: the first guess is Math.floor(8 * 9 / 999) = 0, then 1, then 2, one step at a time. It takes 9 guesses, as slow as a linear search. On uneven data, binary search is safer.
Why the loop checks the range first
The loop only runs while target >= items[low] and target <= items[high]. This keeps pos inside low..high: the fraction is between 0 and 1. Without this check, a target outside the range gives a pos outside the list.
Work it by hand
Every step in a table, with why
Look for 45 in a sorted list of 10 numbers whose last gap is much bigger than the others.
items = [5, 9, 14, 20, 23, 30, 41, 45, 52, 88], target = 45
| Step | low | high | Guess | pos | items[pos] | What happens | Why |
|---|---|---|---|---|---|---|---|
| 1 | 0 | 9 | (45 - 5) * 9 / (88 - 5) is about 4.34 | 4 | 23 | 23 < 45, so low = 5 | The guess lands too early. The last gap, from 52 to 88, is much bigger than the rest, which bends the line and pulls every guess to the left. |
| 2 | 5 | 9 | (45 - 30) * 4 / (88 - 30) is about 1.03 | 6 | 41 | 41 < 45, so low = 7 | The new ends are 30 and 88. The fraction part rounds down to 1, so pos = 5 + 1 = 6. Still too early, but closer. |
| 3 | 7 | 9 | (45 - 45) * 2 / (88 - 45) is 0 | 7 | 45 | Match: return 7 | Now items[low] is 45 itself, so the guess is low. Found. |
It returns 7 after 3 guesses. Binary search would need only 2 checks here, because the one large gap at the end makes the straight line a poor fit. On even data the first guess is often exact: in [12, 24, 36, 48, 60, 72, 84, 96], 72 is found at pos = 5 with one guess.
Compared with other ways
When to pick something else
| Approach | Time | Extra memory | Choose it when |
|---|---|---|---|
| Interpolation search | O(log log n) | O(1) | Huge sorted lists of evenly spread numbers, where every read is expensive, such as reads from disk. |
| Binary search | O(log n) | O(1) | Any sorted list, including uneven values and strings. Its speed never depends on the data. |
| Direct index from a formula | O(1) | O(1) | The values are perfectly regular, so the position can be computed in one step. |
Extra memory: O(1): low, high, and pos.
How the steps grow
- Interpolation search
O(log log n) - Binary search
O(log n) - Direct index from a formula
O(1)
| Approach | n = 10 | n = 1,000 | n = 1,000,000 |
|---|---|---|---|
| Interpolation search | 2 | 3 | 4 |
| Binary search | 3 | 10 | 20 |
| Direct index from a formula | 1 | 1 | 1 |
Inputs that need care
5 tricky inputs
- All values equal:
[7, 7, 7] items[high] - items[low]is0, so the formula would divide by zero. The code checksitems[high] === items[low]first and returnslowif that value is the target, else-1.- A target outside the values:
[5, 9, 14], target2or20 - The loop condition fails before any guess, and it returns
-1at once. Without that check,poswould be negative or past the end. - An empty list:
items = [] highis-1, solow <= highis false and it returns-1. The reads ofitems[low]are never reached, because&&stops at the first false part.- A missing target between values: the trace's list, target
43 - Guesses land on
23,30, and41. Thenlowbecomes7, whereitems[7] = 45is bigger than43, so the loop condition fails and it returns-1. - Dates or strings
- Convert dates to numbers with
getTime()first, so the formula can measure gaps. Strings have no simple "how far apart" measure, so search them with binary search instead.
Variations and follow-ups
Twists interviewers add
Mixing in binary search steps
A safer version alternates: one interpolation guess, then one binary search check in the middle of what's left. The binary step makes sure the range at least halves every two steps, so the worst case becomes O(log n), while even data still gets the fast guesses.
An index from a known rate
When the data is perfectly regular, like log lines every 5 seconds or rows of fixed height, the first guess is always exact and no loop is needed: index = Math.floor((time - start) / 5). That is interpolation with a known line.
Straightening uneven data
If values grow along a known curve, such as i * i * i, the line is a poor fit: in a test on a million such values, plain interpolation needed about 250 guesses on average. Searching on Math.cbrt(items[i]) instead makes the values even again, and the guesses become good.
In an interview
Spot it, explain it, answer follow-ups
- Spot it from "uniformly distributed", "evenly spaced", or a sorted list of numbers where the question hints that you can beat
O(log n). - Explain the guess as a straight line between the points
(low, items[low])and(high, items[high]), and say where that line reaches the heighttarget. Then say the rest is binary search with a smarter choice ofmid. - Name the risks up front: dividing by zero when the ends are equal, a guess outside the range when the target is outside the values, and
O(n)time on skewed data. Interviewers mostly ask about the last one. - The usual follow-up is "why don't standard libraries use it?". Answer: real data is often uneven, the worst case is linear, and each step needs a multiply and a divide. Binary search's steady
O(log n)is the safer default.
Where frontend code uses it
The same idea in real UI code
- A virtualized list with a fixed row height finds the first visible row with
Math.floor(scrollTop / rowHeight). That is interpolation search where the first guess is always exact, because the rows are perfectly even. - Seeking in a video with a steady frame rate: the frame for a time is about
time × fps, a direct guess instead of a search. - Looking up a log entry by timestamp when events arrive at a steady rate: guess the index from the time, then fix it up with a few steps.