Files
codetutor/backend/data/questions/search-in-rotated-sorted-array-ii.yaml
2025-05-30 19:18:33 +01:00

218 lines
10 KiB
YAML

title: Search in Rotated Sorted Array II
slug: search-in-rotated-sorted-array-ii
difficulty: medium
leetcode_id: 81
leetcode_url: https://leetcode.com/problems/search-in-rotated-sorted-array-ii/
categories:
- arrays
- binary-search
patterns:
- binary-search
description: |
There is an integer array `nums` sorted in non-decreasing order (not necessarily with **distinct** values).
Before being passed to your function, `nums` is **rotated** at an unknown pivot index `k` (`0 <= k < nums.length`) such that the resulting array is `[nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]]` (**0-indexed**). For example, `[0,1,2,4,4,4,5,6,6,7]` might be rotated at pivot index `5` and become `[4,5,6,6,7,0,1,2,4,4]`.
Given the array `nums` **after** the rotation and an integer `target`, return `true` *if* `target` *is in* `nums`*, or* `false` *if it is not in* `nums`.
You must decrease the overall operation steps as much as possible.
**Follow up:** This problem is similar to Search in Rotated Sorted Array, but `nums` may contain **duplicates**. Would this affect the runtime complexity? How and why?
constraints: |
- `1 <= nums.length <= 5000`
- `-10^4 <= nums[i] <= 10^4`
- `nums` is guaranteed to be rotated at some pivot
- `-10^4 <= target <= 10^4`
examples:
- input: "nums = [2,5,6,0,0,1,2], target = 0"
output: "true"
explanation: "The target 0 is found in the array."
- input: "nums = [2,5,6,0,0,1,2], target = 3"
output: "false"
explanation: "The target 3 is not in the array."
explanation:
intuition: |
This problem extends Search in Rotated Sorted Array by allowing **duplicate values**. The core insight from the original problem still applies: in a rotated sorted array, at least one half is always sorted, and we can use this to guide our binary search.
However, duplicates introduce a tricky edge case. Consider `nums = [1, 0, 1, 1, 1]` with `left = 0`, `mid = 2`, `right = 4`:
- `nums[left] = 1`, `nums[mid] = 1`, `nums[right] = 1`
- All three values are equal! We can't determine which half is sorted.
Think of it like standing at a broken staircase where the step you're on, and the steps at both ends, are all at the same height. You can't tell which direction leads up or down — you have to take a small step to break the tie.
The solution is simple: when `nums[left] == nums[mid]`, we can't make a decision, so we **shrink the search space by one** (`left++`). This might degrade to O(n) in the worst case (all duplicates), but it's the best we can do without additional information.
approach: |
We solve this using **Modified Binary Search with Duplicate Handling**:
**Step 1: Initialise pointers**
- `left = 0`, `right = len(nums) - 1`
- We'll search within `[left, right]` inclusive
&nbsp;
**Step 2: Binary search with duplicate handling**
- While `left <= right`:
- Calculate `mid = left + (right - left) // 2`
- If `nums[mid] == target`: return `True`
- **Handle duplicates**: If `nums[left] == nums[mid]`:
- We can't determine which half is sorted
- Increment `left` by 1 and continue
- **Determine which half is sorted**:
- If `nums[left] < nums[mid]`: **left half is sorted**
- If `nums[left] <= target < nums[mid]`: search left half (`right = mid - 1`)
- Else: search right half (`left = mid + 1`)
- Else: **right half is sorted**
- If `nums[mid] < target <= nums[right]`: search right half (`left = mid + 1`)
- Else: search left half (`right = mid - 1`)
&nbsp;
**Step 3: Return False if not found**
- If the loop exits without finding the target, return `False`
&nbsp;
The key difference from the no-duplicates version is the duplicate handling step. When we can't determine which half is sorted, we fall back to linear elimination. This ensures correctness while maintaining O(log n) performance for most inputs.
common_pitfalls:
- title: Ignoring the Duplicate Case
description: |
The most common mistake is using the exact same logic as Search in Rotated Sorted Array (without duplicates). That algorithm assumes `nums[left] <= nums[mid]` tells us the left half is sorted.
With duplicates, `nums[left] == nums[mid]` doesn't give us enough information. For example:
- `[1, 0, 1, 1, 1]`: left half contains the rotation point
- `[1, 1, 1, 0, 1]`: right half contains the rotation point
Both have `nums[left] == nums[mid]`, but the sorted half differs!
wrong_approach: "Treating nums[left] == nums[mid] as 'left half sorted'"
correct_approach: "When nums[left] == nums[mid], shrink search space with left++"
- title: Shrinking Both Ends
description: |
Some solutions shrink both `left` and `right` when duplicates are found at the boundaries. While this can work, it's more complex and error-prone.
A simpler approach is to only increment `left` when `nums[left] == nums[mid]`. This is sufficient to make progress while keeping the logic clean.
wrong_approach: "Complex logic to shrink both ends simultaneously"
correct_approach: "Simple left++ when nums[left] == nums[mid]"
- title: Expecting O(log n) Guarantee
description: |
Unlike the no-duplicates version which guarantees O(log n), this problem has **O(n) worst case**. Consider `nums = [1, 1, 1, 1, 1]` — we must check every element.
This is unavoidable. The problem statement hints at this with "decrease the overall operation steps as much as possible" rather than requiring O(log n).
Accept that O(n) worst case is inherent to the problem, not a flaw in your solution.
key_takeaways:
- "**Duplicates break binary search decisions**: When values at boundaries equal the middle value, you can't determine which half is sorted"
- "**Linear fallback is necessary**: Incrementing `left` when stuck ensures progress, even if it degrades to O(n)"
- "**Worst case is O(n)**: This is inherent to the problem — no algorithm can do better when all elements are identical"
- "**Compare to the original**: Understanding how duplicates change the problem helps solidify your understanding of both problems"
time_complexity: "O(log n) average, O(n) worst case. When we can determine the sorted half, we eliminate half the search space. When duplicates prevent this, we fall back to linear elimination."
space_complexity: "O(1). Only a constant number of pointer variables are used."
solutions:
- approach_name: Binary Search with Duplicate Handling
is_optimal: true
code: |
def search(nums: list[int], target: int) -> bool:
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
# Found the target
if nums[mid] == target:
return True
# Handle duplicates: can't determine which half is sorted
if nums[left] == nums[mid]:
left += 1
continue
# Determine which half is sorted
if nums[left] < nums[mid]:
# Left half is sorted
if nums[left] <= target < nums[mid]:
# Target is in the sorted left half
right = mid - 1
else:
# Target is in the right half
left = mid + 1
else:
# Right half is sorted
if nums[mid] < target <= nums[right]:
# Target is in the sorted right half
left = mid + 1
else:
# Target is in the left half
right = mid - 1
# Target not found
return False
explanation: |
**Time Complexity:** O(log n) average, O(n) worst case.
**Space Complexity:** O(1) — Constant extra space.
This solution handles the duplicate case by incrementing `left` when `nums[left] == nums[mid]`. For arrays without many duplicates, it behaves like standard binary search. For arrays with all identical elements, it degrades to linear search — but this is unavoidable.
- approach_name: Binary Search with Both-End Shrinking
is_optimal: false
code: |
def search(nums: list[int], target: int) -> bool:
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] == target:
return True
# Handle duplicates at both ends
if nums[left] == nums[mid] == nums[right]:
left += 1
right -= 1
elif nums[left] <= nums[mid]:
# Left half is sorted
if nums[left] <= target < nums[mid]:
right = mid - 1
else:
left = mid + 1
else:
# Right half is sorted
if nums[mid] < target <= nums[right]:
left = mid + 1
else:
right = mid - 1
return False
explanation: |
**Time Complexity:** O(log n) average, O(n) worst case.
**Space Complexity:** O(1) — Constant extra space.
This variant shrinks both ends when `nums[left] == nums[mid] == nums[right]`. It can be slightly faster in practice for arrays with duplicates at both ends, but the logic is more complex. The simpler single-end shrinking is usually preferred.
- approach_name: Linear Search
is_optimal: false
code: |
def search(nums: list[int], target: int) -> bool:
# Simple membership check
return target in nums
explanation: |
**Time Complexity:** O(n) — Scans the entire array.
**Space Complexity:** O(1) — No extra space used.
A straightforward linear scan using Python's `in` operator. While this has the same worst-case complexity as the binary search solution, it doesn't take advantage of the sorted structure when duplicates are sparse. Useful as a baseline or for very small arrays.