Loading learning content...
The elegant binary search algorithm we developed for rotated arrays with distinct elements has a hidden assumption: every value uniquely identifies its position relative to the rotation point. When we check nums[low] <= nums[mid], we can definitively conclude whether the left half is sorted.
Duplicates shatter this assumption.
Consider the array [1, 1, 1, 1, 1, 2, 1, 1, 1]. When nums[low] = 1, nums[mid] = 1, and nums[high] = 1, we have no information about which half is sorted. The rotation point could be anywhere. This single insight has profound implications for the algorithm's complexity guarantees.
With duplicates, the worst-case time complexity degrades from O(log n) to O(n). This isn't a failure of algorithm design—it's a fundamental limitation. When nums[low] = nums[mid] = nums[high], we cannot eliminate half the array with one comparison. Understanding why this happens and how to handle it is essential for interviews.
By the end of this page, you will understand why duplicates break the O(log n) guarantee, how to modify the algorithm to handle duplicates correctly, and how to explain the complexity trade-offs to an interviewer.
Let's understand precisely why duplicates cause problems. The key check in our algorithm is:
if nums[low] <= nums[mid]: # Left half is sorted
With distinct elements, this check is reliable:
nums[low] <= nums[mid], the elements from low to mid are in ascending ordernums[low] > nums[mid], the rotation point is in the left halfWith duplicates, the check becomes ambiguous:
Consider [2, 2, 2, 3, 1, 2, 2]. Here:
nums[0] = 2, nums[3] = 3, nums[6] = 2nums[0] <= nums[3]? Is 2 <= 3? Yes.[2, 2, 2, 3] is sorted. ✓ Correct!But now consider [1, 1, 1, 1, 1, 2, 1, 1, 1]:
nums[0] = 1, nums[4] = 1, nums[8] = 1nums[0] <= nums[4]? Is 1 <= 1? Yes.WRONG! The left half [1, 1, 1, 1, 1] appears sorted, but the rotation point (value 2, then back to 1) is in the right half. Our check passes, but we can't distinguish this from the case where the entire array is [1, 1, 1, 1, 1, 1, 1, 1, 1] with no rotation.
nums = [1, 1, 1, 1, 1, 2, 1, 1, 1], target = 2low = 0, mid = 4, high = 8
nums[0] = 1, nums[4] = 1, nums[8] = 1
Check: nums[low] <= nums[mid]?
1 <= 1? YES
Conclusion: Left half should be sorted.
But wait! The actual values are:
Left half: [1, 1, 1, 1, 1] - Sorted ✓
Right half: [1, 2, 1, 1, 1] - Contains the rotation!
If we assume left half is sorted and target=2 is not in [1,1],
we'd search right half → Happens to be correct by luck.
Now try: nums = [2, 1, 1, 1, 1, 1, 1, 1, 1], target = 1
mid = 4, nums[0]=2, nums[4]=1
Check: nums[0] <= nums[4]? 2 <= 1? NO
So right half should be sorted...
Right half: [1, 1, 1, 1, 1] - Sorted ✓
Left half: [2, 1, 1, 1] - Contains rotation ✓
Correct again! But...The critical case is when nums[low] = nums[mid] = nums[high]. We genuinely cannot determine which half is sorted.
When nums[low] = nums[mid] = nums[high], there is NO comparison-based way to determine which half contains the rotation point. The only option is to reduce the problem size by a constant factor (typically eliminating just one element at a time), leading to O(n) worst case.
The solution is surprisingly simple: when we encounter the ambiguous case (nums[low] = nums[mid] OR nums[mid] = nums[high]), we can't safely eliminate half the array. Instead, we shrink the search space by just one element.
The strategy:
nums[low] = nums[mid] = nums[high], we cannot determine which half is sortedlow and decrement high (skip the duplicate boundaries)This handles ambiguity at the cost of occasionally making O(1) progress instead of halving.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
def search_with_duplicates(nums: list[int], target: int) -> bool: """ Search for target in a rotated sorted array that MAY contain duplicates. NOTE: Returns bool (existence), not index, because duplicates make index less meaningful and this matches LeetCode's problem formulation. Args: nums: A rotated sorted array possibly with duplicates target: The value to search for Returns: True if target exists in the array, False otherwise Time Complexity: O(log n) average, O(n) worst case Space Complexity: O(1) The worst case happens when most/all elements are the same except one. Example: [1,1,1,1,1,2,1,1,1] - we may need to check almost every element. """ if not nums: return False low, high = 0, len(nums) - 1 while low <= high: mid = low + (high - low) // 2 # Found the target if nums[mid] == target: return True # CRITICAL: Handle the ambiguous case # When nums[low] = nums[mid] = nums[high], we cannot determine # which half is sorted. We must shrink the search space manually. if nums[low] == nums[mid] == nums[high]: low += 1 high -= 1 continue # Re-evaluate with smaller range # Now we can apply the standard logic # At least one of the inequalities must be strict if nums[low] <= nums[mid]: # Left half is sorted if nums[low] <= target < nums[mid]: high = mid - 1 # Target in sorted left half else: low = mid + 1 # Target in right half else: # Right half is sorted if nums[mid] < target <= nums[high]: low = mid + 1 # Target in sorted right half else: high = mid - 1 # Target in left half return False def search_with_duplicates_verbose(nums: list[int], target: int) -> bool: """Same algorithm with detailed tracing.""" if not nums: return False low, high = 0, len(nums) - 1 iteration = 0 print(f"Searching for {target} in {nums}") print("=" * 70) while low <= high: mid = low + (high - low) // 2 iteration += 1 print(f"\nIteration {iteration}: low={low}, mid={mid}, high={high}") print(f" Values: nums[{low}]={nums[low]}, nums[{mid}]={nums[mid]}, nums[{high}]={nums[high]}") if nums[mid] == target: print(f" ✓ FOUND! Target {target} at index {mid}") return True if nums[low] == nums[mid] == nums[high]: print(f" ⚠️ AMBIGUOUS: All boundary values = {nums[low]}") print(f" Cannot determine sorted half, shrinking by 1 each side") low += 1 high -= 1 continue if nums[low] <= nums[mid]: print(f" Left half sorted: [{nums[low]}...{nums[mid]}]") if nums[low] <= target < nums[mid]: print(f" Target in sorted left half") high = mid - 1 else: print(f" Target NOT in sorted left half") low = mid + 1 else: print(f" Right half sorted: [{nums[mid]}...{nums[high]}]") if nums[mid] < target <= nums[high]: print(f" Target in sorted right half") low = mid + 1 else: print(f" Target NOT in sorted right half") high = mid - 1 print(f"\n✗ NOT FOUND after {iteration} iterations") return False # Demonstrationsif __name__ == "__main__": print("\n" + "=" * 70) print("TEST 1: Duplicates but no ambiguity") print("=" * 70) search_with_duplicates_verbose([2, 5, 6, 0, 0, 1, 2], 0) print("\n" + "=" * 70) print("TEST 2: Pathological case - many duplicates") print("=" * 70) search_with_duplicates_verbose([1, 1, 1, 1, 1, 2, 1, 1, 1], 2) print("\n" + "=" * 70) print("TEST 3: All same values - target not present") print("=" * 70) search_with_duplicates_verbose([1, 1, 1, 1, 1], 2) print("\n" + "=" * 70) print("TEST 4: Worst case - degenerate to linear") print("=" * 70) arr = [1] * 100 + [2] + [1] * 100 # 201 elements, mostly 1s print(f"Array of 201 elements (mostly 1s), searching for 2") result = search_with_duplicates(arr, 2) print(f"Result: {result}")Understanding the complexity behavior is crucial for interview discussions. Let's analyze both average and worst cases.
| Scenario | Distinct Elements | With Duplicates |
|---|---|---|
| Best Case | O(1) - target at mid | O(1) - target at mid |
| Average Case | O(log n) | O(log n) - if duplicates are limited |
| Worst Case | O(log n) | O(n) - when nums[low]=nums[mid]=nums[high] |
| Space | O(1) | O(1) |
Why O(n) worst case?
In the worst case with duplicates, every iteration might only eliminate 2 elements (one from each end). Consider searching for 0 in:
[1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1]
At each step:
nums[low] = 1, nums[mid] = 1, nums[high] = 1low and decrement highFor an array of n elements, we need approximately n/2 iterations = O(n).
Can we do better?
Unfortunately, no—at least not with comparison-based algorithms. When all boundary values are identical, there is no information to distinguish which half contains the rotation point. This is a fundamental limitation, not an algorithm deficiency.
If an interviewer asks whether you can achieve O(log n) with duplicates, the correct answer is: 'No, not in the worst case. When all visible boundary values are identical, we have no information to eliminate half the array. The best we can guarantee is O(log n) average case when duplicates are limited.'
Average case analysis:
If duplicates are sparse (most elements are distinct), the algorithm still performs close to O(log n) because:
nums[low] = nums[mid] = nums[high]) occurs rarelyIn practice, if you expect limited duplicates, this algorithm remains efficient. Only in adversarial or highly degenerate inputs does it approach O(n).
Given the limitations with duplicates, are there alternative strategies? Let's examine several approaches and their trade-offs.
low++, high--, skip all duplicates at the boundaries:
while low < high and nums[low] == nums[low+1]: low += 1
while low < high and nums[high] == nums[high-1]: high -= 1
This can help in some cases but doesn't change worst-case complexity. If ALL elements are the same except one, we still need O(n).123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
# Alternative 1: Aggressive duplicate skippingdef search_skip_duplicates(nums: list[int], target: int) -> bool: """Skip runs of duplicates at boundaries.""" if not nums: return False low, high = 0, len(nums) - 1 while low <= high: # Skip duplicates at low end while low < high and nums[low] == nums[low + 1]: low += 1 # Skip duplicates at high end while low < high and nums[high] == nums[high - 1]: high -= 1 mid = low + (high - low) // 2 if nums[mid] == target: return True # Now apply standard logic (less likely to hit ambiguous case) if nums[low] <= nums[mid]: if nums[low] <= target < nums[mid]: high = mid - 1 else: low = mid + 1 else: if nums[mid] < target <= nums[high]: low = mid + 1 else: high = mid - 1 return False # Alternative 2: Find pivot first, then searchdef find_rotation_index_with_dups(nums: list[int]) -> int: """Find index of minimum element (rotation point).""" low, high = 0, len(nums) - 1 while low < high: mid = low + (high - low) // 2 if nums[mid] > nums[high]: low = mid + 1 elif nums[mid] < nums[high]: high = mid else: # nums[mid] == nums[high], can't decide high -= 1 return low def search_two_pass(nums: list[int], target: int) -> bool: """Find pivot, then binary search appropriate half.""" if not nums: return False pivot = find_rotation_index_with_dups(nums) n = len(nums) # Standard binary search, adjusted for rotation low, high = 0, n - 1 while low <= high: mid = low + (high - low) // 2 real_mid = (mid + pivot) % n # Adjust for rotation if nums[real_mid] == target: return True elif nums[real_mid] < target: low = mid + 1 else: high = mid - 1 return False # Compare approachesdef compare_approaches(): test_cases = [ ([2, 5, 6, 0, 0, 1, 2], 0), ([1, 1, 1, 1, 1, 2, 1, 1, 1], 2), ([1, 1, 1, 1, 1], 2), ([3, 1, 1], 3), ] print("Comparing Approaches") print("=" * 60) for arr, target in test_cases: result1 = search_skip_duplicates(arr, target) result2 = search_two_pass(arr, target) print(f"Array: {arr}, Target: {target}") print(f" Skip duplicates: {result1}") print(f" Two-pass: {result2}") print() compare_approaches()Duplicates introduce several edge cases that don't exist with distinct elements. Understanding these is crucial for a robust implementation.
[1, 1, 1, 1, 1] — Returns true if target=1, false otherwise. Algorithm degrades to O(n) but is correct.[1, 1, 1, 2, 2, 2] — Rotation can be anywhere between blocks. Handle boundary carefully.[2, 2, 1, 1] — The rotation point separates the two duplicate blocks. Our algorithm handles this.[1, 1, 1, 2, 1, 1, 1], target=1 — Must search both halves because target appears in both.[1, 1, 1, 2, 1, 1, 1], target=2 — The unique element is the target. May need to search through duplicates to find it.[1, 1, 2, 1, 1] vs [1, 2, 1, 1, 1] — Same elements, different rotations. Both handled correctly.12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
def test_duplicate_edge_cases(search_fn): """Comprehensive test suite for duplicate handling.""" test_cases = [ # (array, target, expected, description) ([1, 1, 1, 1, 1], 1, True, "All same - target present"), ([1, 1, 1, 1, 1], 2, False, "All same - target absent"), ([2, 2, 2, 0, 1], 0, True, "Rotation amid duplicates"), ([2, 2, 2, 0, 2], 0, True, "Target in rotation gap"), ([1, 1, 1, 2, 1, 1, 1], 2, True, "Single non-duplicate target"), ([1, 1, 1, 2, 1, 1, 1], 1, True, "Target in duplicate regions"), ([1, 1, 1, 2, 1, 1, 1], 3, False, "Target not present"), ([3, 1, 1], 3, True, "Minimum duplicates, target at start"), ([1, 1, 3], 3, True, "Minimum duplicates, target at end"), ([1, 1, 1, 2, 2, 2, 1, 1, 1], 2, True, "Block of different value"), ([2, 2, 2, 1, 1, 1, 2, 2, 2], 1, True, "Block in middle"), ([2, 2, 1, 2, 2], 1, True, "Single different value in middle"), ([], 1, False, "Empty array"), ([1], 1, True, "Single element - present"), ([1], 0, False, "Single element - absent"), ] print("Testing Duplicate Edge Cases") print("=" * 70) passed = failed = 0 for arr, target, expected, desc in test_cases: result = search_fn(arr, target) status = "✓" if result == expected else "✗" if result == expected: passed += 1 else: failed += 1 print(f"{status} FAIL: {desc}") print(f" Array: {arr}, Target: {target}") print(f" Expected: {expected}, Got: {result}") print(f"\nResults: {passed}/{passed + failed} passed") return failed == 0 # Test the standard implementationdef search_with_duplicates(nums, target): if not nums: return False low, high = 0, len(nums) - 1 while low <= high: mid = low + (high - low) // 2 if nums[mid] == target: return True if nums[low] == nums[mid] == nums[high]: low += 1 high -= 1 continue if nums[low] <= nums[mid]: if nums[low] <= target < nums[mid]: high = mid - 1 else: low = mid + 1 else: if nums[mid] < target <= nums[high]: low = mid + 1 else: high = mid - 1 return False test_duplicate_edge_cases(search_with_duplicates)When discussing this problem with an interviewer, several points can demonstrate deep understanding and mature engineering judgment.
What interviewers really want to see is that you understand why the problem becomes harder with duplicates, not just that you can write the code. The ability to analyze and explain algorithmic limitations is more valuable than memorizing solutions.
Handling duplicates in rotated sorted arrays is a significant extension that tests deep understanding of binary search invariants. Here's what we covered:
nums[low] = nums[mid] = nums[high], we cannot determine which half is sorted.What's next:
We've covered the algorithm variations in detail. The next page provides a comprehensive time complexity analysis across all scenarios—rotated vs non-rotated, with vs without duplicates, and best/average/worst cases for each.
You now understand how to handle duplicates in rotated sorted arrays, including why the complexity degrades and how to explain this in interviews. This is one of the more nuanced binary search variations, and mastering it demonstrates genuine algorithmic maturity.