Search in an Almost-Sorted Array takes the cleanest algorithm in the book — binary search — and breaks one assumption: the array is nearly sorted, but any element may have swapped places with one of its neighbors. The fix is a tiny one, and it teaches you to never trust a[mid] alone.
Problem. You are given an array a that is sorted except that each element may have been
swapped with an immediate neighbor (so a[i] is at most one position away from where a fully sorted
array would put it). Given a value x, return its index, or -1 if it is absent.
Example: a = [10, 3, 40, 20, 50, 80, 70], x = 40 → answer 2 (the 40 slid one slot left when it
swapped with the 20).
The slow way first
You could just scan every cell — O(n). That always works, but it throws away the structure we were
handed. The array is almost sorted, so binary search should still be on the table; we only need to
account for the wobble.
The question to ask: if a value can sit one position off, what does landing on mid actually tell me?
On its own, a[mid] could be the target's neighbor instead of the target. So I should look a little
wider before I commit to throwing away half the array.
The idea: peek a three-cell window
Run a normal binary search, but at each step inspect a window of three cells — a[mid - 1],
a[mid], and a[mid + 1] — before deciding where to go. If any of the three equals x, return that
index. Otherwise compare x against a[mid] to pick a side, and jump by two (lo = mid + 2 or
hi = mid - 2) so we skip the cells we already checked and never re-examine them.
Jumping by two is the crucial detail. We already know the answer is not in any of the three checked
cells, so moving to mid + 1 (the usual binary-search step) would re-test a cell we just ruled out.
Skipping to mid + 2 keeps the search O(log n) and avoids an infinite loop on tiny ranges.
Walk through it
Step through the animation. lo and hi start at the two ends and mid lands on index 3, value 20.
That is not 40, so we peek the neighbors: a[mid - 1] is 40 — the value that slid left when it swapped
with the 20. We return mid - 1 = 2 without ever discarding the half that secretly held our answer.
Pseudocode
lo, hi = 0, n - 1
while lo <= hi:
mid = (lo + hi) // 2
if a[mid] == x: return mid
if mid > lo and a[mid-1] == x: return mid - 1 # value slid left
if mid < hi and a[mid+1] == x: return mid + 1 # value slid right
if x < a[mid]: hi = mid - 2 # jump past the window
else: lo = mid + 2
return -1 # not presentThe Python solution
def search_almost_sorted(a, x):
lo, hi = 0, len(a) - 1
while lo <= hi:
mid = (lo + hi) // 2
if a[mid] == x:
return mid
if mid > lo and a[mid - 1] == x:
return mid - 1
if mid < hi and a[mid + 1] == x:
return mid + 1
if x < a[mid]:
hi = mid - 2
else:
lo = mid + 2
return -1loandhibound the part of the array still worth searching, exactly as in plain binary search.mid = (lo + hi) // 2is the center of the current window.- The first check is the ordinary hit on
a[mid]. - The next two checks are the swap-safe peeks:
mid > loandmid < higuard against reading outside the current window, then we test the left and right neighbor for the target. x < a[mid]chooses a direction, and we movehiorloby two, not one, because the three cells in the window are already ruled out.
Complexity
| Case | Time | Notes |
|---|---|---|
| Linear scan | O(n) (moderate) | ignores the structure |
| Windowed binary search | O(log n) (fast) | 3 comparisons per step, jump by 2 |
O(1) (fast)We still halve the search space every step, so the runtime stays O(log n); the only change is a
constant amount of extra peeking. No extra memory is needed — the pointers do all the work.
When this pattern shows up
Whenever an array is sorted with a small, bounded amount of noise — a single rotation, one swapped pair, a few out-of-place elements — binary search usually still applies. The move is to widen what you inspect at each step (a small window) and tighten how you advance (jump past what you checked) so the logarithmic bound survives.
Two easy bugs: advancing by one instead of two re-tests a cell and can loop forever on a
two-element range, and peeking a[mid - 1] or a[mid + 1] without the mid > lo / mid < hi guard
reads outside the current window and can return a stale or out-of-range index.
Practice
At mid = 3 the value is 20, not the target 40. Why do we not immediately go right (lo = mid + 1) the way plain binary search would?
1. Why do we inspect three cells (mid-1, mid, mid+1) at each step?
2. Why do we move lo or hi by two instead of one?
3. What is the time complexity of this approach?
4. Why guard the neighbor peeks with mid > lo and mid < hi?