Overview
A rotated sorted array results from taking a sorted array and rotating it around some pivot point. For example, the sorted array [0, 1, 2, 4, 5, 6, 7] rotated at index 4 becomes [4, 5, 6, 7, 0, 1, 2]. The rotation operation preserves the sorted order within each segment but creates a discontinuity at the rotation point.
The search problem requires finding a target element in such an array. The challenge lies in maintaining logarithmic time complexity despite the rotation disrupting the standard binary search assumption of complete sorted order. A naive linear scan achieves O(n) time, but modified binary search approaches can achieve O(log n) by identifying which portion of the array remains sorted at each step.
The problem appears frequently in technical interviews and has practical applications in circular buffers, log rotation systems, and scenarios where sorted data undergoes periodic reorganization. The rotated array structure also models real-world situations like scheduling systems that wrap around (days of the week, hours in a day) or ring buffers in networking.
# Standard sorted array
sorted = [1, 2, 3, 4, 5, 6, 7]
# Same array rotated at index 4
rotated = [5, 6, 7, 1, 2, 3, 4]
# The rotation preserves sorted segments
# Left segment: [5, 6, 7] - sorted
# Right segment: [1, 2, 3, 4] - sorted
The key insight involves recognizing that at least one half of any subarray in a rotated sorted array remains sorted. Binary search divides the array in half, and examining the middle element reveals which half maintains sorted order. This sorted half can then be checked using standard binary search logic, while the unsorted half requires recursive application of the same strategy.
Key Principles
Binary Search Foundation: The algorithm builds on binary search but adapts the comparison logic. Standard binary search assumes complete sorted order, using middle element comparison to eliminate half the search space. Rotated arrays require additional checks to determine which half remains sorted before applying elimination logic.
Sorted Segment Identification: At any division point, comparing the leftmost, middle, and rightmost elements reveals which segment maintains sorted order. If array[left] <= array[mid], the left half is sorted. Otherwise, the right half must be sorted. This invariant holds because rotation creates exactly one discontinuity point.
Two-Phase Comparison: After identifying the sorted segment, the algorithm performs two checks:
- Determine if the target falls within the sorted segment's range
- If yes, search that segment; if no, search the opposite segment
This strategy ensures each iteration eliminates half the remaining elements, maintaining O(log n) complexity.
Duplicate Handling Complexity: Arrays with duplicate elements complicate the sorted segment identification. When array[left] == array[mid] == array[right], the algorithm cannot determine which half is sorted without additional checks. This degrades worst-case performance to O(n) for arrays with many duplicates.
# Identifying sorted segments
def find_sorted_segment(arr, left, right, mid)
# Left segment sorted
if arr[left] <= arr[mid]
return :left
# Right segment sorted
elsif arr[mid] <= arr[right]
return :right
end
end
arr = [4, 5, 6, 7, 0, 1, 2]
left, right = 0, 6
mid = (left + right) / 2 # => 3
# arr[0] = 4, arr[3] = 7, arr[6] = 2
# Since 4 <= 7, left segment [4,5,6,7] is sorted
Target Range Determination: Once the sorted segment is identified, check if the target value falls within its range. For a sorted left segment, verify array[left] <= target <= array[mid]. For a sorted right segment, verify array[mid] <= target <= array[right]. Range checks use standard comparison operators without special rotation logic.
Rotation Point Properties: The rotation point (minimum element) marks where the discontinuity occurs. Elements before this point belong to the higher sorted segment, and elements after belong to the lower sorted segment. Finding the rotation point itself is a related problem that uses similar binary search modifications.
Single Element Edge Case: Arrays with one element trivially solve the search problem. Arrays with two elements require special handling to avoid infinite loops when calculating the midpoint.
Ruby Implementation
Ruby's array indexing and comparison operators make implementing rotated array search straightforward. The standard approach uses iterative binary search with custom comparison logic.
def search_rotated(nums, target)
left = 0
right = nums.length - 1
while left <= right
mid = (left + right) / 2
# Found target
return mid if nums[mid] == target
# Determine which segment is sorted
if nums[left] <= nums[mid]
# Left segment is sorted
if nums[left] <= target && target < nums[mid]
# Target is in sorted left segment
right = mid - 1
else
# Target is in unsorted right segment
left = mid + 1
end
else
# Right segment is sorted
if nums[mid] < target && target <= nums[right]
# Target is in sorted right segment
left = mid + 1
else
# Target is in unsorted left segment
right = mid - 1
end
end
end
-1 # Target not found
end
# Example usage
arr = [4, 5, 6, 7, 0, 1, 2]
search_rotated(arr, 0) # => 4
search_rotated(arr, 3) # => -1
Ruby's division operator performs integer division automatically, eliminating the need for explicit floor operations when calculating the midpoint. The <= comparisons handle arrays with duplicate elements at boundaries, though this doesn't fully solve the duplicate problem.
For recursive implementations, Ruby's method call overhead is higher than iteration, but the code expresses the divide-and-conquer logic more explicitly:
def search_rotated_recursive(nums, target, left = 0, right = nums.length - 1)
return -1 if left > right
mid = (left + right) / 2
return mid if nums[mid] == target
if nums[left] <= nums[mid]
# Left segment sorted
if nums[left] <= target && target < nums[mid]
search_rotated_recursive(nums, target, left, mid - 1)
else
search_rotated_recursive(nums, target, mid + 1, right)
end
else
# Right segment sorted
if nums[mid] < target && target <= nums[right]
search_rotated_recursive(nums, target, mid + 1, right)
else
search_rotated_recursive(nums, target, left, mid - 1)
end
end
end
Handling duplicates requires additional logic to skip over equal elements when the sorted segment cannot be determined:
def search_rotated_with_duplicates(nums, target)
left = 0
right = nums.length - 1
while left <= right
mid = (left + right) / 2
return mid if nums[mid] == target
# Cannot determine sorted segment
if nums[left] == nums[mid] && nums[mid] == nums[right]
left += 1
right -= 1
next
end
if nums[left] <= nums[mid]
if nums[left] <= target && target < nums[mid]
right = mid - 1
else
left = mid + 1
end
else
if nums[mid] < target && target <= nums[right]
left = mid + 1
else
right = mid - 1
end
end
end
-1
end
# Example with duplicates
arr = [2, 5, 6, 0, 0, 1, 2]
search_rotated_with_duplicates(arr, 0) # => 3 or 4
A functional approach using Ruby's enumeration methods sacrifices performance but provides clarity:
def search_rotated_functional(nums, target)
nums.each_with_index do |num, idx|
return idx if num == target
end
-1
end
This linear search serves as a fallback for edge cases or when the array is too small to benefit from binary search overhead.
Ruby arrays support negative indexing, but rotated array algorithms use standard positive indices to maintain consistency with the mathematical formulation. Converting between representations adds unnecessary complexity.
Performance Considerations
Time Complexity: The modified binary search achieves O(log n) time complexity for arrays without duplicates. Each iteration eliminates half the remaining search space by identifying which segment is sorted and whether the target falls within it. This matches standard binary search performance despite the rotation.
Arrays with duplicates degrade to O(n) worst case when all elements are identical except the target. The algorithm must increment left and decrement right pointers linearly when unable to determine which segment is sorted, eliminating the logarithmic guarantee.
require 'benchmark'
def generate_rotated_array(size, rotation_point)
sorted = (1..size).to_a
sorted.rotate(rotation_point)
end
# Benchmark comparison
Benchmark.bm(20) do |x|
arr_1000 = generate_rotated_array(1000, 500)
arr_10000 = generate_rotated_array(10000, 5000)
arr_100000 = generate_rotated_array(100000, 50000)
x.report("Binary (n=1000):") do
1000.times { search_rotated(arr_1000, -1) }
end
x.report("Binary (n=10000):") do
1000.times { search_rotated(arr_10000, -1) }
end
x.report("Binary (n=100000):") do
1000.times { search_rotated(arr_100000, -1) }
end
end
Space Complexity: The iterative implementation uses O(1) additional space, storing only pointer variables. The recursive implementation requires O(log n) stack space for the call stack, matching the recursion depth. For large arrays, iteration avoids stack overflow risks.
Comparison Count Analysis: Each iteration performs 3-4 comparisons: one to check if target is found, one to determine which segment is sorted, and 1-2 to check if target falls within the sorted segment's range. This constant factor is slightly higher than standard binary search but maintains the same asymptotic complexity.
Cache Performance: Rotated array search exhibits poor cache locality compared to sequential scans. The binary search pattern jumps between distant array indices, causing cache misses. For small arrays that fit in L1 cache, linear search may outperform binary search due to better spatial locality.
# Linear search competitive for small arrays
def linear_search(nums, target)
nums.index(target) || -1
end
# Crossover point testing
small_arr = generate_rotated_array(20, 10)
Benchmark.bm(15) do |x|
x.report("Binary small:") do
10000.times { search_rotated(small_arr, -1) }
end
x.report("Linear small:") do
10000.times { linear_search(small_arr, -1) }
end
end
Branch Prediction: Modern CPUs struggle with the conditional branches in rotated array search because the pattern of which segment is sorted varies unpredictably based on the rotation point and target location. This unpredictability stalls the CPU pipeline, adding latency to each comparison.
Early Termination: Finding the target at the midpoint on the first check provides best-case O(1) performance. This occurs when the rotation point happens to place the target at the array's center. Average case still performs O(log n) comparisons.
Practical Examples
Finding Configuration Values: A configuration system stores timestamped settings in a circular buffer. As new configurations arrive, they overwrite old entries, creating a rotated sorted structure. Searching for a configuration by timestamp requires the rotated search algorithm.
class ConfigurationRing
def initialize(capacity)
@buffer = Array.new(capacity)
@timestamps = Array.new(capacity)
@write_pos = 0
@size = 0
end
def add_config(timestamp, config)
@timestamps[@write_pos] = timestamp
@buffer[@write_pos] = config
@write_pos = (@write_pos + 1) % @buffer.length
@size = [@size + 1, @buffer.length].min
end
def find_by_timestamp(target_ts)
# Timestamps form rotated sorted array
idx = search_rotated_timestamps(@timestamps, target_ts)
idx == -1 ? nil : @buffer[idx]
end
private
def search_rotated_timestamps(timestamps, target)
left, right = 0, @size - 1
while left <= right
mid = (left + right) / 2
return mid if timestamps[mid] == target
if timestamps[left] <= timestamps[mid]
if timestamps[left] <= target && target < timestamps[mid]
right = mid - 1
else
left = mid + 1
end
else
if timestamps[mid] < target && target <= timestamps[right]
left = mid + 1
else
right = mid - 1
end
end
end
-1
end
end
# Usage
ring = ConfigurationRing.new(10)
ring.add_config(100, {setting: 'a'})
ring.add_config(105, {setting: 'b'})
ring.add_config(110, {setting: 'c'})
ring.find_by_timestamp(105) # => {setting: 'b'}
Log File Analysis: A log rotation system maintains sorted log entries across multiple rotated files. Finding a specific log entry by timestamp requires searching through the rotated sequence.
class LogRotationSearch
def initialize(log_files)
# Each file contains sorted timestamps
@files = log_files
@boundaries = extract_boundaries(log_files)
end
def find_log_entry(timestamp)
# Boundaries may be rotated if files wrapped around
file_idx = find_file_with_timestamp(timestamp)
return nil unless file_idx
search_within_file(@files[file_idx], timestamp)
end
private
def find_file_with_timestamp(timestamp)
# @boundaries is rotated sorted array of [min, max] ranges
left, right = 0, @boundaries.length - 1
while left <= right
mid = (left + right) / 2
min_ts, max_ts = @boundaries[mid]
return mid if timestamp >= min_ts && timestamp <= max_ts
# Determine which segment is sorted
if @boundaries[left][0] <= @boundaries[mid][0]
# Left sorted
if @boundaries[left][0] <= timestamp && timestamp < min_ts
right = mid - 1
else
left = mid + 1
end
else
# Right sorted
if max_ts < timestamp && timestamp <= @boundaries[right][1]
left = mid + 1
else
right = mid - 1
end
end
end
nil
end
def extract_boundaries(files)
files.map { |f| [f.first_timestamp, f.last_timestamp] }
end
def search_within_file(file, timestamp)
# Standard binary search within single sorted file
file.entries.bsearch { |entry| entry.timestamp >= timestamp }
end
end
Scheduling System: A shift scheduling application maintains worker assignments in sorted order by shift start time. When the schedule wraps around midnight, it forms a rotated array. Finding the worker scheduled for a specific time uses rotated search.
class ShiftScheduler
Shift = Struct.new(:start_time, :worker_id)
def initialize
@shifts = []
end
def add_shift(hour, worker_id)
# Hours 0-23, may wrap around midnight
@shifts << Shift.new(hour, worker_id)
end
def rotate_schedule(hours)
@shifts.rotate!(hours)
end
def find_worker_at_time(hour)
idx = search_shift_by_hour(hour)
idx == -1 ? nil : @shifts[idx].worker_id
end
private
def search_shift_by_hour(target_hour)
left, right = 0, @shifts.length - 1
while left <= right
mid = (left + right) / 2
mid_hour = @shifts[mid].start_time
return mid if mid_hour == target_hour
left_hour = @shifts[left].start_time
right_hour = @shifts[right].start_time
if left_hour <= mid_hour
# Left sorted
if left_hour <= target_hour && target_hour < mid_hour
right = mid - 1
else
left = mid + 1
end
else
# Right sorted
if mid_hour < target_hour && target_hour <= right_hour
left = mid + 1
else
right = mid - 1
end
end
end
-1
end
end
# Usage
scheduler = ShiftScheduler.new
(0..23).each { |hour| scheduler.add_shift(hour, "worker_#{hour}") }
scheduler.rotate_schedule(6) # Rotate to start at 6am
scheduler.find_worker_at_time(8) # => "worker_8"
Priority Queue with Rotation: A specialized priority queue implementation uses a rotated sorted array to maintain elements. Rotation occurs when the queue wraps around after dequeuing operations.
class RotatingPriorityQueue
def initialize(capacity)
@capacity = capacity
@items = []
@priorities = []
@head = 0
end
def enqueue(item, priority)
insert_position = find_insert_position(priority)
@priorities.insert(insert_position, priority)
@items.insert(insert_position, item)
end
def dequeue
return nil if @items.empty?
item = @items.delete_at(@head)
@priorities.delete_at(@head)
@head = (@head + 1) % [@items.length, @capacity].min if @items.any?
item
end
def contains_priority?(priority)
idx = search_rotated(@priorities, priority)
idx != -1
end
private
def find_insert_position(priority)
# Binary search for insertion point in rotated array
return 0 if @priorities.empty?
left, right = 0, @priorities.length - 1
result = @priorities.length
while left <= right
mid = (left + right) / 2
if @priorities[left] <= @priorities[mid]
if priority < @priorities[mid]
result = mid
right = mid - 1
else
left = mid + 1
end
else
if priority > @priorities[mid]
left = mid + 1
else
result = mid
right = mid - 1
end
end
end
result
end
def search_rotated(arr, target)
# Standard rotated array search
left, right = 0, arr.length - 1
while left <= right
mid = (left + right) / 2
return mid if arr[mid] == target
if arr[left] <= arr[mid]
if arr[left] <= target && target < arr[mid]
right = mid - 1
else
left = mid + 1
end
else
if arr[mid] < target && target <= arr[right]
left = mid + 1
else
right = mid - 1
end
end
end
-1
end
end
Common Patterns
Two-Pointer Segment Identification: The most common pattern maintains left and right pointers while identifying which half remains sorted. After calculating the midpoint, compare array[left] with array[mid] to determine segment order. This pattern handles any rotation point location.
def identify_sorted_segment_pattern(nums)
left, right = 0, nums.length - 1
results = []
while left <= right
mid = (left + right) / 2
if nums[left] <= nums[mid]
results << {segment: :left, range: [left, mid], sorted: true}
results << {segment: :right, range: [mid + 1, right], sorted: false}
else
results << {segment: :left, range: [left, mid], sorted: false}
results << {segment: :right, range: [mid + 1, right], sorted: true}
end
break # Demo single iteration
end
results
end
Range Boundary Checking: After identifying the sorted segment, verify whether the target falls within its value range. This pattern combines position-based (index) and value-based (array element) comparisons.
def range_check_pattern(nums, target, left, mid, right)
if nums[left] <= nums[mid]
# Left segment sorted
in_left_range = nums[left] <= target && target < nums[mid]
{
sorted_segment: :left,
target_in_sorted: in_left_range,
action: in_left_range ? "search left" : "search right"
}
else
# Right segment sorted
in_right_range = nums[mid] < target && target <= nums[right]
{
sorted_segment: :right,
target_in_sorted: in_right_range,
action: in_right_range ? "search right" : "search left"
}
end
end
Iterative vs Recursive Patterns: Iterative implementations use a while loop with explicit pointer updates. Recursive implementations pass updated bounds as parameters. Both achieve the same logic but differ in space complexity and stack management.
# Iterative pattern
def iterative_pattern(nums, target)
left, right = 0, nums.length - 1
while left <= right
mid = (left + right) / 2
return mid if nums[mid] == target
# Update pointers based on logic
if condition
right = mid - 1
else
left = mid + 1
end
end
-1
end
# Recursive pattern
def recursive_pattern(nums, target, left = 0, right = nums.length - 1)
return -1 if left > right
mid = (left + right) / 2
return mid if nums[mid] == target
# Recurse with updated bounds
condition ?
recursive_pattern(nums, target, left, mid - 1) :
recursive_pattern(nums, target, mid + 1, right)
end
Minimum Finding Pattern: Finding the rotation point (minimum element) uses similar logic but searches for the discontinuity rather than a specific target. This pattern checks if nums[mid] > nums[right] to determine which side contains the minimum.
def find_rotation_point(nums)
left, right = 0, nums.length - 1
while left < right
mid = (left + right) / 2
# Minimum is in right half
if nums[mid] > nums[right]
left = mid + 1
else
# Minimum is in left half or is mid
right = mid
end
end
left # Index of minimum element (rotation point)
end
arr = [4, 5, 6, 7, 0, 1, 2]
rotation_point = find_rotation_point(arr)
# => 4 (index of element 0)
Duplicate Squeezing Pattern: When duplicates prevent segment identification, incrementally move both pointers inward. This pattern sacrifices logarithmic performance in worst case but handles all duplicate configurations.
def duplicate_squeeze_pattern(nums, target)
left, right = 0, nums.length - 1
while left <= right
mid = (left + right) / 2
return mid if nums[mid] == target
# Cannot determine sorted segment
if nums[left] == nums[mid] && nums[mid] == nums[right]
# Squeeze from both ends
left += 1
right -= 1
next
end
# Continue with normal logic
# ...
end
-1
end
Binary Search Combination Pattern: Some problems require first finding the rotation point, then performing standard binary search on the correct segment. This pattern trades a single complex search for two simpler searches.
def two_phase_search(nums, target)
# Phase 1: Find rotation point
rotation = find_rotation_point(nums)
# Phase 2: Standard binary search on correct segment
if target >= nums[rotation] && target <= nums[-1]
# Search right segment
nums[rotation..-1].bsearch_index { |x| x >= target }&.then { |i| i + rotation }
else
# Search left segment
nums[0...rotation].bsearch_index { |x| x >= target }
end || -1
end
Reference
Algorithm Comparison
| Approach | Time Complexity | Space Complexity | Handles Duplicates | Notes |
|---|---|---|---|---|
| Modified Binary Search | O(log n) | O(1) iterative, O(log n) recursive | No | Standard solution, fails with duplicates |
| Binary Search with Squeeze | O(n) worst case | O(1) | Yes | Degrades to linear with many duplicates |
| Two-Phase Search | O(log n) | O(1) | No | Find rotation point first, then standard search |
| Linear Scan | O(n) | O(1) | Yes | Fallback for small arrays or edge cases |
Key Decision Points
| Condition | Left Segment State | Right Segment State | Action |
|---|---|---|---|
| nums[left] <= nums[mid] | Sorted | Unsorted | Check if target in left range |
| nums[left] > nums[mid] | Unsorted | Sorted | Check if target in right range |
| nums[left] == nums[mid] == nums[right] | Unknown | Unknown | Squeeze both pointers inward |
| left >= right | N/A | N/A | Search complete, target not found |
Search Logic Flow
| Sorted Segment | Target Range Check | Next Search Space |
|---|---|---|
| Left | nums[left] <= target < nums[mid] | Left half: [left, mid-1] |
| Left | Target outside left range | Right half: [mid+1, right] |
| Right | nums[mid] < target <= nums[right] | Right half: [mid+1, right] |
| Right | Target outside right range | Left half: [left, mid-1] |
Common Edge Cases
| Case | Array State | Required Handling |
|---|---|---|
| Single element | [x] | Direct comparison |
| Two elements | [x, y] | Prevent infinite loop in midpoint calculation |
| No rotation | [1, 2, 3, 4, 5] | Works as standard binary search |
| Full rotation | Returns to original | Works as standard binary search |
| Target at rotation point | Target is minimum element | Found when mid equals rotation point |
| All duplicates | [5, 5, 5, 5, 5] | Degrades to O(n) linear scan |
Complexity Analysis
| Array Size | Max Iterations (no duplicates) | Max Comparisons per Iteration | Total Comparisons |
|---|---|---|---|
| 10 | 4 | 4 | 16 |
| 100 | 7 | 4 | 28 |
| 1000 | 10 | 4 | 40 |
| 10000 | 14 | 4 | 56 |
Ruby-Specific Methods
| Method | Purpose | Example |
|---|---|---|
| Array#rotate | Create rotated array | [1,2,3].rotate(1) returns [2,3,1] |
| Array#bsearch_index | Standard binary search | [1,2,3,4,5].bsearch_index returns index |
| Array#index | Linear search fallback | [4,5,6,1,2].index(1) returns 3 |
| Integer division / | Calculate midpoint | (0 + 6) / 2 equals 3 |
Rotation Point Properties
| Property | Description | Formula |
|---|---|---|
| Minimum element | Always at rotation point | min(array) |
| Array split | Two sorted subarrays | [rotation..end] + [0...rotation] |
| Discontinuity | Only break in sorted order | nums[i] > nums[i+1] where i is rotation-1 |
| Segment count | Number of sorted portions | 2 if rotated, 1 if not |