How do you search a bitonic array — samples that rise then fall — for a target in O(log n)?
answer
- not sorted, but built from sorted runs
- find the seam before you search
- the hump has exactly one local maximum
- two searches, opposite comparison directions
- sequential passes add, they do not nest
basics
~20 sBitonic search runs in two stages: a slope-comparison binary search locates the single apex, then two ordinary binary searches cover the rising side ascending and the falling side descending. Three logarithmic passes in sequence are still O(log n).
solid answer
~50 sA bitonic series — a rocket's velocity profile, rising to a single apex and then falling — is not sorted, but it is the *composition* of two sorted runs, so solve it by decomposition. First find the apex with the same slope comparison used for peak finding: `a[mid] < a[mid+1]` means the apex is to the right, otherwise it is at `mid` or to the left. Because a strictly bitonic profile has exactly one local maximum, "any peak" and "the maximum" coincide here — that is the property doing the work. Then run a standard ascending binary search on `[0, apex]` and a descending one on `[apex, n-1]`, flipping the comparison in the second. The accounting is 3 × O(log n) = O(log n): the passes run in sequence, not nested, so they add rather than multiply.
go deeper
Recall that a single-humped series is two sorted runs joined at an apex, and that a plain binary search over the whole thing is wrong because the ordering reverses partway.
Walk the three stages, flip the comparison on the descending side without being reminded, and add the costs correctly to O(log n) rather than multiplying them.
Bring up the preconditions unprompted: flat runs destroy the halving argument, non-bitonic input is silently mishandled, and validating the shape per query costs more than it saves.
The generalisable call is decomposition — a series made of a few known monotone runs can be searched logarithmically by finding the seams first. Decide where that guarantee is enforced: at ingest, in the type, or nowhere.
## What bitonic means A series is bitonic when it increases up to some index and decreases from there on — a single hump. A rocket's velocity samples from ignition through burnout and into drag-limited descent are the archetype. Either monotone piece may be empty, so a purely rising or purely falling series counts as a degenerate bitonic one, and the apex sits at an end. The array is emphatically **not** sorted; a plain binary search for a value will happily walk off toward the wrong slope and report a miss on data that contains the target. ## Stage one: locate the apex Use the slope comparison from peak finding, unchanged: ``` lo = 0 hi = length(a) - 1 while lo < hi: mid = floor((lo + hi) / 2) if a[mid] < a[mid + 1]: lo = mid + 1 else: hi = mid apex = lo ``` This is O(log n). The subtle point worth saying out loud in an interview: peak finding promises only *some* local maximum, which is normally a weaker guarantee than "the maximum". On a strictly bitonic profile the two coincide, because such a profile has exactly one local maximum by construction. The bitonic precondition is what upgrades the answer, not the algorithm. ## Stage two: two directional searches The rising side `[0, apex]` is sorted ascending and the falling side `[apex, n-1]` is sorted descending. Run one ordinary binary search on each, with the comparison flipped in the descending one: there, a probe value greater than the target means the target lies to the *right*, not the left. Return the first hit. Including `apex` in both ranges is harmless; excluding it from both is the off-by-one that makes the apex value itself unfindable, and it is the bug this decomposition most often ships with. ## The cost accounting Three logarithmic passes run one after another, so the costs **add**: O(log n) + O(log n) + O(log n) = O(log n). The frequent wrong answer is O(log² n), which would be right if the searches were *nested* — one running inside each step of another — and they are not. Space is O(1) beyond the input in iterative form. If all you need is the maximum value rather than membership of a target, stage one alone answers it and the other two passes are dead work. ## Degenerate and hostile inputs - **Monotone input.** If the samples only rise, the apex is the last index and the descending side is empty. The implementation must tolerate an empty range rather than assuming both sides are non-trivial. - **Equal adjacent samples.** A flat run — a plateau at the apex, or a held velocity during a coast phase — breaks the strictness the decomposition relies on. The probe `a[mid] < a[mid+1]` reads level and steers left, but level is genuinely ambiguous: the apex could be on either side. In the worst case you lose the halving argument and slide toward an O(n) scan. This is the same failure mode that duplicates inflict on other almost-sorted binary searches, and the honest interview answer is to state the precondition ("strictly bitonic, no equal adjacent readings") rather than to pretend the log bound survives. - **Input that is not bitonic at all.** Nothing in the method detects that. Fed a multi-humped profile it returns a confident wrong answer, so validating or guaranteeing the shape upstream is part of using it. Verifying bitonicity costs O(n), which defeats the purpose if you do it per query — do it once at ingest, or derive it from the physics of how the data is produced. ## Why this is worth knowing beyond the trivia The reusable idea is **decomposition into monotone pieces**: when a series is not sorted but is built from a small, known number of sorted runs, find the seams in logarithmic time and then search each run normally. The bitonic case is the two-run instance. Recognising it stops candidates from either giving up on logarithmic search entirely or, worse, running a plain binary search on data whose precondition it silently violates.
- Why is the total O(log n) rather than O(log squared n)?Because the three passes run in sequence, not nested. Costs multiply only when one search executes inside every step of another; here the apex search finishes, then each side search runs once. Three logarithmic terms add to a constant times log n, which is O(log n).
- The profile holds a constant value across its apex. What breaks?The slope probe reads level and cannot tell which side the true turning point is on, so the halving argument loses its justification and the worst case degrades toward O(n). State strict bitonicity — no equal adjacent samples — as a precondition rather than claiming the logarithmic bound regardless.
- How would you handle a profile that is bitonic but you cannot guarantee it?The method cannot detect a violation; it returns a confident wrong answer on multi-humped data. Verify the shape once at ingest for O(n), or derive the guarantee from how the data is generated, and treat per-query verification as pointless since it costs more than the linear scan it replaces.
A road that climbs one side of a pass and drops the other: find the summit first, then you have two ordinary one-way roads to walk with a map.
saying these in an interview costs you the question
- Runs a plain binary search on the whole bitonic array
- Says the total cost is O(log squared n)
- Forgets to flip the comparison on the falling side
- Assumes both monotone sides are non-empty
- Claims the log bound survives flat runs at the apex