154. Find Minimum in Rotated Sorted Array II
Problem Description
You are given an array that was originally sorted in ascending order, but has been rotated between 1 and n times. A rotation means taking the last element and moving it to the front of the array.
For example, if you start with [0,1,4,4,5,6,7]
:
- After 1 rotation:
[7,0,1,4,4,5,6]
- After 4 rotations:
[4,5,6,7,0,1,4]
- After 7 rotations:
[0,1,4,4,5,6,7]
(back to original)
The array may contain duplicate values. Your task is to find and return the minimum element in this rotated array.
The challenge is to minimize the number of operations needed to find this minimum value. A naive approach would check every element, but you need to find a more efficient solution.
The solution uses a modified binary search approach. The key insight is comparing the middle element with the rightmost element to determine which half of the array contains the minimum:
- If
nums[mid] > nums[right]
: The minimum must be in the right half (after mid) - If
nums[mid] < nums[right]
: The minimum is in the left half (including mid) - If
nums[mid] == nums[right]
: Due to duplicates, we can't determine which half contains the minimum, so we cautiously shrink the search space by movingright
one position left
This approach handles the complexity introduced by duplicates while maintaining efficiency in most cases.
Intuition
The key observation is that even though the array is rotated, it maintains a special property: it consists of two sorted portions. When we rotate a sorted array, we essentially split it into two parts - both still sorted internally, but with a "break point" where the minimum element sits.
Think of it like a mountain range that's been cut and shifted. The original sorted array is like a smooth incline from lowest to highest. After rotation, it becomes two inclines with a sudden drop between them - and that drop is where our minimum lives.
In a normal sorted array, we could use binary search because we know exactly how elements relate to each other. Here, we can still use binary search, but we need to be clever about which half to search.
When we pick a middle element, we compare it with the rightmost element. Why the right? Because:
- If
nums[mid] > nums[right]
, something's wrong with the normal sorted order - the minimum must have been rotated into the right portion - If
nums[mid] < nums[right]
, the right portion is properly sorted, so the minimum must be in the left portion (or could be mid itself)
The tricky part comes with duplicates. When nums[mid] == nums[right]
, we lose our ability to determine which half is "broken" by the rotation. Consider [3,3,1,3]
- the middle and right are both 3, but the minimum 1 is between them. We can't tell just from the values.
In this case, we play it safe by shrinking our search space by just one element from the right. This ensures we don't accidentally skip over the minimum, though it means in worst case (many duplicates) we might degrade to linear time.
This approach elegantly balances efficiency with correctness, using the sorted nature of the sub-arrays to guide our search while carefully handling the edge case of duplicates.
Solution Approach
We implement a modified binary search algorithm to find the minimum element efficiently. Here's the step-by-step breakdown:
Initialize the search boundaries:
left, right = 0, len(nums) - 1
We start with pointers at both ends of the array.
Binary search with modifications:
while left < right: mid = (left + right) >> 1
The loop continues while we have a search space. We calculate the middle index using bit shift (>> 1
), which is equivalent to integer division by 2.
Decision logic based on comparison:
-
Case 1:
nums[mid] > nums[right]
if nums[mid] > nums[right]: left = mid + 1
This means the rotation point (and minimum) is in the right half. For example, in
[4,5,6,7,0,1,2]
, if mid points to 6 and right points to 2, we know the minimum must be after position mid. -
Case 2:
nums[mid] < nums[right]
elif nums[mid] < nums[right]: right = mid
The right portion is properly sorted, so the minimum is either at mid or to its left. We keep mid in our search space by setting
right = mid
(notmid - 1
). -
Case 3:
nums[mid] == nums[right]
else: right -= 1
With duplicates, we can't determine which half contains the minimum. We cautiously shrink the search space by one element from the right. This ensures we don't miss the minimum even if it means slower convergence.
Return the result:
return nums[left]
When the loop exits, left
and right
converge to the minimum element's position.
Time Complexity:
- Best/Average case:
O(log n)
when there are few or no duplicates - Worst case:
O(n)
when the array contains many duplicates (e.g.,[1,1,1,1,0,1,1]
)
Space Complexity: O(1)
- only using a constant amount of extra space for pointers.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the algorithm with the array [4,5,6,7,0,1,2]
:
Initial state:
- Array:
[4,5,6,7,0,1,2]
left = 0
,right = 6
Iteration 1:
mid = (0 + 6) >> 1 = 3
nums[mid] = 7
,nums[right] = 2
- Since
7 > 2
, the minimum must be in the right half - Update:
left = mid + 1 = 4
- Search space:
[0,1,2]
Iteration 2:
left = 4
,right = 6
mid = (4 + 6) >> 1 = 5
nums[mid] = 1
,nums[right] = 2
- Since
1 < 2
, the right portion is sorted, minimum is at mid or left of it - Update:
right = mid = 5
- Search space:
[0,1]
Iteration 3:
left = 4
,right = 5
mid = (4 + 5) >> 1 = 4
nums[mid] = 0
,nums[right] = 1
- Since
0 < 1
, the right portion is sorted, minimum is at mid or left of it - Update:
right = mid = 4
- Search space:
[0]
Final state:
left = 4
,right = 4
(loop exits sinceleft == right
)- Return
nums[4] = 0
Now let's see how duplicates are handled with [2,2,2,0,2,2]
:
Initial state:
- Array:
[2,2,2,0,2,2]
left = 0
,right = 5
Iteration 1:
mid = (0 + 5) >> 1 = 2
nums[mid] = 2
,nums[right] = 2
- Since they're equal, we can't determine which half has the minimum
- Update:
right = right - 1 = 4
- Search space:
[2,2,2,0,2]
Iteration 2:
left = 0
,right = 4
mid = (0 + 4) >> 1 = 2
nums[mid] = 2
,nums[right] = 2
- Again equal, so decrement right
- Update:
right = 3
- Search space:
[2,2,2,0]
Iteration 3:
left = 0
,right = 3
mid = (0 + 3) >> 1 = 1
nums[mid] = 2
,nums[right] = 0
- Since
2 > 0
, minimum is in the right half - Update:
left = mid + 1 = 2
- Search space:
[2,0]
Iteration 4:
left = 2
,right = 3
mid = (2 + 3) >> 1 = 2
nums[mid] = 2
,nums[right] = 0
- Since
2 > 0
, minimum is in the right half - Update:
left = mid + 1 = 3
- Search space:
[0]
Final state:
left = 3
,right = 3
(loop exits)- Return
nums[3] = 0
The algorithm successfully navigates through duplicates by carefully shrinking the search space when ambiguity arises, ensuring the minimum is never skipped.
Solution Implementation
1class Solution:
2 def findMin(self, nums: List[int]) -> int:
3 """
4 Find the minimum element in a rotated sorted array that may contain duplicates.
5 Uses binary search to achieve O(log n) average time complexity.
6
7 Args:
8 nums: A rotated sorted array that may contain duplicate elements
9
10 Returns:
11 The minimum element in the array
12 """
13 # Initialize binary search boundaries
14 left = 0
15 right = len(nums) - 1
16
17 # Continue searching while the search space has more than one element
18 while left < right:
19 # Calculate middle index using bit shift (equivalent to // 2)
20 mid = (left + right) >> 1
21
22 # Case 1: Mid element is greater than right element
23 # The minimum must be in the right half (after mid)
24 if nums[mid] > nums[right]:
25 left = mid + 1
26 # Case 2: Mid element is less than right element
27 # The minimum is in the left half (including mid)
28 elif nums[mid] < nums[right]:
29 right = mid
30 # Case 3: Mid element equals right element
31 # Cannot determine which side contains minimum, safely shrink from right
32 else:
33 right -= 1
34
35 # When left == right, we've found the minimum element
36 return nums[left]
37
1class Solution {
2 /**
3 * Finds the minimum element in a rotated sorted array that may contain duplicates.
4 * Uses binary search with special handling for duplicate values.
5 *
6 * @param nums the rotated sorted array
7 * @return the minimum element in the array
8 */
9 public int findMin(int[] nums) {
10 // Initialize two pointers for binary search
11 int left = 0;
12 int right = nums.length - 1;
13
14 // Continue searching while the search space has more than one element
15 while (left < right) {
16 // Calculate the middle index using bit shift (equivalent to division by 2)
17 int mid = (left + right) >> 1;
18
19 // Case 1: Middle element is greater than rightmost element
20 // This means the minimum is in the right half (rotation point is on the right)
21 if (nums[mid] > nums[right]) {
22 left = mid + 1;
23 }
24 // Case 2: Middle element is less than rightmost element
25 // This means the right half is sorted, so minimum is in left half (including mid)
26 else if (nums[mid] < nums[right]) {
27 right = mid;
28 }
29 // Case 3: Middle element equals rightmost element
30 // Cannot determine which half contains minimum, safely shrink from right
31 else {
32 right--;
33 }
34 }
35
36 // When left equals right, we've found the minimum element
37 return nums[left];
38 }
39}
40
1class Solution {
2public:
3 int findMin(vector<int>& nums) {
4 // Initialize binary search boundaries
5 int left = 0;
6 int right = nums.size() - 1;
7
8 // Binary search to find the minimum element
9 while (left < right) {
10 // Calculate middle index using bit shift (equivalent to dividing by 2)
11 int mid = (left + right) >> 1;
12
13 // If mid element is greater than right element,
14 // the minimum must be in the right half (after mid)
15 if (nums[mid] > nums[right]) {
16 left = mid + 1;
17 }
18 // If mid element is less than right element,
19 // the minimum is in the left half (including mid)
20 else if (nums[mid] < nums[right]) {
21 right = mid;
22 }
23 // If mid element equals right element (duplicates case),
24 // we can't determine which side has the minimum,
25 // so we safely reduce the search space by moving right pointer left
26 else {
27 --right;
28 }
29 }
30
31 // When left == right, we've found the minimum element
32 return nums[left];
33 }
34};
35
1/**
2 * Finds the minimum element in a rotated sorted array that may contain duplicates
3 * Uses binary search with special handling for duplicate values
4 * Time Complexity: O(log n) average case, O(n) worst case when many duplicates exist
5 * Space Complexity: O(1)
6 *
7 * @param nums - The rotated sorted array that may contain duplicates
8 * @returns The minimum element in the array
9 */
10function findMin(nums: number[]): number {
11 // Initialize binary search boundaries
12 let left: number = 0;
13 let right: number = nums.length - 1;
14
15 // Continue searching while the search space has more than one element
16 while (left < right) {
17 // Calculate middle index using bit shift for integer division
18 const mid: number = (left + right) >> 1;
19
20 // Case 1: Mid element is greater than right element
21 // The minimum must be in the right half (after mid)
22 if (nums[mid] > nums[right]) {
23 left = mid + 1;
24 }
25 // Case 2: Mid element is less than right element
26 // The minimum is in the left half (including mid)
27 else if (nums[mid] < nums[right]) {
28 right = mid;
29 }
30 // Case 3: Mid element equals right element
31 // Cannot determine which half contains minimum, safely exclude right element
32 else {
33 right--;
34 }
35 }
36
37 // When left equals right, we've found the minimum element
38 return nums[left];
39}
40
Time and Space Complexity
Time Complexity: O(n)
in the worst case, O(log n)
in the average case
The algorithm uses binary search to find the minimum element in a rotated sorted array that may contain duplicates.
- In the best/average case where duplicates are minimal, the search space is halved in each iteration, resulting in
O(log n)
time complexity. - In the worst case, when the array contains many duplicates (especially when
nums[mid] == nums[right]
), we can only eliminate one element at a time by decrementingright
. Consider an array like[2,2,2,2,2,2,2,1,2,2]
- we might need to linearly scan through most elements, resulting inO(n)
time complexity.
Space Complexity: O(1)
The algorithm only uses a constant amount of extra space for the variables left
, right
, and mid
, regardless of the input size. No additional data structures or recursive calls are used.
Common Pitfalls
1. Incorrect Boundary Update in Case 2
A frequent mistake is setting right = mid - 1
instead of right = mid
when nums[mid] < nums[right]
.
Why it's wrong: When the mid element is less than the right element, the mid element itself could be the minimum. By setting right = mid - 1
, you might exclude the actual minimum from your search space.
Example causing failure:
nums = [2, 1] # Initial: left = 0, right = 1, mid = 0 # nums[0] = 2 > nums[1] = 1, so we should search right half # But if we incorrectly used right = mid - 1 in other cases...
Correct approach:
elif nums[mid] < nums[right]: right = mid # Keep mid in search space
2. Attempting to Optimize the Duplicate Case Too Aggressively
Some might try to skip multiple elements or use left += 1
when handling duplicates.
Why it's wrong: When nums[mid] == nums[right]
, we cannot determine which side contains the minimum. Moving left pointer forward or skipping multiple elements might miss the minimum.
Example causing failure:
nums = [1, 1, 1, 0, 1] # If mid = 2, right = 4, both are 1 # Moving left pointer might skip over the 0
Correct approach:
else: right -= 1 # Safely shrink from right by one element only
3. Using Wrong Comparison Target
Comparing nums[mid]
with nums[left]
instead of nums[right]
makes the logic more complex and error-prone.
Why it's problematic: The relationship between mid and left doesn't consistently indicate where the minimum lies, especially with duplicates. The right comparison provides clearer decision boundaries.
Correct approach: Always compare with nums[right]
for consistent logic.
4. Not Handling Single Element or Already Sorted Arrays
While the provided solution handles these cases correctly, forgetting to consider them during implementation can lead to issues.
Edge cases to remember:
- Single element:
[1]
→ Should return 1 immediately - No rotation:
[1, 2, 3, 4, 5]
→ Minimum is at index 0 - Full rotation: Array rotated n times returns to original position
The current solution handles all these cases naturally through the while loop condition and comparison logic.
What are the most two important steps in writing a depth first search function? (Select 2)
Recommended Readings
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!