Peak Index in a Mountain Array: Binary Search Without a Sorted Array
Solve Peak Index in a Mountain Array in O(log n) with slope-based binary search — intuition, JavaScript code, a full walkthrough, and common mistakes.
This article has an interactive companion
Don't just read it — step through it interactively, one state change at a time.
Most people learn binary search as "the fast way to find a value in a sorted array" — and then freeze the first time an interviewer hands them an array that isn't sorted. Peak Index in a Mountain Array is the problem that breaks that mental block.
A mountain array climbs strictly up, hits one peak, and falls strictly down. It is not sorted. And yet binary search solves it in O(log n), because binary search never needed sortedness — it needs a question whose answer flips exactly once across the array. Learn to spot that flip, and a whole family of problems (rotated arrays, boundary searches, "search on the answer") collapses into one template.
The problem
An array arr is a mountain if it has at least 3 elements, strictly increases to some index i, and strictly decreases after it. Given a mountain array, return the index of the peak — the i where arr[i] is the maximum. The follow-up demands O(log n) time.
Input: arr = [0, 2, 1, 0]
Output: 1 // arr[1] = 2 is the peak
Input: arr = [0, 1, 3, 5, 4, 2]
Output: 3 // climbs 0→1→3→5, then falls 5→4→2Two guarantees in the statement do a lot of quiet work:
- The slopes are strict — no plateaus, no equal neighbors. Every comparison between adjacent elements gives a definitive answer.
- There is exactly one peak, and it is never at index
0orn - 1(both slopes must exist).
The brute force baseline
Walk from the left until the array stops climbing. The first index whose next element is smaller is the peak.
function peakIndexInMountainArray(arr) {
for (let i = 0; i < arr.length - 1; i++) {
if (arr[i] > arr[i + 1]) return i;
}
return arr.length - 1; // unreachable for a valid mountain
}This is O(n), and honestly it passes: 10⁵ elements is nothing for a linear scan. So why bother beating it? Because the follow-up explicitly asks for O(log n), and because this problem exists to test one thing: do you know binary search works on more than sorted arrays? Answering with a linear scan tells the interviewer you don't. The same reasoning applies to arr.indexOf(Math.max(...arr)) — correct, O(n), and it dodges the entire point.
The key insight: binary search the slope, not the value
You are not searching for a value. You are searching for a turning point — the index where the array switches from climbing to falling. So ask a different question at each index:
isDescending(i) = arr[i] > arr[i + 1]Evaluate that predicate across any mountain array and you get a clean pattern:
arr = [0, 1, 3, 5, 4, 2]
isDescending = F F F T T // flips exactly once, at the peakEvery index on the ascent answers false; every index from the peak onward answers true. One flip, no exceptions — the strict slopes guarantee it. The peak is simply the first index where the predicate is true.
That FFFTT shape is the entire requirement for binary search. Check the middle: if arr[mid] < arr[mid + 1], you're still on the ascent, so the peak lies strictly to the right — discard mid and everything left of it. Otherwise you're at or past the peak — discard everything right of mid, but keep mid itself, because it might be the answer. Either way, half the candidates vanish per comparison.
This is the mountain peak binary search pattern, a special case of find-the-first-true — the same skeleton behind lower-bound searches and First Bad Version.
The optimal solution
This is exactly the algorithm the visualizer steps through:
var peakIndexInMountainArray = function(arr) {
let left = 0, right = arr.length - 1;
while (left < right) {
const mid = Math.floor((left + right) / 2);
if (arr[mid] < arr[mid + 1]) left = mid + 1;
else right = mid;
}
return left;
};Three details make this template correct, and each one is load-bearing:
while (left < right), not<=. The loop maintains the invariant the peak is always inside[left, right]. It stops the moment the window shrinks to one index — which must be the peak.left = mid + 1on the ascent.arr[mid] < arr[mid + 1]provesmiditself cannot be the peak (its right neighbor is bigger), so it's safe to exclude it.right = mid, notmid - 1, on the descent.arr[mid] > arr[mid + 1]proves the peak is atmidor earlier — excludingmidhere could throw away the answer.
Notice there's no early-return arr[mid] === target check anywhere. Boundary-style binary search doesn't hunt for a match; it squeezes the window until only the answer remains.
Walkthrough: arr = [0, 1, 3, 5, 4, 2]
| Step | left | right | mid | Compare | Meaning | Move |
|---|---|---|---|---|---|---|
| 1 | 0 | 5 | 2 | arr[2]=3 < arr[3]=5 | still climbing | left = 3 |
| 2 | 3 | 5 | 4 | arr[4]=4 > arr[5]=2 | at or past peak | right = 4 |
| 3 | 3 | 4 | 3 | arr[3]=5 > arr[4]=4 | at or past peak | right = 3 |
| 4 | 3 | 3 | — | left === right | window closed | return 3 |
Three comparisons on six elements, and the window [left, right] never once excluded the true peak. Step 3 is the one worth staring at: mid = 3 is the peak, and right = mid keeps it alive while right = mid - 1 would have destroyed it. You can step through it interactively and watch the window collapse comparison by comparison.
Complexity
| Approach | Time | Space | Why |
|---|---|---|---|
| Linear scan | O(n) | O(1) | checks every index until the slope flips |
| Max + indexOf | O(n) | O(1) | two full passes; ignores the mountain structure |
| Slope binary search | O(log n) | O(1) | halves the candidate window per comparison |
On 10⁵ elements the binary search finishes in ~17 comparisons instead of up to 10⁵. Space is constant either way — just two pointers.
Common mistakes
- Writing
right = mid - 1on the descending branch. Whenmidis the peak itself, the comparisonarr[mid] > arr[mid + 1]is true — andmid - 1skips the answer.right = midis the contract: the descending case only proves the peak is at or beforemid. - Using
while (left <= right)withright = mid. Onceleft === right,mid === leftand the descending branch setsright = mid— nothing changes, and the loop spins forever. The<condition andright = midare a matched pair; don't mix templates. - Fearing
arr[mid + 1]goes out of bounds. It can't. Inside the loopleft < right, somid < right ≤ n - 1, which meansmid + 1 ≤ n - 1. The template protects itself. - Comparing against
arr[mid - 1]instead. Nowmid = 0does go out of bounds, and you need extra edge handling. The forward comparison is the clean one. - Returning
arr[left]instead ofleft. The problem asks for the peak's index, not its value. Easy to fumble after solving value-returning cousins of this problem.
Where this pattern shows up next
Predicate-based binary search — find where a condition flips — is one of the highest-leverage templates in interviews:
- Binary Search — the classic sorted-array version; master its invariants before bending them.
- Guess Number Higher or Lower — the same halving logic driven by an oracle's feedback instead of array reads.
- Sqrt(x) — binary search over an answer range where the predicate is
mid * mid <= x, no array at all. - Search in Rotated Sorted Array — another "not sorted, still binary-searchable" array, where you reason about which half is orderly.
FAQ
Why does binary search work on a mountain array if it isn't sorted?
Because binary search doesn't require a sorted array — it requires a predicate that is monotonic over the index range. In a mountain array, the test arr[i] > arr[i + 1] is false for every index on the ascent and true from the peak onward: it flips exactly once. That single flip lets each comparison discard half the remaining indices with certainty, which is all binary search ever needed. Sortedness is just the most familiar way to get such a predicate.
What is the time complexity of Peak Index in a Mountain Array?
The slope-based binary search runs in O(log n) time and O(1) space: each iteration performs one comparison and halves the window [left, right], so ~17 iterations cover an array of 10⁵ elements. A linear scan or indexOf(Math.max(...arr)) is O(n) — accepted by the judge, but it misses the follow-up requirement and the point of the problem.
Why is it right = mid and not right = mid - 1?
When arr[mid] > arr[mid + 1], you only know you're on the descending slope — which includes the possibility that mid is the peak. Setting right = mid - 1 could discard the answer. By contrast, when arr[mid] < arr[mid + 1], index mid is provably not the peak (its neighbor is bigger), so left = mid + 1 is safe. The asymmetry between the two moves mirrors the asymmetry in what each comparison proves.
Can arr[mid + 1] ever be out of bounds?
No. The loop condition left < right guarantees mid = floor((left + right) / 2) is strictly less than right, and right never exceeds n - 1. Therefore mid + 1 ≤ n - 1 on every iteration. This is a built-in advantage of the left < right template — comparing against arr[mid - 1] instead would need explicit edge handling at index 0.
How is this different from Find Peak Element?
Find Peak Element (LeetCode 162) allows arbitrary arrays with multiple local peaks and only guarantees that neighbors differ; you may return any peak. Peak Index in a Mountain Array guarantees exactly one peak with strictly monotonic slopes on both sides. Remarkably, both use the identical loop — compare arr[mid] with arr[mid + 1] and walk toward the larger side — but the mountain guarantee makes the correctness argument simpler: there is only one turning point the search can converge to.
Make it stick: run this one yourself
Don't just read it — step through it interactively, one state change at a time.