Ternary search
Find the peak (or the lowest point) of a list or function that goes up and then down by comparing two inner points.
O(log n)Space O(1)3 practice problemsStep-through pictureThe idea
Some data rises and then falls, like the height of a thrown ball over time, or a list such as [1, 3, 8, 12, 15, 11, 6, 2]. There is one peak, and we want to find it. The list is not sorted, so plain binary search does not apply. Ternary search does.
It picks two points inside the range, m1 and m2, about one third and two thirds of the way along. If the value at m1 is smaller than the value at m2, the curve is still rising at m1, so the peak can't be at or before m1. Otherwise, the peak can't be at or after m2. Either way, about a third of the range is thrown away.
The same idea works on smooth functions with real numbers, such as "which point on this line is closest to the mouse?". There you repeat a fixed number of times, say 100, and the range shrinks to a tiny width around the best point.
An everyday example
Finding the top of a hill in fog
You walk along a trail over a single hill in thick fog, and you want the highest point. You send two friends to two spots, one a third of the way along and one two thirds along, and each tells you their height. If the first friend is lower than the second, the top can't be behind the first friend, because the trail was still climbing there. So you forget the first third of the trail. Then you send them out again on what is left.
| In the example | In the code |
|---|---|
| The trail from start to end | low to high |
| The two friends' spots | m1 and m2 |
| The first friend is lower | values[m1] < values[m2] |
| Forget the trail up to the first friend | low = m1 + 1 |
| Forget the trail from the second friend on | high = m2 - 1 |
From the slow way to ternary search
- To find the biggest value in a list that rises and then falls, the obvious way scans every item and keeps the largest:
nchecks. That ignores the shape. In[2, 5, 9, 14, 20, 26, 31, 27, 18, 11, 6, 3], each item can tell you which side of the peak you are on, if you compare it with another one. - One check at one index isn't enough. Knowing that
values[5]is26says nothing about where the peak is. Two checks do help: compare the values at two indexesm1 < m2. Ifvalues[m1] < values[m2], the curve was still rising somewhere afterm1, so the peak can't be atm1or before it. - If instead
values[m1] >= values[m2], the curve was already falling bym2, so the peak can't be atm2or after it. Placingm1andm2at one third and two thirds of the range means either result drops about a third of it, so each loop leaves about two thirds of the range. - When three or fewer items are left, the range is too small to split into thirds, so just check those items one by one. Two probes at the thirds, drop the side that can't hold the peak, then a short scan at the end: that loop is ternary search.
How it works
- Set
lowandhighto the ends of the range. - While the range has more than three items, compute
third = Math.floor((high - low) / 3),m1 = low + third, andm2 = high - third. - If
values[m1] < values[m2], the peak is afterm1: setlow = m1 + 1. - Otherwise the peak is before
m2: sethigh = m2 - 1. - When three or fewer items are left, check them one by one and return the index of the largest.
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 ternaryPeak(values) {let low = 0;let high = values.length - 1;while (high - low > 2) {const third = Math.floor((high - low) / 3);const m1 = low + third;const m2 = high - third;if (values[m1] < values[m2]) {low = m1 + 1;} else {high = m2 - 1;}}let best = low;for (let i = low + 1; i <= high; i++) {if (values[i] > values[best]) best = i;}return best;}
How fast it is
| Time | O(log n) |
|---|---|
| Extra memory | O(1) |
Each loop throws away about a third of the range, so the number of loops grows with the logarithm of n. Only a few indexes are stored.
- Best case
O(log n)There is no early exit: even when the first probe lands on the peak, the loop keeps going until three items are left.
- Average case
O(log n)Any single-peak list: each loop keeps about two thirds of the range.
- Worst case
O(log n)About
logbase1.5ofnloops, at most about 31 for a million items, with two reads per loop.
Common mistakes
- Using it on data with flat parts, such as
[1, 5, 5, 5, 5, 2]. Whenvalues[m1]equalsvalues[m2]on a flat stretch, you can't tell which side the peak is on. Ternary search needs strict rises and falls. - Using it on data with more than one peak. It finds a peak, not always the highest one.
- Looping
while (low < high)with integerm1andm2. When only two items are left,thirdis0, so nothing moves and the loop never ends. - For real numbers, stopping when
high - lowis "small enough" with a tiny value like1e-12. Rounding errors may keep the gap from ever getting that small. Loop a fixed number of times instead.
When to use it
Look for these clues in a problem:
- The values go up and then down (or down and then up), with exactly one peak or dip.
- The question asks for the maximum or minimum of a smooth function over a range of real numbers.
- Words like "single-peaked", "unimodal", "bitonic", or "first increasing then decreasing".
- You can compute the value at any point, but you can't see the whole curve at once.
Check yourself
Answer each one in your head first, then open it.
Why does ternary search read two values per loop instead of one?
One value alone doesn't say whether you are left or right of the peak. Comparing two values says which way the curve goes between them.
Why does the loop stop when high - low is 2?
With three or fewer items, third would be 0 and the probes would just sit on the range's ends. A short scan over at most three items is simpler and just as fast.
Is ternary search faster than binary search on the slope?
No. Both read two values per loop, but ternary search keeps two thirds of the range while the slope search keeps half. For a million items that is about 31 loops against 20.
Why does a flat stretch break it?
When values[m1] equals values[m2], the peak could be on either side, but the code always drops the right side. In [1, 2, 2, 2, 2, 9, 2] it drops the 9.
Practice problems
Stuck on one? Tuto is in the playground too, and gives you hints, never the answer.
0 of 3 solved. Solve one to earn the bronze medal, half for silver, and all of them for gold.
Start practicingmedium
Go 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 peak's index is somewhere from low to high. It holds at the start, when the range is the whole list. It relies on the list rising strictly to one peak and then falling strictly, with no flat stretches.
Each step keeps it. If values[m1] < values[m2], then m1 can't be at or after the peak, because from the peak onward every value is smaller than the one before, so values[m2] would be smaller. So the peak is after m1, and low = m1 + 1 is safe. If values[m1] >= values[m2], then m2 can't be at or before the peak, because up to the peak every value is bigger than the one before, so values[m2] would be bigger. So high = m2 - 1 is safe.
While more than three items remain, third is at least 1, so low or high moves by at least 2 and the loop ends. The final scan then checks every index still in the range, and by the promise the peak is among them, so the largest of them is the peak.
Going deeper
3 more ideas
Why throwing away a third is safe
Say the values rise strictly to one peak and then fall strictly. If values[m1] < values[m2], then m1 is on the rising side: if it were at or after the peak, everything to its right would be smaller. So the peak is to the right of m1. If values[m1] >= values[m2], the same reasoning shows m2 is on the falling side, so the peak is to the left of m2. Either way the peak stays inside low to high.
The version for real numbers
For a function f(x) on a range of real numbers, use m1 = low + (high - low) / 3 and m2 = high - (high - low) / 3 with no rounding, and set low = m1 or high = m2. Each step keeps two thirds of the range, so after 100 steps the range is (2/3)^100 of its first size, far smaller than any rounding error. Then return f((low + high) / 2). To find a lowest point instead, flip the comparison.
How long it takes, and a faster cousin
Each step keeps two thirds of the range, so the number of steps is about log base 1.5 of n, which is still O(log n). On a list of whole-number positions, binary search on the slope is a bit faster: compare values[mid] with values[mid + 1]. If the next one is bigger, the peak is to the right. Ternary search shines on real-number functions, where there is no "next item".
Work it by hand
Every step in a table, with why
Find the peak of a list of 12 numbers that rises to 31 and then falls.
values = [2, 5, 9, 14, 20, 26, 31, 27, 18, 11, 6, 3]
| Step | low | high | Indexes read | Values | What happens | Why |
|---|---|---|---|---|---|---|
| 1 | 0 | 11 | m1 = 3, m2 = 8 | 14, 18 | 14 < 18, so low = 4 | third is 3. The curve is still rising after index 3, so the peak is to its right. Indexes 0 to 3 are dropped. |
| 2 | 4 | 11 | m1 = 6, m2 = 9 | 31, 11 | 31 >= 11, so high = 8 | third is 2. The value at m2 is lower, so the curve is falling there. Indexes 9 to 11 are dropped. |
| 3 | 4 | 8 | m1 = 5, m2 = 7 | 26, 27 | 26 < 27, so low = 6 | third is 1. Since 26 < 27, the peak is after index 5. Only indexes 6 to 8 are left. |
| 4 | 6 | 8 | i = 7 | 27 vs 31 | Not bigger: best stays 6 | high - low is 2, so the loop ends. The scan starts with best = 6 and compares each later index with it. |
| 5 | 6 | 8 | i = 8 | 18 vs 31 | Not bigger: best stays 6 | The last index in the range is smaller too, so the scan ends. |
It returns 6, the index of 31, after 3 loops and 2 scan steps. On 12 items that saves little, but on a million items the loop runs at most about 31 times.
Compared with other ways
When to pick something else
| Approach | Time | Extra memory | Choose it when |
|---|---|---|---|
| Ternary search | O(log n) | O(1) | The data is a function of real numbers with one peak or valley, where there is no "next item" to compare with. |
| Binary search on the slope | O(log n) | O(1) | The data is a list with whole-number positions. It needs fewer reads than ternary search. |
| Linear scan | O(n) | O(1) | The list is small, has flat parts, or may have more than one peak. It is always correct. |
Extra memory: O(1): low, high, third, m1, m2, and best.
How the steps grow
- Ternary search, Binary search on the slope
O(log n) - Linear scan
O(n)
| Approach | n = 10 | n = 1,000 | n = 1,000,000 |
|---|---|---|---|
| Ternary search | 3 | 10 | 20 |
| Binary search on the slope | 3 | 10 | 20 |
| Linear scan | 10 | 1,000 | 1 million |
Inputs that need care
5 tricky inputs
- One, two, or three items
high - lowis at most2, so the loop never runs and the scan picks the largest. An empty list returns0, which isn't a real index, so checkvalues.lengthfirst if empty input is possible.- The peak at an end:
[40, 31, 22, 9] - The list only falls. The comparison says
values[m1] >= values[m2],highmoves left, and the scan returns0. A list that only rises returns its last index the same way. - A flat stretch:
[1, 2, 2, 2, 2, 9, 2] - It returns
1, a2, not the9at index 5. Withvalues[m1]equal tovalues[m2], the code can't tell which side the peak is on and drops the wrong side. Use a full scan when flats are possible. - Two peaks:
[5, 20, 4, 1, 7, 12, 3] - It returns
5, the12, but the highest value is the20at index 1. Ternary search is only correct when there is exactly one peak. - A real-number function, like the distance from the mouse to points on a line
- There are no indexes, so loop a fixed number of times, such as 100, and use
m1 = low + (high - low) / 3without rounding. Stopping at a tiny gap like1e-12can loop forever because of rounding.
Variations and follow-ups
Twists interviewers add
Binary search on the slope
For whole-number indexes, compare values[mid] with values[mid + 1]. If the next one is bigger, the peak is to the right, so low = mid + 1; otherwise high = mid. That halves the range per loop instead of keeping two thirds: about 20 loops for a million items, against about 31.
Golden-section search
Place the two probes at about 38.2% and 61.8% of the range. After each step, one old probe lands exactly where a new probe is needed, so each loop computes only one new value instead of two. This matters when each value is expensive to compute, like a full layout pass.
Lowest point of a valley
For data that falls and then rises, like cost against batch size, flip the test: if values[m1] > values[m2], the lowest point is after m1. Then keep the smallest value in the final scan.
In an interview
Spot it, explain it, answer follow-ups
- Spot it from "strictly increasing then strictly decreasing", "bitonic", "unimodal", "mountain", or "minimize a cost that falls and then rises" over a range of real numbers.
- Say the rule out loud: "if the left probe is lower, the curve is still climbing after it, so the peak is to its right". Then say that it needs strict rises and falls, and what breaks on flat parts.
- On an integer array, offer binary search on the slope as the better choice and say why: one comparison of neighbors halves the range. Keep ternary search for real-number functions.
- The usual follow-up is "what if there might be several peaks?". Answer: ternary search can return a lower peak, as in
[5, 20, 4, 1, 7, 12, 3]. Binary search on the slope still finds some local peak inO(log n), but finding the highest of several needs a full scan.
Where frontend code uses it
The same idea in real UI code
- Snapping in a design tool: the distance from the cursor to points along a straight segment falls and then rises, so ternary search finds the closest point on the segment.
- Tuning a single setting by measurement, such as an image quality setting that balances file size and visual score, when the score rises and then falls.
- Finding the busiest moment in a traffic chart that has one clear peak, such as a launch-day spike.