1095. Find in Mountain Array
Problem Description
This is an interactive problem where you need to find a target value in a mountain array with limited access.
A mountain array is defined as an array that:
- Has at least 3 elements
- Has a peak element at some index
i
(where0 < i < arr.length - 1
) - Strictly increases from the start to the peak:
arr[0] < arr[1] < ... < arr[i]
- Strictly decreases from the peak to the end:
arr[i] > arr[i + 1] > ... > arr[arr.length - 1]
You're given a MountainArray
object and a target value. Your task is to find and return the minimum index where mountainArr.get(index) == target
. If the target doesn't exist in the array, return -1
.
The catch is that you cannot directly access the array. You can only use two methods:
MountainArray.get(k)
: Returns the element at indexk
(0-indexed)MountainArray.length()
: Returns the length of the array
There's a constraint: your solution must make no more than 100 calls to MountainArray.get()
, otherwise it will be judged as Wrong Answer.
The solution uses a two-phase approach:
-
Find the peak: Uses binary search to locate the peak of the mountain array by comparing adjacent elements. If
get(mid) > get(mid + 1)
, the peak is at or beforemid
; otherwise, it's aftermid
. -
Search for target: Once the peak is found, the array is divided into two parts:
- The ascending part (from start to peak): Regular binary search
- The descending part (from peak+1 to end): Binary search with reversed comparison logic
The clever use of the parameter k
(1 or -1) in the search
function allows the same binary search logic to work for both ascending and descending portions by multiplying the comparison values by k
.
Intuition
The key insight is recognizing that a mountain array has special properties we can exploit. Since we have a strict limit on the number of calls (100), we need an efficient approach - this immediately suggests binary search, which runs in O(log n)
time.
The mountain array essentially consists of two sorted sequences joined at a peak: one ascending and one descending. If we can find the peak, we can treat each side as a sorted array and apply binary search.
Why find the peak first? Without knowing where the peak is, we can't determine which "side" of the mountain we're on when we find an element. The comparison logic differs between the ascending side (where larger indices mean larger values) and the descending side (where larger indices mean smaller values).
To find the peak efficiently, we observe that at any point in the array:
- If
arr[mid] > arr[mid + 1]
, we're on the descending side or at the peak itself, so the peak must be at positionmid
or earlier - If
arr[mid] < arr[mid + 1]
, we're on the ascending side, so the peak must be after positionmid
This allows us to binary search for the peak in O(log n)
calls.
Once we have the peak, searching for the target becomes straightforward - we perform binary search on the ascending portion first (since we want the minimum index). If not found there, we search the descending portion.
The elegant trick in the code is using a multiplier k
(1 for ascending, -1 for descending) to unify the binary search logic. By multiplying both the current element and target by k
, we convert the descending search into an ascending one: in the descending portion, smaller values are actually "greater" in terms of their position from the peak, so multiplying by -1 reverses the comparison appropriately.
This approach ensures we use at most 3 * log n
calls to get()
: one binary search to find the peak, and potentially two more to search both sides, well within the 100-call limit.
Learn more about Binary Search patterns.
Solution Approach
The implementation consists of two main parts: finding the peak and searching for the target.
Step 1: Find the Peak
l, r = 0, n - 1 while l < r: mid = (l + r) >> 1 if mountain_arr.get(mid) > mountain_arr.get(mid + 1): r = mid else: l = mid + 1
We use binary search with pointers l
and r
starting at the array boundaries. At each iteration:
- Calculate
mid = (l + r) >> 1
(bit shift for integer division) - Compare
mountain_arr.get(mid)
withmountain_arr.get(mid + 1)
- If
get(mid) > get(mid + 1)
, we're on the descending side or at the peak, so mover = mid
- Otherwise, we're on the ascending side, so move
l = mid + 1
When the loop exits, l
points to the peak index.
Step 2: Binary Search Helper Function
def search(l: int, r: int, k: int) -> int:
while l < r:
mid = (l + r) >> 1
if k * mountain_arr.get(mid) >= k * target:
r = mid
else:
l = mid + 1
return -1 if mountain_arr.get(l) != target else l
This helper function performs binary search on a given range [l, r]
. The parameter k
controls the search direction:
k = 1
for ascending portion: normal binary searchk = -1
for descending portion: reverses the comparison logic
The key insight is k * mountain_arr.get(mid) >= k * target
:
- For ascending (
k = 1
): this becomesget(mid) >= target
- For descending (
k = -1
): this becomes-get(mid) >= -target
, which is equivalent toget(mid) <= target
This multiplication trick allows us to use the same binary search logic for both ascending and descending sequences.
Step 3: Search for Target
ans = search(0, l, 1) return search(l + 1, n - 1, -1) if ans == -1 else ans
First, search the ascending portion [0, l]
with k = 1
. If found, return immediately (since we want the minimum index). If not found (ans == -1
), search the descending portion [l + 1, n - 1]
with k = -1
.
Time Complexity: O(log n)
- We perform at most three binary searches, each taking O(log n)
time.
Space Complexity: O(1)
- Only using a constant amount of extra space for variables.
API Calls: Maximum 3 * log n
calls to mountain_arr.get()
, well within the 100-call limit for reasonable array sizes.
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 solution with a concrete example:
Mountain Array: [1, 3, 5, 7, 6, 4, 2]
Target: 4
Expected Output: 5
(index where 4 is located)
Step 1: Find the Peak
We start with l = 0
, r = 6
(array length - 1).
Iteration 1:
mid = (0 + 6) >> 1 = 3
- Compare
get(3) = 7
withget(4) = 6
- Since
7 > 6
, we're on the descending side or at peak - Update
r = 3
Iteration 2:
l = 0
,r = 3
mid = (0 + 3) >> 1 = 1
- Compare
get(1) = 3
withget(2) = 5
- Since
3 < 5
, we're on the ascending side - Update
l = 2
Iteration 3:
l = 2
,r = 3
mid = (2 + 3) >> 1 = 2
- Compare
get(2) = 5
withget(3) = 7
- Since
5 < 7
, we're on the ascending side - Update
l = 3
Now l = r = 3
, so the peak is at index 3 with value 7.
Step 2: Search for Target = 4
Search ascending portion [0, 3] with k = 1:
l = 0
,r = 3
mid = 1
:1 * get(1) = 3
vs1 * 4 = 4
- Since
3 < 4
, updatel = 2
- Since
l = 2
,r = 3
mid = 2
:1 * get(2) = 5
vs1 * 4 = 4
- Since
5 >= 4
, updater = 2
- Since
- Now
l = r = 2
, checkget(2) = 5 ≠ 4
- Return
-1
(not found in ascending portion)
Search descending portion [4, 6] with k = -1:
l = 4
,r = 6
mid = 5
:-1 * get(5) = -4
vs-1 * 4 = -4
- Since
-4 >= -4
, updater = 5
- Since
- Now
l = 4
,r = 5
mid = 4
:-1 * get(4) = -6
vs-1 * 4 = -4
- Since
-6 < -4
, updatel = 5
- Since
- Now
l = r = 5
, checkget(5) = 4 = 4
✓ - Return
5
Final Answer: 5
The solution efficiently found the target using only 10 calls to get()
: 3 to find the peak, 3 for the ascending search, and 4 for the descending search.
Solution Implementation
1# """
2# This is MountainArray's API interface.
3# You should not implement it, or speculate about its implementation
4# """
5# class MountainArray:
6# def get(self, index: int) -> int:
7# def length(self) -> int:
8
9
10class Solution:
11 def findInMountainArray(self, target: int, mountain_arr: 'MountainArray') -> int:
12 def binary_search(left: int, right: int, ascending: int) -> int:
13 """
14 Perform binary search in a sorted portion of the mountain array.
15
16 Args:
17 left: Starting index of search range
18 right: Ending index of search range
19 ascending: 1 for ascending order, -1 for descending order
20
21 Returns:
22 Index of target if found, -1 otherwise
23 """
24 while left < right:
25 mid = (left + right) // 2
26
27 # Use ascending parameter to handle both ascending and descending searches
28 # When ascending=1: normal comparison for ascending array
29 # When ascending=-1: reversed comparison for descending array
30 if ascending * mountain_arr.get(mid) >= ascending * target:
31 right = mid
32 else:
33 left = mid + 1
34
35 # Check if the element at final position is the target
36 return left if mountain_arr.get(left) == target else -1
37
38 # Get the length of the mountain array
39 array_length = mountain_arr.length()
40
41 # Find the peak element using binary search
42 left, right = 0, array_length - 1
43 while left < right:
44 mid = (left + right) // 2
45
46 # If mid element is greater than next element, peak is on left side (including mid)
47 if mountain_arr.get(mid) > mountain_arr.get(mid + 1):
48 right = mid
49 else:
50 # Peak is on right side
51 left = mid + 1
52
53 # At this point, left is the index of the peak element
54 peak_index = left
55
56 # First, search in the ascending part (from start to peak)
57 result = binary_search(0, peak_index, 1)
58
59 # If not found in ascending part, search in descending part (from peak+1 to end)
60 if result == -1:
61 result = binary_search(peak_index + 1, array_length - 1, -1)
62
63 return result
64
1/**
2 * // This is MountainArray's API interface.
3 * // You should not implement it, or speculate about its implementation
4 * interface MountainArray {
5 * public int get(int index) {}
6 * public int length() {}
7 * }
8 */
9
10class Solution {
11 private MountainArray mountainArray;
12 private int targetValue;
13
14 /**
15 * Finds the minimum index of target in a mountain array.
16 * A mountain array is an array that increases to a peak and then decreases.
17 *
18 * @param target The value to search for
19 * @param mountainArr The mountain array to search in
20 * @return The minimum index where target is found, or -1 if not found
21 */
22 public int findInMountainArray(int target, MountainArray mountainArr) {
23 int arrayLength = mountainArr.length();
24
25 // Step 1: Find the peak element index using binary search
26 int left = 0;
27 int right = arrayLength - 1;
28
29 while (left < right) {
30 int mid = (left + right) >>> 1; // Unsigned right shift to avoid overflow
31
32 // If mid element is greater than next element, peak is on the left side (including mid)
33 if (mountainArr.get(mid) > mountainArr.get(mid + 1)) {
34 right = mid;
35 } else {
36 // Peak is on the right side
37 left = mid + 1;
38 }
39 }
40
41 int peakIndex = left;
42
43 // Initialize instance variables for use in search method
44 this.mountainArray = mountainArr;
45 this.targetValue = target;
46
47 // Step 2: Search in the ascending part (left side of peak)
48 // Pass direction = 1 for ascending order
49 int resultInAscending = binarySearch(0, peakIndex, 1);
50
51 // Step 3: If not found in ascending part, search in descending part
52 // Pass direction = -1 for descending order
53 return resultInAscending == -1 ?
54 binarySearch(peakIndex + 1, arrayLength - 1, -1) :
55 resultInAscending;
56 }
57
58 /**
59 * Performs binary search in either ascending or descending portion of the array.
60 *
61 * @param left The left boundary of search range
62 * @param right The right boundary of search range
63 * @param direction 1 for ascending order, -1 for descending order
64 * @return The index where target is found, or -1 if not found
65 */
66 private int binarySearch(int left, int right, int direction) {
67 while (left < right) {
68 int mid = (left + right) >>> 1; // Unsigned right shift to avoid overflow
69
70 // Multiply by direction to handle both ascending and descending cases
71 // For ascending (direction=1): normal comparison
72 // For descending (direction=-1): reversed comparison
73 if (direction * mountainArray.get(mid) >= direction * targetValue) {
74 right = mid;
75 } else {
76 left = mid + 1;
77 }
78 }
79
80 // Check if the element at final position equals target
81 return mountainArray.get(left) == targetValue ? left : -1;
82 }
83}
84
1/**
2 * // This is the MountainArray's API interface.
3 * // You should not implement it, or speculate about its implementation
4 * class MountainArray {
5 * public:
6 * int get(int index);
7 * int length();
8 * };
9 */
10
11class Solution {
12public:
13 int findInMountainArray(int target, MountainArray& mountainArr) {
14 int arrayLength = mountainArr.length();
15
16 // Step 1: Find the peak element index using binary search
17 int left = 0, right = arrayLength - 1;
18 while (left < right) {
19 int mid = left + (right - left) / 2;
20 // If mid element is greater than next element, peak is on left side (including mid)
21 if (mountainArr.get(mid) > mountainArr.get(mid + 1)) {
22 right = mid;
23 } else {
24 // Peak is on right side
25 left = mid + 1;
26 }
27 }
28 int peakIndex = left;
29
30 // Lambda function for binary search in ascending or descending part
31 // multiplier: 1 for ascending part, -1 for descending part
32 auto binarySearch = [&](int searchLeft, int searchRight, int multiplier) -> int {
33 while (searchLeft < searchRight) {
34 int mid = searchLeft + (searchRight - searchLeft) / 2;
35 // Use multiplier to handle both ascending and descending cases
36 // For ascending: multiplier = 1, normal comparison
37 // For descending: multiplier = -1, reversed comparison
38 if (multiplier * mountainArr.get(mid) >= multiplier * target) {
39 searchRight = mid;
40 } else {
41 searchLeft = mid + 1;
42 }
43 }
44 // Check if the element at final position equals target
45 return mountainArr.get(searchLeft) == target ? searchLeft : -1;
46 };
47
48 // Step 2: Search in the ascending part (left side of peak)
49 int result = binarySearch(0, peakIndex, 1);
50
51 // Step 3: If not found in ascending part, search in descending part (right side of peak)
52 if (result == -1) {
53 result = binarySearch(peakIndex + 1, arrayLength - 1, -1);
54 }
55
56 return result;
57 }
58};
59
1/**
2 * // This is the MountainArray's API interface.
3 * // You should not implement it, or speculate about its implementation
4 * class MountainArray {
5 * get(index: number): number {}
6 *
7 * length(): number {}
8 * }
9 */
10
11/**
12 * Finds the minimum index of target in a mountain array
13 * A mountain array increases to a peak and then decreases
14 * @param target - The value to search for
15 * @param mountainArr - The mountain array to search in
16 * @returns The minimum index where target is found, or -1 if not found
17 */
18function findInMountainArray(target: number, mountainArr: MountainArray): number {
19 const arrayLength: number = mountainArr.length();
20
21 // Step 1: Find the peak of the mountain array using binary search
22 let left: number = 0;
23 let right: number = arrayLength - 1;
24
25 while (left < right) {
26 const mid: number = Math.floor((left + right) / 2);
27
28 // If mid element is greater than next element, peak is on the left side (including mid)
29 if (mountainArr.get(mid) > mountainArr.get(mid + 1)) {
30 right = mid;
31 } else {
32 // Otherwise, peak is on the right side
33 left = mid + 1;
34 }
35 }
36
37 const peakIndex: number = left;
38
39 /**
40 * Binary search helper function that works for both ascending and descending parts
41 * @param searchLeft - Left boundary of search range
42 * @param searchRight - Right boundary of search range
43 * @param direction - 1 for ascending order, -1 for descending order
44 * @returns Index of target if found, -1 otherwise
45 */
46 const binarySearch = (searchLeft: number, searchRight: number, direction: number): number => {
47 while (searchLeft < searchRight) {
48 const mid: number = Math.floor((searchLeft + searchRight) / 2);
49
50 // Multiply by direction to handle both ascending and descending cases
51 // For ascending (direction=1): normal comparison
52 // For descending (direction=-1): reversed comparison
53 if (direction * mountainArr.get(mid) >= direction * target) {
54 searchRight = mid;
55 } else {
56 searchLeft = mid + 1;
57 }
58 }
59
60 // Check if the element at final position is the target
61 return mountainArr.get(searchLeft) === target ? searchLeft : -1;
62 };
63
64 // Step 2: Search in the ascending part (left side of peak)
65 const resultInAscending: number = binarySearch(0, peakIndex, 1);
66
67 // Step 3: If not found in ascending part, search in descending part (right side of peak)
68 // Only search descending part if target wasn't found in ascending part
69 return resultInAscending !== -1 ? resultInAscending : binarySearch(peakIndex + 1, arrayLength - 1, -1);
70}
71
Time and Space Complexity
Time Complexity: O(log n)
The algorithm consists of three binary search operations:
- First binary search to find the peak element:
O(log n)
where each iteration makes 2 calls tomountain_arr.get()
- Second binary search on the ascending part (left side):
O(log n)
where each iteration makes 1 call tomountain_arr.get()
- Third binary search on the descending part (right side) - only executed if target not found in ascending part:
O(log n)
where each iteration makes 1 call tomountain_arr.get()
Since these searches are performed sequentially (not nested), the total time complexity is O(log n) + O(log n) + O(log n) = O(log n)
.
The total number of API calls to mountain_arr.get()
is at most O(log n)
calls.
Space Complexity: O(1)
The algorithm uses only a constant amount of extra space:
- A few integer variables (
l
,r
,mid
,n
,ans
) - The
search
helper function which doesn't use recursion - The parameter
k
in the search function
No additional data structures are created that scale with input size, making the space complexity constant.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Off-by-One Error When Finding the Peak
A common mistake is incorrectly handling the boundary when finding the peak element. Developers often write:
# WRONG - May miss the peak or cause index out of bounds while l <= r: # Using <= instead of < mid = (l + r) // 2 if mountain_arr.get(mid) > mountain_arr.get(mid + 1): r = mid - 1 # Wrong: might skip the peak else: l = mid + 1
Why it's wrong: Using r = mid - 1
could skip over the peak element entirely. When get(mid) > get(mid + 1)
, mid
could be the peak itself, so we shouldn't exclude it.
Solution: Use while l < r
and set r = mid
(not mid - 1
) to keep the peak in the search range:
# CORRECT while l < r: mid = (l + r) // 2 if mountain_arr.get(mid) > mountain_arr.get(mid + 1): r = mid # Keep mid as potential peak else: l = mid + 1
2. Incorrect Binary Search Boundaries for Descending Portion
Another pitfall is using the wrong starting index when searching the descending portion:
# WRONG - Including peak twice result = binary_search(0, peak_index, 1) # Search ascending if result == -1: result = binary_search(peak_index, array_length - 1, -1) # Wrong: peak_index instead of peak_index + 1
Why it's wrong: The peak element is already checked when searching the ascending portion. Including it again in the descending search wastes an API call and could lead to incorrect results if the binary search implementation doesn't handle edge cases properly.
Solution: Start the descending search from peak_index + 1
:
# CORRECT result = binary_search(0, peak_index, 1) if result == -1: result = binary_search(peak_index + 1, array_length - 1, -1)
3. Forgetting to Check Array Length Edge Cases
Not handling arrays with minimum length (3 elements) properly:
# WRONG - May fail for small arrays
def binary_search(left: int, right: int, ascending: int) -> int:
while left < right:
# ... binary search logic
# Forgot to handle case where left > right initially
Why it's wrong: When searching the descending portion of a 3-element array where the peak is at index 2, the descending search range would be [3, 2]
, making left > right
initially.
Solution: Add a check before or during the search:
# CORRECT
def binary_search(left: int, right: int, ascending: int) -> int:
if left > right: # Handle empty range
return -1
while left < right:
# ... rest of binary search
4. Misunderstanding the Multiplication Trick for Descending Search
Incorrectly implementing the comparison for descending arrays:
# WRONG - Incorrect comparison logic if ascending == 1: if mountain_arr.get(mid) >= target: right = mid else: left = mid + 1 else: # ascending == -1 if mountain_arr.get(mid) <= target: # Wrong logic right = mid else: left = mid + 1
Why it's wrong: This doesn't correctly handle the binary search for descending arrays. In a descending array, when get(mid) <= target
, we should search the left half (larger values), not maintain the same logic as ascending.
Solution: Use the multiplication trick consistently:
# CORRECT if ascending * mountain_arr.get(mid) >= ascending * target: right = mid else: left = mid + 1
This automatically handles both cases:
- Ascending (
ascending = 1
):get(mid) >= target
- Descending (
ascending = -1
):-get(mid) >= -target
→get(mid) <= target
Depth first search is equivalent to which of the tree traversal order?
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!