Facebook Pixel

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 (where 0 < 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 index k (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:

  1. 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 before mid; otherwise, it's after mid.

  2. 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 position mid or earlier
  • If arr[mid] < arr[mid + 1], we're on the ascending side, so the peak must be after position mid

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) with mountain_arr.get(mid + 1)
  • If get(mid) > get(mid + 1), we're on the descending side or at the peak, so move r = 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 search
  • k = -1 for descending portion: reverses the comparison logic

The key insight is k * mountain_arr.get(mid) >= k * target:

  • For ascending (k = 1): this becomes get(mid) >= target
  • For descending (k = -1): this becomes -get(mid) >= -target, which is equivalent to get(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 Evaluator

Example 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 with get(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 with get(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 with get(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 vs 1 * 4 = 4
    • Since 3 < 4, update l = 2
  • l = 2, r = 3
  • mid = 2: 1 * get(2) = 5 vs 1 * 4 = 4
    • Since 5 >= 4, update r = 2
  • Now l = r = 2, check get(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, update r = 5
  • Now l = 4, r = 5
  • mid = 4: -1 * get(4) = -6 vs -1 * 4 = -4
    • Since -6 < -4, update l = 5
  • Now l = r = 5, check get(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:

  1. First binary search to find the peak element: O(log n) where each iteration makes 2 calls to mountain_arr.get()
  2. Second binary search on the ascending part (left side): O(log n) where each iteration makes 1 call to mountain_arr.get()
  3. 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 to mountain_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) >= -targetget(mid) <= target
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Depth first search is equivalent to which of the tree traversal order?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More