[[Arrays]]
Problem
There is an integer array nums sorted in ascending order (with distinct values).
Prior to being passed to your function, nums is possibly left rotated at an unknown 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 left rotated by 3 indices 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.
Example 1:
Input: nums = [4,5,6,7,0,1,2], target = 0
Output: 4
Example 2:
Input: nums = [4,5,6,7,0,1,2], target = 3
Output: -1
Example 3:
Input: nums = [1], target = 0
Output: -1
Brainstorming
so the inputs are the nums array and the target just like the binary search , the numbers in nums are all integers
we can use the idea of binary search but it would need modification the key insight is at every step of slicing the search space by half we can guarantee that alteast one half will be sorted and we can search in that half first
the target being the first or the last element doesn’t affect our process since we will follow the following method
- find the mid point
- check if the mid element is the target or not
if not check which side is sorted , which ever is sorted search in that side.
if its not in that side , it means its on the other side
to determine wether the left half is sorted and contains or number or not we can do nums[left] < nums[mid] the other thing would be to check can be if nums[left] < nums[target] < nums[mid]
the lower bound is nums[0] and nums[mid-1] is the upper bound of the left half, if after that we don’t find our target between those bounds it means that we should check in the in the right half where the bounds are nums[mid +1] till len(nums)
so if its not in the left half that means we need to move the left pointer to mid + 1
so the logic is
check if nums[0] <= nums[mid] if its not it means that the right half is sorted we move the left pointer to mid + 1
if it is check if nums[0] < target < nums[mid] if its not it means we need to search in the right half we move the left pointer to mid + 1
if the above check is correct that means the the target is in the bounds of the left array hence we move the right pointer to mid -1
before me move we need to check if target is in the sorted area of not so whether the sorted area is left or right we need to check immediately if target is between the bounds or
# check for sorted
# ... inside while loop
mid = (left + right ) // 2
# we have a check which checks if mid == target not relevant here)
if nums[0] < nums[mid-1]: # check if half is sorted
if nums[0] < target < nums[mid-1]: # check if target is in the sorted half
left = mid + 1 # then you move the left pointer ahead
else : # the right is sorted
if nums[0] < target < nums[mid-1]: # check if target is in the sorted half
right = mid -1 # then you move the right pointer behind
as the search space narrows we need to move away from static boundaries to more dynamic ones
# check for sorted
# ... inside while loop
mid = (left + right ) // 2
# we have a check which checks if mid == target not relevant here)
if nums[left] < nums[mid-1]: # check if half is sorted
if nums[left] < target < nums[mid-1]: # check if target is in the sorted half
right = mid - 1 # then you move the left pointer ahead
else : # the right is sorted
if nums[right] < target < nums[len(n)-1]: # check if target is in the sorted half
left = mid + 1 # then you move the right pointer behind
# ... inside while loop
mid = (left + right) // 2
# (check for nums[mid] == target is here)
if nums[left] <= nums[mid-1]: # we check for left to be sorted
if nums[left] <= target <= nums[mid-1]: # target not in sorted array
right = mid - 1
else: # we move the left pointer
left = mid + 1
else : # the right is sorted
if nums[right] <= target < =nums[len(n)-1]: # we check
left = mid + 1
else: # its not in the sorted array
right = mid -1
- Correct Boundary Checks: You are now using
<=which correctly handles cases where the target might be one of the boundary elements. - Correct Sorted Half Identification:
if nums[left] <= nums[mid]is the robust way to check if the left half is the sorted portion. (Note: your pseudocode still hasmid-1, but your thinking is onmid, which is correct). - The “Invisible Else” is Now Visible: You’ve correctly added the
elseblocks.- If the left half is sorted BUT the target is not in it, you correctly deduce you must search the right half by setting
left = mid + 1. - If the right half is sorted BUT the target is not in it, you correctly deduce you must search the left half by setting
right = mid - 1.
- If the left half is sorted BUT the target is not in it, you correctly deduce you must search the right half by setting
- Dynamic Boundaries: You’ve correctly identified that the bounds for the right-half check should be
nums[mid]tonums[right]. (Note: your pseudocode still haslen(n)-1, but your logic impliesnums[right]).
Let’s refine your pseudocode into the final, clean algorithm. This is Phase 4: The Articulation.
Final Algorithm
Setup:
- Initialize
left = 0,right = len(nums) - 1.
Main Loop:
while left <= right:- Calculate
mid = (left + right) // 2. - If
nums[mid] == target, you’ve found it. Returnmid. - Check if the left half is the sorted portion:
if nums[left] <= nums[mid]:- Check if the target is within the bounds of this sorted left half:
if nums[left] <= target < nums[mid]:- Yes, it is. Discard the right half:
right = mid - 1.
- Yes, it is. Discard the right half:
- Else (target is not in the sorted left half):
- You must search the unsorted right half. Discard the left half:
left = mid + 1.
- You must search the unsorted right half. Discard the left half:
- Check if the target is within the bounds of this sorted left half:
- Else (the right half must be the sorted portion):
- Check if the target is within the bounds of this sorted right half:
if nums[mid] < target <= nums[right]:- Yes, it is. Discard the left half:
left = mid + 1.
- Yes, it is. Discard the left half:
- Else (target is not in the sorted right half):
- You must search the unsorted left half. Discard the right half:
right = mid - 1.
- You must search the unsorted left half. Discard the right half:
- Check if the target is within the bounds of this sorted right half:
- Calculate
Termination:
- If the loop finishes (
left > right), the target was not found. Return-1.
nums = [4,5,6,7,0,1,2]
target = 0
def search_in_rotated_array(nums:List[int], target:int) -> int:
left = 0
right = len(nums) -1
while left <= right:
mid = (left + right) //2
if nums[mid] == target:
return mid
if nums[left] <= nums[mid]: # check if left is sorted
if nums[left] <= target <= nums[mid]: # check if target is in the sorted half
right = mid -1 # move the right to shrink the search space
else:
left = mid + 1 # move the left to shrink the search space
else: # the right have is sorted
if nums[mid] <= target <= nums[right]: # check if the target is in the sorted half
left = mid +1 # move the left to shrink the search space
else:
right = mid - 1 # move the right to shrink the search space
return -1
search_in_rotated_array(nums, target)
Variations duplicates
Cementing the idea
- Key Information: “at each iteration at least 1 half is sorted.” Correct.
- How it Helped: “since we checked which half is sorted we could safely discard the other half if the target also fell in that sorted list…” Precisely. You used the sorted half as your point of reference. If the target was within the bounds of the sorted half, you searched there. If not, you knew with certainty it had to be in the other, messier half, and you could discard the sorted portion.
This pattern is a modification of Binary Search. You can think of it as “Binary Search on a Conditionally Sorted Array.” The core is the same—find a condition that lets you throw away half the data.
On Duplicates (A Final Thought Exercise)
You said you don’t know what you would do if there are duplicates. Let’s explore that. You have all the tools to figure it out.
The Problematic Case:
Imagine nums = [3, 1, 2, 3, 3, 3, 3] and target = 1.
left = 0,right = 6.mid = 3.nums[left]is3.nums[mid]is3.nums[right]is3.
Now, apply the first check from your perfect algorithm:
if nums[left] <= nums[mid]:
- Is
3 <= 3? Yes. - Your code concludes the left half (
[3, 1, 2, 3]) is the sorted portion. - Is that conclusion correct?
What has just broken down? And what is the simplest, safest action you could take in this specific, ambiguous situation where nums[left] == nums[mid] == nums[right]?
You are spot on with your first two points. And your third point is the million-dollar question. You’ve correctly identified that this action degrades the performance.
Let’s be very precise about how it degrades. Your guess of O(n log n) is a great starting point, but let’s trace the absolute worst-case to find the real answer.
The Worst-Case Scenario:
Consider this array: nums = [3, 3, 3, 3, 3, 3, 1, 3] and target = 1.
left = 0,right = 7.mid = 3.nums[left]is 3.nums[mid]is 3.- This is the ambiguous case (
nums[left] == nums[mid]). - You apply your rule:
left += 1. - New search space:
[3, 3, 3, 3, 3, 1, 3](indices 1 to 7).
Next Iteration:
left = 1,right = 7.mid = 4.nums[left]is 3.nums[mid]is 3.- This is the ambiguous case again.
- You apply your rule:
left += 1. - New search space:
[3, 3, 3, 1, 3](indices 2 to 7).
What is happening here? Are we dividing the search space in half? Or are we just chipping one element off the front?
In this worst-case scenario, the algorithm stops being a “divide and conquer” algorithm. It degenerates. What does its behavior look like in this specific situation?
In the worst-case scenario caused by duplicates (specifically, nums[left] == nums[mid] == nums[right]), the algorithm can no longer guarantee that it’s discarding a known sorted half. Your safe fallback—shrinking the boundaries by one—is correct, but in the worst case, it degrades the search from a logarithmic O(log n) to a linear O(n) search.
This is the key trade-off and the correct answer for the “follow-up with duplicates” question. You maintain correctness, but you have to sacrifice the strict O(log n) guarantee in that one ambiguous case. The average case is still much better than linear, but the worst case is not.
This concludes our session on Binary Search and “Search in Rotated Sorted Array.”
Session Reflection
- First Principles: You started with the core idea of “divide and conquer” for binary search and correctly identified that the key to the rotated array problem was to find the “always sorted” half in any given search space.
- Logic Refinement: You successfully navigated the tricky boundary conditions (
<=vs<,mid-1vsmid) and the nestedif/elselogic to build a complete, robust algorithm. - Complexity Analysis: You correctly identified the time complexities of both the brute-force (
O(n)) and optimized (O(log n)) solutions, and you reasoned through why the introduction of duplicates could degrade the performance back toO(n)in the worst case.
This is exactly the thought process of a senior-level engineer. You’ve built the solution from the ground up, not from memorization.
What problem would you like to tackle in our next session?