Maximum in a Bitonic Array is a clean lesson in modified binary search. The array is not sorted, yet we can still throw away half of it each step — because its shape tells us which direction the peak lies.
Problem. A bitonic array first strictly increases, then strictly decreases. Given such an
array a, return its maximum value (the single peak). Aim for O(log n) time.
Example: a = [1, 3, 8, 12, 4, 2] → answer 12 (it rises 1 → 3 → 8 → 12, then falls 12 → 4 → 2).
The slow way first
The obvious idea: scan the whole array and track the largest value. That works and is simple, but it is O(n) — it touches every element, ignoring the structure we were handed.
The question to ask: while I am standing on one element, what does my immediate surroundings tell me? If the next element is bigger, I am still climbing and the peak is ahead of me. If the next element is smaller, I have already crested and the peak is behind me (or right here). That single comparison points me toward the peak — exactly what binary search needs.
The idea: let the slope steer you
Keep a window [lo, hi]. Look at the middle mid and compare it with its right neighbour a[mid + 1]:
- If
a[mid] < a[mid + 1], we are on the ascending slope. The peak is strictly to the right, solo = mid + 1. - Otherwise
a[mid] > a[mid + 1], we are on the descending slope. The peak ismiditself or to the left, sohi = mid(we keepmid).
Each step halves the window. When lo and hi meet, they sit on the peak.
The key insight: because the array has exactly one peak, the slope at mid is never ambiguous, and the rising side always contains the peak. That is what makes the halving valid.
Walk through it
Step through the animation. The window [lo, hi] shrinks as discarded cells dim out. At mid = 2 the value 8 is below its neighbour 12, so we are climbing and jump right. At mid = 4 the value 4 is above its neighbour 2, so we are falling and pull hi left. One more compare and lo meets hi on 12 — the peak.
Pseudocode
lo, hi = 0, last index
while lo < hi:
mid = (lo + hi) // 2
if a[mid] < a[mid + 1]: # rising slope
lo = mid + 1 # peak is strictly to the right
else: # falling slope
hi = mid # peak is here or to the left (keep mid)
return a[lo] # lo == hi sits on the peakThe Python solution
def find_max(a):
lo, hi = 0, len(a) - 1
while lo < hi:
mid = (lo + hi) // 2
if a[mid] < a[mid + 1]:
lo = mid + 1
else:
hi = mid
return a[lo]loandhibound the part of the array that might still hold the peak.- The loop runs while
lo < hi(not<=): once they are equal the window is a single cell and we are done. mid = (lo + hi) // 2rounds down, somid + 1is always a valid index inside the window — the right-neighbour read never runs off the end.- Line 5 is the heart of it:
a[mid] < a[mid + 1]asks am I still climbing? If yes, the peak is strictly right, solo = mid + 1. - Otherwise we keep
midas a candidate withhi = mid— nevermid - 1, or we could skip the peak.
Complexity
| Case | Time | Notes |
|---|---|---|
| Linear scan | O(n) (moderate) | checks every element |
| Binary search (this solution) | O(log n) (fast) | halves the window each step |
O(1) (fast)We use only a couple of index variables, so the extra space is O(1). Halving the window turns the O(n) scan into O(log n) — for a million elements that is about twenty comparisons instead of a million.
When this pattern shows up
Binary search is not only for sorted arrays. Whenever a local comparison (this element vs its neighbour) reliably tells you which half to keep, you can binary-search. Peak-finding, "find any peak," and search in a rotated array all use this which-side-is-promising move.
Two off-by-one traps. Use while lo < hi and hi = mid (not mid - 1) so you never discard the peak;
and only read a[mid + 1] while lo < hi, which guarantees mid + 1 is in range.
Practice
For a = [1, 3, 8, 12, 4, 2], at mid = 4 we compare a[4] = 4 with a[5] = 2. Which slope is this, and how does the window change?
1. What do we compare at each middle to decide which way to go?
2. On the descending slope we set hi = mid instead of hi = mid - 1. Why?
3. Why is the loop condition while lo < hi rather than lo <= hi?
4. What is the time complexity of this approach?