251 lines
11 KiB
YAML
251 lines
11 KiB
YAML
title: Search in Rotated Sorted Array
|
|
slug: search-in-rotated-sorted-array
|
|
difficulty: medium
|
|
leetcode_id: 33
|
|
leetcode_url: https://leetcode.com/problems/search-in-rotated-sorted-array/
|
|
categories:
|
|
- arrays
|
|
- binary-search
|
|
patterns:
|
|
- slug: binary-search
|
|
is_optimal: true
|
|
|
|
function_signature: "def search(nums: list[int], target: int) -> int:"
|
|
|
|
test_cases:
|
|
visible:
|
|
- input: { nums: [4, 5, 6, 7, 0, 1, 2], target: 0 }
|
|
expected: 4
|
|
- input: { nums: [4, 5, 6, 7, 0, 1, 2], target: 3 }
|
|
expected: -1
|
|
- input: { nums: [1], target: 0 }
|
|
expected: -1
|
|
hidden:
|
|
- input: { nums: [1], target: 1 }
|
|
expected: 0
|
|
- input: { nums: [3, 1], target: 1 }
|
|
expected: 1
|
|
- input: { nums: [5, 1, 3], target: 5 }
|
|
expected: 0
|
|
- input: { nums: [1, 2, 3, 4, 5], target: 3 }
|
|
expected: 2
|
|
- input: { nums: [6, 7, 8, 1, 2, 3, 4, 5], target: 8 }
|
|
expected: 2
|
|
- input: { nums: [3, 4, 5, 6, 7, 8, 1, 2], target: 2 }
|
|
expected: 7
|
|
- input: { nums: [2, 3, 4, 5, 6, 7, 8, 1], target: 6 }
|
|
expected: 4
|
|
|
|
description: |
|
|
There is an integer array `nums` sorted in ascending order (with **distinct** values).
|
|
|
|
Prior to being passed to your function, `nums` is **possibly rotated** at an unknown pivot index `k` (`1 <= 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,5,6,7]` might be rotated at pivot index `3` and become `[4,5,6,7,0,1,2]`.
|
|
|
|
Given the array `nums` **after** the possible rotation and an integer `target`, return *the index of* `target` *if it is in* `nums`*, or* `-1` *if it is not in* `nums`.
|
|
|
|
You must write an algorithm with **O(log n)** runtime complexity.
|
|
|
|
constraints: |
|
|
- `1 <= nums.length <= 5000`
|
|
- `-10^4 <= nums[i] <= 10^4`
|
|
- All values of `nums` are **unique**
|
|
- `nums` is an ascending array that is possibly rotated
|
|
- `-10^4 <= target <= 10^4`
|
|
|
|
examples:
|
|
- input: "nums = [4,5,6,7,0,1,2], target = 0"
|
|
output: "4"
|
|
explanation: "The target 0 is found at index 4."
|
|
- input: "nums = [4,5,6,7,0,1,2], target = 3"
|
|
output: "-1"
|
|
explanation: "The target 3 is not in the array, so return -1."
|
|
- input: "nums = [1], target = 0"
|
|
output: "-1"
|
|
explanation: "Single element array doesn't contain the target."
|
|
|
|
explanation:
|
|
intuition: |
|
|
Imagine a sorted array as a staircase going upward. When you rotate it, you're cutting the staircase at some point and moving the upper portion to the end — creating a "broken" staircase with two ascending sections.
|
|
|
|
For example, `[0,1,2,4,5,6,7]` becomes `[4,5,6,7,0,1,2]`. We now have two sorted "halves": `[4,5,6,7]` (the higher half) and `[0,1,2]` (the lower half).
|
|
|
|
The key insight is: **at least one half of the array is always properly sorted**. When you pick a middle element, you can determine which half is sorted by comparing the endpoints. Then you check if your target falls within that sorted range — if yes, search there; if not, search the other half.
|
|
|
|
Think of it like this: you're at the middle of a broken staircase. You look left and right. One side will be a clean, unbroken staircase (sorted). You can quickly tell if your target is on that sorted side. If not, it must be on the "broken" side, so you continue searching there.
|
|
|
|
approach: |
|
|
We solve this using **Modified Binary Search**:
|
|
|
|
**Step 1: Initialise pointers**
|
|
|
|
- `left = 0`, `right = len(nums) - 1`
|
|
- We'll search within `[left, right]` inclusive
|
|
|
|
|
|
|
|
**Step 2: Binary search with sorted-half detection**
|
|
|
|
- While `left <= right`:
|
|
- Calculate `mid = left + (right - left) // 2`
|
|
- If `nums[mid] == target`: return `mid`
|
|
- **Determine which half is sorted**:
|
|
- If `nums[left] <= nums[mid]`: **left half is sorted**
|
|
- If `nums[left] <= target < nums[mid]`: target is in left half, set `right = mid - 1`
|
|
- Else: target is in right half, set `left = mid + 1`
|
|
- Else: **right half is sorted**
|
|
- If `nums[mid] < target <= nums[right]`: target is in right half, set `left = mid + 1`
|
|
- Else: target is in left half, set `right = mid - 1`
|
|
|
|
|
|
|
|
**Step 3: Return -1 if not found**
|
|
|
|
- If the loop exits without finding the target, return `-1`
|
|
|
|
|
|
|
|
The algorithm works because we can always identify one sorted half and determine if the target lies within that range. If it does, we search there; otherwise, we search the other (potentially rotated) half.
|
|
|
|
common_pitfalls:
|
|
- title: Confusing Which Half is Sorted
|
|
description: |
|
|
The condition `nums[left] <= nums[mid]` (with `<=`, not `<`) correctly identifies when the left half is sorted. The `=` is important for cases where `left == mid` (small subarrays).
|
|
|
|
For example, with `[3, 1]` and `left = 0, mid = 0`:
|
|
- `nums[left] == nums[mid] == 3`
|
|
- The left "half" (just element 3) is trivially sorted
|
|
|
|
Using `<` instead of `<=` would incorrectly identify the right half as sorted.
|
|
wrong_approach: "if nums[left] < nums[mid] (missing equality)"
|
|
correct_approach: "if nums[left] <= nums[mid]"
|
|
|
|
- title: Incorrect Target Range Checks
|
|
description: |
|
|
When checking if the target is in the sorted half, be precise about the boundaries:
|
|
- For left half: `nums[left] <= target < nums[mid]` (exclude mid since we already checked it)
|
|
- For right half: `nums[mid] < target <= nums[right]` (exclude mid)
|
|
|
|
Missing the equality check with the endpoints can cause you to search the wrong half.
|
|
wrong_approach: "nums[left] < target < nums[mid]"
|
|
correct_approach: "nums[left] <= target < nums[mid]"
|
|
|
|
- title: Using Two-Pass Approach
|
|
description: |
|
|
A tempting approach is to first find the pivot (minimum element) using "Find Minimum in Rotated Sorted Array", then do a standard binary search on the correct half. While this works, it requires **two passes** (O(log n) + O(log n)).
|
|
|
|
The single-pass approach is cleaner and more elegant — you determine the sorted half and search direction simultaneously in one loop.
|
|
wrong_approach: "Find pivot first, then binary search"
|
|
correct_approach: "Single binary search with sorted-half detection"
|
|
|
|
key_takeaways:
|
|
- "**One sorted half**: In a rotated sorted array, at least one half is always properly sorted — use this property to guide your search"
|
|
- "**Compare with endpoints**: Comparing `nums[left]` with `nums[mid]` tells you which half is sorted"
|
|
- "**Range membership**: Once you identify the sorted half, checking if target is in that range is straightforward"
|
|
- "**Foundation problem**: This technique is the basis for many rotated array problems (find minimum, search with duplicates, etc.)"
|
|
|
|
time_complexity: "O(log n). Each iteration eliminates half the search space."
|
|
space_complexity: "O(1). Only a constant number of pointer variables are used."
|
|
|
|
solutions:
|
|
- approach_name: Binary Search with Sorted-Half Detection
|
|
is_optimal: true
|
|
code: |
|
|
def search(nums: list[int], target: int) -> int:
|
|
left, right = 0, len(nums) - 1
|
|
|
|
while left <= right:
|
|
mid = left + (right - left) // 2
|
|
|
|
# Found the target
|
|
if nums[mid] == target:
|
|
return mid
|
|
|
|
# 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 -1
|
|
explanation: |
|
|
**Time Complexity:** O(log n) — Search space halves each iteration.
|
|
|
|
**Space Complexity:** O(1) — Constant extra space.
|
|
|
|
At each step, we identify which half is sorted (by comparing `nums[left]` with `nums[mid]`), then check if the target falls within that sorted range. If yes, we search that half; otherwise, we search the other half. This ensures we always make progress toward the target or confirm its absence.
|
|
|
|
- approach_name: Two-Pass (Find Pivot + Binary Search)
|
|
is_optimal: false
|
|
code: |
|
|
def search(nums: list[int], target: int) -> int:
|
|
n = len(nums)
|
|
|
|
# Step 1: Find the pivot (index of minimum element)
|
|
left, right = 0, n - 1
|
|
while left < right:
|
|
mid = left + (right - left) // 2
|
|
if nums[mid] > nums[right]:
|
|
left = mid + 1
|
|
else:
|
|
right = mid
|
|
pivot = left
|
|
|
|
# Step 2: Determine which half to search
|
|
# If target >= nums[0], it's in the left portion (before pivot)
|
|
# Otherwise, it's in the right portion (pivot to end)
|
|
if target >= nums[0]:
|
|
left, right = 0, pivot - 1 if pivot > 0 else 0
|
|
else:
|
|
left, right = pivot, n - 1
|
|
|
|
# Handle edge case: array not rotated or single element
|
|
if pivot == 0:
|
|
left, right = 0, n - 1
|
|
|
|
# Step 3: Standard binary search in the chosen range
|
|
while left <= right:
|
|
mid = left + (right - left) // 2
|
|
if nums[mid] == target:
|
|
return mid
|
|
elif nums[mid] < target:
|
|
left = mid + 1
|
|
else:
|
|
right = mid - 1
|
|
|
|
return -1
|
|
explanation: |
|
|
**Time Complexity:** O(log n) — Two binary searches, each O(log n).
|
|
|
|
**Space Complexity:** O(1) — Constant extra space.
|
|
|
|
This approach first finds the pivot (minimum element) to understand the array's structure, then performs a standard binary search on the appropriate half. While correct and having the same complexity, it's more verbose than the single-pass approach. Useful if you've already solved "Find Minimum in Rotated Sorted Array" and want to build on that.
|
|
|
|
- approach_name: Linear Search
|
|
is_optimal: false
|
|
code: |
|
|
def search(nums: list[int], target: int) -> int:
|
|
# Simple scan through the array
|
|
for i, num in enumerate(nums):
|
|
if num == target:
|
|
return i
|
|
return -1
|
|
explanation: |
|
|
**Time Complexity:** O(n) — Scans every element.
|
|
|
|
**Space Complexity:** O(1) — No extra space used.
|
|
|
|
A straightforward linear scan. This doesn't meet the O(log n) requirement but demonstrates the baseline approach. With constraints up to `n = 5000`, linear search would still pass but isn't optimal.
|