19 min read

Search in Rotated Sorted Array in C++

Search in Rotated Sorted Array in C++ is one of the most classic and frequently tested algorithmic problems in technical interviews at top tier technology companies like Google, Meta, Amazon, and Microsoft. Given an array of unique integers sorted in ascending order that has been rotated at an unknown pivot index k, the objective is to find the index of a given target value in O(log N) runtime complexity, or return -1 if the target does not exist. In this deep dive, you will learn the complete algorithmic progression: starting from an intuitive Brute Force linear search, evolving through a Two-Pass binary search approach, and ultimately engineering the Optimal Single-Pass Modified Binary Search. We will also explore CPU branch prediction mechanics, dry-run state traces, mathematical complexity proofs, and a 10,000,000-element performance benchmark lab using clear code, outputs, and β€œTry it yourself” challenges after each section.

πŸ”Ή What is a Rotated Sorted Array? Problem Invariants & Mental Model

To master this problem, we must understand what happens mathematically when a sorted array is rotated. Suppose we start with a strictly increasing sorted array of distinct integers:

Original Array: [0, 1, 2, 4, 5, 6, 7]

If we rotate the array to the right by k = 4 positions (or pivot at index 4), the elements shift cyclically:

Rotated Array:  [4, 5, 6, 7, 0, 1, 2]
Index:           0  1  2  3  4  5  6

Notice what happened to the monotonic curve of the array. Instead of a single continuous slope, the array is now partitioned into two strictly increasing continuous subarrays separated by a sharp drop (inflection point / pivot):

Values
  8 ─          ╭── [7] (Peak)
  6 ─       ╭──╯
  4 ─ ╭─────╯               (Inflection Drop)
  2 ─ β”‚                        ╭─── [2]
  0 ─ β”Ό                        β”‚ ╭─ [1]
    └─┴────┴────┴────┴────┴────┴─┴───────
      0    1    2    3    4    5    6    (Indices)
      [--- Segment 1 ---] [ Segment 2 ]
  • Segment 1 (Left Sorted Subarray): [4, 5, 6, 7] (all elements are strictly greater than or equal to nums[0]).
  • Segment 2 (Right Sorted Subarray): [0, 1, 2] (all elements are strictly less than nums[0]).
  • Global Pivot Property: Every element in Segment 1 is strictly greater than every element in Segment 2 because the original array contained unique sorted numbers.
  • Rotated Array I Invariant: All elements in the array are strictly unique. (If duplicates were allowed, as in Rotated Sorted Array II, we could have cases like [1, 0, 1, 1, 1] where nums[low] == nums[mid] == nums[high], destroying deterministic logarithmic search).

Try it yourself

  • What happens if the array is rotated by k = 0 or k = N positions? Does the array remain a standard, unrotated sorted array?
  • Identify the pivot index (minimum element) in the array [6, 7, 8, 1, 2, 3, 4, 5].

πŸ”Ή Approach 1: Brute Force (Linear Scan)

Intuition & Reasoning: When faced with an unsorted or partially scrambled array, the most direct, naive solution is to iterate through the array sequentially from index 0 to N – 1 and compare each element with the target. If a match is found, return the index; if the loop completes without finding the target, return -1.

#include <iostream>
#include <vector>

using namespace std;

// Brute Force: Sequential scan across the array
int search_brute_force(const vector<int>& nums, int target) {
    for (int i = 0; i < static_cast<int>(nums.size()); ++i) {
        if (nums[i] == target) {
            return i; // Target found
        }
    }
    return -1; // Target not present
}

int main() {
    vector<int> nums = {4, 5, 6, 7, 0, 1, 2};
    
    cout << "--- Brute Force Linear Scan ---\n";
    cout << "Index of target 0: " << search_brute_force(nums, 0) << " (Expected: 4)\n";
    cout << "Index of target 3: " << search_brute_force(nums, 3) << " (Expected: -1)\n";
    return 0;
}

Output

--- Brute Force Linear Scan ---
Index of target 0: 4 (Expected: 4)
Index of target 3: -1 (Expected: -1)

Takeaway: The linear scan correctly identifies the target in N steps. However, it completely ignores the sorted nature of the underlying segments.

Step by Step Dry Run (Target = 0, Array = [4, 5, 6, 7, 0, 1, 2]):

β”Œβ”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Step  β”‚ Index (i) β”‚ Value nums[i]β”‚ Condition (nums[i] == 0)     β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 1     β”‚ 0         β”‚ 4            β”‚ 4 == 0 (False)               β”‚
β”‚ 2     β”‚ 1         β”‚ 5            β”‚ 5 == 0 (False)               β”‚
β”‚ 3     β”‚ 2         β”‚ 6            β”‚ 6 == 0 (False)               β”‚
β”‚ 4     β”‚ 3         β”‚ 7            β”‚ 7 == 0 (False)               β”‚
β”‚ 5     β”‚ 4         β”‚ 0            β”‚ 0 == 0 (True) -> Return 4    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Complexity Analysis & The Bottleneck:

  • Time Complexity (TC): O(N): In the worst-case scenario (target is at the last index or does not exist), the algorithm must inspect all N elements sequentially.
  • Space Complexity (SC): O(1): Uses only a single integer loop counter with zero auxiliary heap allocations.
  • The Bottleneck: For an array of size 10,000,000 (such as an in-memory database index or time-series ring buffer), linear search requires millions of iterations and L1/L2 cache loads. An O(N) lookup in a sorted/rotated structure wastes the structural guarantee that each half of the array is already sorted.

Try it yourself

  • If the input array contains 1,000,000 elements, how many comparisons does Linear Search perform in the worst case vs Binary Search? (Hint: 1,000,000 vs ~20).
  • Can early stopping (e.g. stopping when nums[i] > target) work reliably on a rotated array? Why or why not?

πŸ”Ή Approach 2: Better (Two-Pass Binary Search: Find Pivot + Subarray Search)

Intuition & Reasoning: Since a rotated sorted array consists of two individually sorted subarrays (Segment 1 and Segment 2), we can decompose the search into two logical steps:

  • Step 1 (Find the Pivot / Minimum Element): Use Binary Search in O(log N) to find the index of the smallest element (the inflection point k where rotation occurred).
  • Step 2 (Target Subarray Identification): Compare target with nums[pivot] and nums[N - 1]:
    • If target >= nums[pivot] && target <= nums[N - 1], the target must lie in the right sorted segment [pivot, N - 1].
    • Otherwise, the target must lie in the left sorted segment [0, pivot - 1].
  • Step 3 (Standard Binary Search): Execute standard std::binary_search or custom binary search on that specific sorted segment in O(log N).
#include <iostream>
#include <vector>

using namespace std;

// Step 1: Find the index of the minimum element (the pivot) in O(log N)
int find_pivot_index(const vector<int>& nums) {
    int low = 0;
    int high = static_cast<int>(nums.size()) - 1;

    while (low < high) {
        int mid = low + (high - low) / 2;

        if (nums[mid] > nums[high]) {
            // Inflection point must be to the right of mid
            low = mid + 1;
        } else {
            // Inflection point is at mid or to the left of mid
            high = mid;
        }
    }
    return low; // Index of smallest element
}

// Step 2: Standard classic binary search on a strictly sorted range [low, high]
int standard_binary_search(const vector<int>& nums, int low, int high, int target) {
    while (low <= high) {
        int mid = low + (high - low) / 2;
        if (nums[mid] == target) {
            return mid;
        } else if (nums[mid] < target) {
            low = mid + 1;
        } else {
            high = mid - 1;
        }
    }
    return -1;
}

// Complete Two-Pass Algorithm
int search_two_pass(const vector<int>& nums, int target) {
    if (nums.empty()) return -1;

    int n = static_cast<int>(nums.size());
    int pivot = find_pivot_index(nums);

    // Determine which sorted subarray to search
    if (target >= nums[pivot] && target <= nums[n - 1]) {
        return standard_binary_search(nums, pivot, n - 1, target);
    } else {
        return standard_binary_search(nums, 0, pivot - 1, target);
    }
}

int main() {
    vector<int> nums = {4, 5, 6, 7, 0, 1, 2};

    cout << "--- Two-Pass Binary Search ---\n";
    int pivot = find_pivot_index(nums);
    cout << "Array Minimum (Pivot) Index: " << pivot << " (Value: " << nums[pivot] << ")\n";
    cout << "Index of target 0: " << search_two_pass(nums, 0) << " (Expected: 4)\n";
    cout << "Index of target 6: " << search_two_pass(nums, 6) << " (Expected: 2)\n";
    cout << "Index of target 3: " << search_two_pass(nums, 3) << " (Expected: -1)\n";
    return 0;
}

Output

--- Two-Pass Binary Search ---
Array Minimum (Pivot) Index: 4 (Value: 0)
Index of target 0: 4 (Expected: 4)
Index of target 6: 2 (Expected: 2)
Index of target 3: -1 (Expected: -1)

Takeaway: Two-Pass Binary Search reduces time complexity from O(N) to O(log N) by finding the pivot index first and then executing binary search on the appropriate segment.

Step by Step Trace of Pivot Discovery on [4, 5, 6, 7, 0, 1, 2]:

Initial: low = 0, high = 6

Iteration 1:
- mid = 0 + (6 - 0)/2 = 3 (nums[3] = 7)
- nums[mid] (7) > nums[high] (2) -> Minimum is to the right.
- low = mid + 1 = 4

Iteration 2:
- low = 4, high = 6
- mid = 4 + (6 - 4)/2 = 5 (nums[5] = 1)
- nums[mid] (1) <= nums[high] (2) -> Minimum is at mid or left.
- high = mid = 5

Iteration 3:
- low = 4, high = 5
- mid = 4 + (5 - 4)/2 = 4 (nums[4] = 0)
- nums[mid] (0) <= nums[high] (1) -> high = mid = 4

Termination: low == high == 4 -> Pivot Index = 4 (Value = 0).

Complexity Analysis & Limitations:

  • Time Complexity (TC): O(log N): Consists of two consecutive logarithmic passes:
    • Pass 1 (Find Pivot): O(log N) comparisons.
    • Pass 2 (Binary Search Subarray): O(log N) comparisons.
    • Total TC = O(log N) + O(log N) = O(log N).
  • Space Complexity (SC): O(1): Modifies no input memory and requires only three integer index variables.
  • Remaining Limitation: While theoretically O(log N), this approach requires two separate while loops and extra boundary logic to identify which segment to search. In high frequency coding interviews and low-latency systems, we can eliminate the first pass entirely by making boundary decisions dynamically in a Single Pass.

Try it yourself

  • Trace what find_pivot_index returns if the array is already sorted and not rotated at all (e.g. [1, 2, 3, 4, 5]).
  • Why does find_pivot_index use while (low < high) with high = mid instead of while (low <= high) with high = mid - 1?

πŸ”Ή Approach 3: Optimal (Single-Pass Modified Binary Search)

The Core Algorithmic Insight: In standard binary search, we divide the search space in half by evaluating nums[mid]. In a rotated sorted array, whenever we pick any arbitrary index mid, at least one half (either [low..mid] or [mid..high]) is GUARANTEED to be strictly sorted.

Why is this true? Because the inflection point (pivot) can only exist in one of the two halves. The other half is completely unaffected by the rotation and remains monotonically increasing!

Case 1: Left Half [low..mid] is Sorted (nums[low] <= nums[mid])
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚       [low ............ mid]         β”‚        [mid + 1 .......... high]     β”‚
β”‚       Strictly Increasing            β”‚        Contains Inflection Point     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
If target is in range [nums[low], nums[mid]):  -> Search Left:  high = mid - 1
Else:                                          -> Search Right: low  = mid + 1

Case 2: Right Half [mid..high] is Sorted (nums[low] > nums[mid])
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚       [low ............ mid - 1]     β”‚        [mid .............. high]     β”‚
β”‚       Contains Inflection Point      β”‚        Strictly Increasing           β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
If target is in range (nums[mid], nums[high]]: -> Search Right: low  = mid + 1
Else:                                          -> Search Left:  high = mid - 1

By checking whether nums[low] <= nums[mid], we immediately identify which half is sorted. Once we know the sorted half, checking if the target lies within that half is a simple boundary check: target >= nums[low] && target < nums[mid]. This allows us to discard half of the search space in every step in a single pass!

#include <iostream>
#include <vector>

using namespace std;

// Optimal: Single-Pass Modified Binary Search
int search_optimal(const vector<int>& nums, int target) {
    int low = 0;
    int high = static_cast<int>(nums.size()) - 1;

    while (low <= high) {
        // Prevent signed integer overflow compared to (low + high) / 2
        int mid = low + (high - low) / 2;

        // 1. Direct Hit
        if (nums[mid] == target) {
            return mid;
        }

        // 2. Determine which half is strictly sorted
        if (nums[low] <= nums[mid]) {
            // LEFT half [low..mid] is strictly sorted
            if (target >= nums[low] && target < nums[mid]) {
                // Target lies strictly within the left sorted range
                high = mid - 1;
            } else {
                // Target must be in the right half
                low = mid + 1;
            }
        } else {
            // RIGHT half [mid..high] is strictly sorted
            if (target > nums[mid] && target <= nums[high]) {
                // Target lies strictly within the right sorted range
                low = mid + 1;
            } else {
                // Target must be in the left half
                high = mid - 1;
            }
        }
    }

    return -1; // Target does not exist
}

int main() {
    vector<int> nums = {4, 5, 6, 7, 0, 1, 2};

    cout << "--- Optimal Single-Pass Binary Search ---\n";
    cout << "Search 0: Index = " << search_optimal(nums, 0) << " (Expected: 4)\n";
    cout << "Search 4: Index = " << search_optimal(nums, 4) << " (Expected: 0)\n";
    cout << "Search 2: Index = " << search_optimal(nums, 2) << " (Expected: 6)\n";
    cout << "Search 3: Index = " << search_optimal(nums, 3) << " (Expected: -1)\n";
    return 0;
}

Output

--- Optimal Single-Pass Binary Search ---
Search 0: Index = 4 (Expected: 4)
Search 4: Index = 0 (Expected: 0)
Search 2: Index = 6 (Expected: 6)
Search 3: Index = -1 (Expected: -1)

Takeaway: The single-pass binary search resolves target lookups in half the instructions of the two-pass algorithm while maintaining strict O(log N) time complexity.

Step by Step State Transition Trace (Array = [4, 5, 6, 7, 0, 1, 2], Target = 0):

β”Œβ”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Pass β”‚ low β”‚ high β”‚ mid β”‚ nums[mid] β”‚ Sorted Half Check    β”‚ Target Range Check     β”‚ Next Actionβ”‚
β”œβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 1    β”‚ 0   β”‚ 6    β”‚ 3   β”‚ 7         β”‚ nums[0](4) <= 7 (L)  β”‚ 0 in [4, 7)? (False)   β”‚ low = 4    β”‚
β”‚ 2    β”‚ 4   β”‚ 6    β”‚ 5   β”‚ 1         β”‚ nums[4](0) <= 1 (L)  β”‚ 0 in [0, 1)? (True)    β”‚ high = 4   β”‚
β”‚ 3    β”‚ 4   β”‚ 4    β”‚ 4   β”‚ 0         β”‚ nums[4] == 0 (HIT!)  β”‚ Target Found!          β”‚ Return 4   β”‚
β””β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Complexity Analysis (Proof of Optimum):

  • Time Complexity (TC): O(log N): At every iteration, the search interval [low, high] is reduced by exactly half ((high - low + 1) / 2). For an array of size N, the maximum number of iterations before low > high is ⌊logβ‚‚ NβŒ‹ + 1.
    • For N = 1,000,000 elements: maximum 20 iterations.
    • For N = 1,000,000,000 elements: maximum 30 iterations.
  • Space Complexity (SC): O(1): Operates completely in-place using three stack-allocated primitive integers (low, high, mid). Zero heap allocations.

Try it yourself

  • Trace the execution when target = 5 in [4, 5, 6, 7, 0, 1, 2]. How many iterations does it take to hit index 1?
  • What happens if the target is 8 (larger than any element in the array)? Trace the values of low and high when the loop terminates.

πŸ”Ή Hardware Mechanics: CPU Branch Prediction & Cache Locality in Binary Search

From a systems engineering perspective, understanding how modern CPUs execute search algorithms reveals why algorithmic time complexity doesn’t always tell the whole story on small inputs:

1. Cache Locality: Linear Scan vs Binary Search

When the CPU executes a Linear Search on a contiguous std::vector<int>, the hardware L1/L2 Stream Prefetcher detects the sequential access pattern. It automatically loads subsequent 64-byte cache lines into the L1 data cache ahead of time. For small arrays (e.g. N < 64 elements), linear search often runs faster than binary search because all elements reside in a single L1 cache line!

In contrast, Binary Search jumps across widely separated indices (e.g. index 5,000,000 → 2,500,000 → 1,250,000). These non-contiguous jumps cause L1/L2 Cache Misses until the search range narrows down to fit within a single cache line.

2. Branch Prediction Penalties

In our optimal binary search loop, the CPU encounters two primary conditional branches: nums[low] <= nums[mid] and target >= nums[low] && target < nums[mid]. Modern superscalar CPUs speculatively execute instructions down predicted branch paths. If the branch predictor mispredicts which half to search, the CPU must flush its entire instruction pipeline, incurring a 15 to 20 CPU cycle penalty.

Try it yourself

  • Why does low + (high - low) / 2 generate identical machine code to (low + high) / 2 when low + high does not overflow, but protects against undefined behavior in C++?
  • Can you rewrite the mid calculation using bitwise shifts as low + ((high - low) >> 1)? (Remember operator precedence: + has higher precedence than >>!).

πŸ”Ή Lab: High Throughput Performance Benchmark (10,000,000 Elements)

Let’s conduct a rigorous benchmark comparing our three search strategies on an array containing 10,000,000 unique integers rotated at a random index. We will perform 100,000 target lookups and measure total execution time, average nanoseconds per query, and throughput.

#include <iostream>
#include <vector>
#include <chrono>
#include <random>
#include <iomanip>
#include <algorithm>

using namespace std;

// 1. Brute Force
int search_linear(const vector<int>& nums, int target) {
    for (int i = 0; i < static_cast<int>(nums.size()); ++i) {
        if (nums[i] == target) return i;
    }
    return -1;
}

// 2. Two-Pass Binary Search
int find_pivot(const vector<int>& nums) {
    int low = 0, high = static_cast<int>(nums.size()) - 1;
    while (low < high) {
        int mid = low + (high - low) / 2;
        if (nums[mid] > nums[high]) low = mid + 1;
        else high = mid;
    }
    return low;
}

int binary_search_range(const vector<int>& nums, int low, int high, int target) {
    while (low <= high) {
        int mid = low + (high - low) / 2;
        if (nums[mid] == target) return mid;
        if (nums[mid] < target) low = mid + 1;
        else high = mid - 1;
    }
    return -1;
}

int search_two_pass_benchmark(const vector<int>& nums, int target) {
    int n = static_cast<int>(nums.size());
    int pivot = find_pivot(nums);
    if (target >= nums[pivot] && target <= nums[n - 1]) {
        return binary_search_range(nums, pivot, n - 1, target);
    }
    return binary_search_range(nums, 0, pivot - 1, target);
}

// 3. Optimal Single-Pass Binary Search
int search_single_pass(const vector<int>& nums, int target) {
    int low = 0, high = static_cast<int>(nums.size()) - 1;
    while (low <= high) {
        int mid = low + (high - low) / 2;
        if (nums[mid] == target) return mid;

        if (nums[low] <= nums[mid]) {
            if (target >= nums[low] && target < nums[mid]) high = mid - 1;
            else low = mid + 1;
        } else {
            if (target > nums[mid] && target <= nums[high]) low = mid + 1;
            else high = mid - 1;
        }
    }
    return -1;
}

int main() {
    constexpr size_t N = 10000000; // 10 Million elements
    constexpr int NUM_QUERIES = 100000;

    cout << "--- CppMonk Rotated Array Search Benchmark Lab ---\n";
    cout << "Generating sorted array with " << N << " elements...\n";

    vector<int> nums(N);
    for (size_t i = 0; i < N; ++i) {
        nums[i] = static_cast<int>(i * 2); // Even numbers: 0, 2, 4, 6...
    }

    // Rotate array at pivot index 3,750,000
    size_t pivot_k = 3750000;
    rotate(nums.begin(), nums.begin() + pivot_k, nums.end());
    cout << "Array rotated at index: " << pivot_k << "\n\n";

    // Prepare random target queries
    mt19937_64 rng(42);
    uniform_int_distribution<int> dist(0, static_cast<int>(N * 2));
    vector<int> queries(NUM_QUERIES);
    for (int i = 0; i < NUM_QUERIES; ++i) {
        queries[i] = dist(rng);
    }

    // Benchmark 1: Two-Pass Binary Search
    {
        auto start = chrono::high_resolution_clock::now();
        int found_count = 0;
        for (int q : queries) {
            if (search_two_pass_benchmark(nums, q) != -1) ++found_count;
        }
        auto end = chrono::high_resolution_clock::now();
        double ms = chrono::duration_cast<chrono::microseconds>(end - start).count() / 1000.0;
        double ns_per_op = (ms * 1000000.0) / NUM_QUERIES;
        double throughput = (NUM_QUERIES * 1000.0) / ms;

        cout << fixed << setprecision(2);
        cout << "1. Two-Pass Binary Search:\n";
        cout << "   Duration:       " << ms << " ms (" << NUM_QUERIES << " queries)\n";
        cout << "   Avg Latency:    " << ns_per_op << " ns / query\n";
        cout << "   Throughput:     " << (throughput / 1000000.0) << " Million queries/sec\n\n";
    }

    // Benchmark 2: Single-Pass Optimal Binary Search
    {
        auto start = chrono::high_resolution_clock::now();
        int found_count = 0;
        for (int q : queries) {
            if (search_single_pass(nums, q) != -1) ++found_count;
        }
        auto end = chrono::high_resolution_clock::now();
        double ms = chrono::duration_cast<chrono::microseconds>(end - start).count() / 1000.0;
        double ns_per_op = (ms * 1000000.0) / NUM_QUERIES;
        double throughput = (NUM_QUERIES * 1000.0) / ms;

        cout << "2. Single-Pass Optimal Binary Search:\n";
        cout << "   Duration:       " << ms << " ms (" << NUM_QUERIES << " queries)\n";
        cout << "   Avg Latency:    " << ns_per_op << " ns / query\n";
        cout << "   Throughput:     " << (throughput / 1000000.0) << " Million queries/sec\n";
    }

    return 0;
}

Output

--- CppMonk Rotated Array Search Benchmark Lab ---
Generating sorted array with 10000000 elements...
Array rotated at index: 3750000

1. Two-Pass Binary Search:
   Duration:       6.84 ms (100000 queries)
   Avg Latency:    68.40 ns / query
   Throughput:     14.62 Million queries/sec

2. Single-Pass Optimal Binary Search:
   Duration:       3.48 ms (100000 queries)
   Avg Latency:    34.80 ns / query
   Throughput:     28.74 Million queries/sec

Takeaway: Single-Pass Optimal Binary Search delivers a 1.96x speedup over Two-Pass Binary Search, processing over 28.7 Million queries per second at an average latency of just 34.8 nanoseconds across an array of 10,000,000 elements!

Try it yourself

  • Run the benchmark with -O3 compiler optimization and observe how the compiler vectorizes index calculations and eliminates redundant bounds checking.
  • Why does Single-Pass run almost twice as fast as Two-Pass? (Hint: Two-Pass performs two full logarithmic traversals down to leaf indices, while Single-Pass performs only one).

πŸ”Ή Key Pitfalls & Common Interview Traps

  • Trap 1: Using (low + high) / 2 instead of low + (high - low) / 2: In C++, if low + high exceeds INT_MAX (2,147,483,647), signed integer overflow results in undefined behavior (often wrapping into a negative index and triggering a segmentation fault). Always write low + (high - low) / 2.
  • Trap 2: Using Strict < Instead of <= when checking sorted halves: Writing if (nums[low] < nums[mid]) fails when low == mid (such as in a 2-element array [3, 1]). When low == mid, the single element at low is trivially sorted, so we MUST check if (nums[low] <= nums[mid]).
  • Trap 3: Incorrect Boundary Inclusions in Target Range Checks: When checking if target lies in the sorted left half, the lower bound is inclusive and the upper bound is exclusive (since nums[mid] == target was already checked): target >= nums[low] && target < nums[mid]. Missing the = on nums[low] will cause the algorithm to skip the target when target == nums[low].
  • Trap 4: Confusing Rotated Array I with Rotated Array II (Duplicates): If duplicate elements exist (e.g. [1, 0, 1, 1, 1]), then nums[low] == nums[mid] == nums[high]. In that scenario, it is impossible to determine which half is sorted in O(1), and the algorithm must shrink the search boundary with ++low; --high;, degrading worst-case runtime to O(N). For unique elements (Rotated Array I), runtime is strictly guaranteed to be O(log N).

Try it yourself

  • Write a test case on a 2-element array [3, 1] searching for 1. Step through the code and verify that nums[low] <= nums[mid] correctly routes the search to the right half.
  • Construct an array with duplicates where nums[low] == nums[mid] == nums[high] and explain why standard binary search gets stuck.

πŸ”Ή Comparison of All Approaches

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Approach                            β”‚ Time Complexity  β”‚ Space Complexity β”‚ Key Characteristics    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 1. Brute Force (Linear Scan)        β”‚ O(N)             β”‚ O(1)             β”‚ Naive, ignores order   β”‚
β”‚ 2. Two-Pass Binary Search           β”‚ O(log N)         β”‚ O(1)             β”‚ 2 full binary passes   β”‚
β”‚ 3. Optimal Single-Pass Search       β”‚ O(log N)         β”‚ O(1)             β”‚ 1 pass, 28M ops/sec    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ”Ή FAQs on Search in Rotated Sorted Array in C++

Q1: What is the primary invariant that enables O(log N) search in a rotated sorted array?
In any rotated sorted array of distinct values, picking any arbitrary midpoint index guarantees that at least one half (either [low..mid] or [mid..high]) is strictly sorted and monotonically increasing.

Q2: Why does the presence of duplicate elements degrade the algorithm to O(N)?
When duplicates exist (Rotated Array II), we can have nums[low] == nums[mid] == nums[high] (e.g. [2, 2, 2, 0, 2]). In this case, we cannot tell which half contains the inflection point and must linearly shrink the search space via ++low and --high, yielding O(N) worst-case time.

Q3: How do we find the minimum element in a rotated sorted array?
We use a binary search where we compare nums[mid] with nums[high]. If nums[mid] > nums[high], the minimum element is to the right (low = mid + 1); otherwise, it is at or to the left of mid (high = mid). The search terminates when low == high.

Q4: Why do we use low + (high – low) / 2 instead of (low + high) / 2?
In C++, if low + high exceeds INT_MAX (231 – 1), it triggers signed integer arithmetic overflow, causing undefined behavior and negative index crashes. low + (high - low) / 2 avoids overflow entirely.

Q5: Can we use std::lower_bound or std::binary_search directly on a rotated array?
No. The C++ standard library algorithms (std::lower_bound, std::upper_bound, std::binary_search) require the entire range to be partitioned with respect to the search comparator. A rotated array violates global monotonic ordering, so standard STL binary search algorithms produce undefined results unless applied specifically to an isolated sorted segment.

Q6: How many rotations make the array return to its original sorted order?
Rotating an array of size N by N positions (or any integer multiple of N) returns the array to its original, unrotated sorted sequence.

Q7: What is the maximum number of comparisons needed for N = 1,000,000 elements?
In Single-Pass Binary Search, the search space is halved on each step. logβ‚‚(1,000,000) β‰ˆ 19.93, meaning the algorithm will find the target or conclude it is missing in at most 20 comparisons.

Q8: What are common real world use cases for rotated sorted arrays?
Rotated sorted arrays model cyclic memory buffers (ring buffers), round-robin task dispatchers, circular time-series metrics storage, and distributed hash table (DHT) key lookups in peer-to-peer networks.

πŸ”Ή Wrapping Up

Search in Rotated Sorted Array in C++ is a foundational problem that tests your ability to adapt binary search to non-trivial piecewise monotonic structures. By identifying the core invariant (that at least one half of the array is always strictly sorted), we eliminate the need for finding the pivot first and solve the problem in a single elegant O(log N) pass with O(1) memory overhead. To continue mastering binary search and low-level algorithms, explore our guides on Google DSA Sheet, Vector in C++, and STL in C++. Practice the β€œTry it yourself” exercises above to make these patterns second nature!

Leave a Comment