Rotated array search: identify the ordered halfLESSON 2.13 · 13 OF 43 IN CHAPTER
PART A / Coding problems and trade-offs
Step 39 of 252
LESSON 2.13 · 13 OF 43 IN CHAPTERTry it, then open the solution

Rotated array search: identify the ordered half

Constructed practice problem; no company attribution. Prerequisites: binary search boundaries.

Candidate brief

THE PROBLEM

A device stores distinct sorted sequence numbers in a circular array, then exports them starting from an arbitrary position. Find a target in that exported order. How does the midpoint tell you which half still has ordinary sorted order?

Write this:

def rotated_search(nums, target):
    ...
Contract Decision
Input List/tuple that is a rotation of strictly increasing integers, plus integer target
Output Index in the supplied array, or -1 if absent
Boundaries Empty gives -1; unrotated and singleton arrays are valid; no mutation
Invalid input Invalid types, duplicates, or invalid rotation raise ValueError
Excluded Duplicates and concurrent mutation; checked validation costs O(n)
Optional refresher · the underlying tool

Rotation keeps two sorted pieces, even when the whole list looks disordered. At each midpoint, ask which half is known to be sorted before discarding the other:

nums = [4,5,6,1,2,3]
lo, mid, hi = 0, 2, 5
print(nums[lo] <= nums[mid])  # True: left half is sorted

For target 2, the answer is position 4. Clarify duplicate values: if ties prevent identifying a sorted half, the log-time guarantee may vanish.

A design choice worth saying aloud

Keep lo, mid, and hi as indices, never as values; compare nums[lo] and nums[mid] to prove which half is ordered before discarding it. Duplicate endpoints can make that proof inconclusive. If duplicates become allowed, show the ambiguous [1,1,1,0,1] case and qualify the worst-case bound.

What the interviewer expects

The interviewer gives you the scenario and the contract above. Explain what a successful call returns, walk through one example below, and name what your state means before choosing a data structure.

Done means: Index in the supplied array, or -1 if absent.

Now predict each output before looking at the reference; invalid input should leave any existing state unchanged unless the contract says otherwise.

Test-case scenarios to settle before coding

01 · Representative

Input / starting state
[4,5,7,0,1,2], target 1
Expected result
4

What it is testing: One half remains ordered at every step.

02 · Absent

Input / starting state
same array, target 6
Expected result
-1

What it is testing: Absence uses an index sentinel.

03 · Empty

Input / starting state
[], any target
Expected result
-1

What it is testing: No midpoint exists.

04 · Unrotated

Input / starting state
[1,2,3], target 2
Expected result
1

What it is testing: A rotation by zero is valid.

05 · Singleton

Input / starting state
[1], target 1 / 2
Expected result
0 / -1

What it is testing: Both smallest success and failure paths matter.

06 · Invalid/atomic

Input / starting state
duplicates or invalid rotation
Expected result
ValueError; input unchanged

What it is testing: The ordered-half proof relies on the contract.

For each case, show which branch or state change produces that result.

rotated_search([4, 5, 7, 0, 1, 2], 1) == 4; target 6 gives -1. rotated_search([1], 1) == 0; rotated_search([2, 1, 3], 1) raises ValueError. The output refers to the exported order, not the original sorted index.

Before opening the explanation, restate the contract, trace the smallest useful example, implement a baseline, and identify the repeated work. Then implement your improvement independently and derive tests from the contract. Say what your state means before saying which data structure stores it.

Worked lesson, changed requirements, and reference

Reuse ordering without reconstructing the array

A linear scan is a correct O(n) baseline using O(1) auxiliary space. Sorting a copy would lose original indices unless carried along, and it does unnecessary work. In a valid rotated array with distinct values, at most one descending break occurs between adjacent positions. For any active interval, at least one side of its midpoint is sorted. That side has an ordinary numeric range test.

Search [4, 5, 7, 0, 1, 2] for 1 lo / hi mid / value Ordered side and decision
first step 0 / 5 2 / 7 left [4, 5, 7]; 1 outside, go right
second step 3 / 5 4 / 1 equal; return 4

Maintain the invariant that a present target lies inside the inclusive interval [lo, hi]. Check equality first. If nums[lo] <= nums[mid], the left half is sorted. Keep it only when nums[lo] <= target < nums[mid]; otherwise discard it. When the left half is not sorted, the right half is, so retain it exactly when nums[mid] < target <= nums[hi]. Distinctness makes the classification decisive. Discarding the midpoint after failed equality makes the interval shrink. An empty interval proves absence, not merely failure to guess the right pivot.

The core loop is O(log(n+1)) time and O(1) auxiliary space. The public reference validates in O(n), making end-to-end time O(n), with constant auxiliary space. For n > 1, count descending edges including the wraparound edge: a valid strict rotation has exactly one descent and no equal adjacent circular values. Cutting at that descent yields a strictly increasing sequence, proving the validation rule sufficient. An unrotated sequence's descent is its wrap edge.

Predict the ambiguity for midpoint equality. Two arrays can show the same left, middle, and right values while hiding the smaller target on opposite sides.

Array, target 0 Left / middle / right Why classification fails
[1, 0, 1, 1, 1] 1 / 1 / 1 target lies left of middle
[1, 1, 1, 0, 1] 1 / 1 / 1 target lies right of middle

After checking equality, when all three boundary values are equal, discard one element at each end and continue. Correctness survives but worst-case time becomes O(n). Define whether any matching index or the first exported index is required; the original early return does not enforce a first-index tie rule.

Follow-up 2: many queries share an immutable rotation

Diagram: Follow-up 2: many queries share an immutable rotation

Store the pivot and search logical value nums[(p+i) % n]; no sorted copy is necessary if the underlying array remains stable. A senior candidate defends the half-range comparisons and tests every rotation of small arrays. A lead candidate defines immutable snapshot lifetime and rejects claims of logarithmic worst-case search after allowing arbitrary duplicates or unvalidated updates.

Run and check

From the repository root:

cd curriculum/01-code/02-data-structures-algorithms/problems/12-rotated-array-search
python -m unittest -v test_solution.py

Reference implementation (download file, source below) · Contract and oracle tests (download file, source below). Read the tests after your attempt. A green reference suite verifies the supplied implementation; it does not demonstrate independent transfer. Reimplement one follow-up with the reference closed and explain which old invariant no longer holds.

Reference implementation · solution.py
def _integers(values):
    if not isinstance(values, (list, tuple)) or any(type(x) is not int for x in values):
        raise ValueError("expected a list or tuple of integers, excluding bool")

def rotated_search(nums, target):
    _integers(nums)
    if type(target) is not int:
        raise ValueError("target must be an integer")
    n = len(nums)
    if n > 1:
        descents = 0
        for i in range(n):
            current, following = nums[i], nums[(i + 1) % n]
            if current == following:
                raise ValueError("values must be distinct")
            descents += current > following
        if descents != 1:
            raise ValueError("input must be a strict sorted rotation")
    lo, hi = 0, n - 1
    while lo <= hi:
        mid = lo + (hi - lo) // 2
        if nums[mid] == target:
            return mid
        if nums[lo] <= nums[mid]:
            if nums[lo] <= target < nums[mid]:
                hi = mid - 1
            else:
                lo = mid + 1
        elif nums[mid] < target <= nums[hi]:
            lo = mid + 1
        else:
            hi = mid - 1
    return -1
Contract and oracle tests · test_solution.py
import itertools
import unittest
from solution import rotated_search

class RotationTests(unittest.TestCase):
    def test_contract(self):
        self.assertEqual(rotated_search([4, 5, 7, 0, 1, 2], 1), 4)
        self.assertEqual(rotated_search([], 6), -1)
        self.assertEqual(rotated_search([1], 1), 0)
        for nums in [[2, 1, 3], [1, 1], [2, 1, 2], [True], [1, 3, 2, 4]]:
            with self.assertRaises(ValueError):
                rotated_search(nums, 1)

    def test_every_rotation_against_linear_lookup(self):
        for n in range(7):
            for ordered in itertools.combinations(range(-3, 4), n):
                for pivot in range(max(1, n)):
                    nums = ordered[pivot:] + ordered[:pivot]
                    for target in range(-4, 5):
                        expected = nums.index(target) if target in nums else -1
                        self.assertEqual(rotated_search(nums, target), expected)

if __name__ == "__main__":
    unittest.main()