852. Peak Index in a Mountain Array
Problem Description
In this problem, we're given an array arr
that represents a mountain, which means it has the shape of a peak: the elements first increase and then decrease. The constraints that define a mountain are:
- The length of the array should be at least 3.
- There exists an index
i
where0 < i < arr.length - 1
, and the elements of the array increase fromarr[0]
toarr[i - 1]
, reach a peak atarr[i]
, and then decrease fromarr[i + 1]
toarr[arr.length - 1]
.
Our goal is to find the peak of the mountain, which is the index i
at the top of the mountain. Importantly, we're asked to find this peak index within a logarithmic time complexity, more precisely O(log(arr.length))
, which suggests that a straightforward linear scan won't be sufficient as it would take linear time.
Intuition
To find the peak index in an efficient way that meets the time complexity requirement of O(log(arr.length))
, we immediately think of the binary search algorithm. Binary search cuts the problem space in half each time it makes a comparison, which results in the logarithmic time complexity.
The intuition behind using binary search for this problem lies in the properties of a mountain array. Since the elements strictly increase up to the peak and then strictly decrease, if we pick a point in the array and look at its immediate neighbor(s), we can decide which half of the array the peak lies in:
- If the current element is greater than its right neighbor, we know that the peak is at this element or to its left.
- Conversely, if the current element is less than its right neighbor, the peak must be to the right of this element.
We can keep narrowing down our search space using these comparisons until we converge on the peak element.
The provided solution in Python performs this algorithm efficiently using a while loop to iterate through the potential peak indices within the boundaries defined by left
and right
. We intentionally start left
at 1 and right
at len(arr) - 2
to ensure we do not consider the endpoints of the array, which cannot be peaks based on the mountain array definition. The use of the bitwise shift operation >> 1
is a common technique to quickly divide mid
by 2, which is a part of the binary search approach to find the middle index between left
and right
.
Learn more about Binary Search patterns.
Solution Approach
The solution to finding the peak index in the mountain array employs the binary search algorithm. Here's a detailed walkthrough of the implementation based on the provided code:
-
We initialize two pointers,
left
andright
, that define the range within which we'll conduct our search. As the peak cannot be the first or last element (by the definition of a mountain array), we setleft
to 1 andright
tolen(arr) - 2
. -
We enter a while loop that will run as long as
left
is less thanright
, ensuring that we consider at least two elements. This is necessary because we compare the element at the midpoint with its immediate right neighbor to determine the direction of the search. -
Within the loop, we calculate the midpoint
mid
using(left + right) >> 1
, which is equivalent to(left + right) / 2
but faster computationally as it is a bit-shift operation that dividesright
andleft
by 2. -
We then perform a comparison between the elements at
arr[mid]
andarr[mid + 1]
. Ifarr[mid]
is greater thanarr[mid + 1]
, this implies we are currently at a descending part of the array, or we may have found the peak. Thus, the peak index must be atmid
or to its left. In this case, we move theright
pointer tomid
to narrow the search range. -
If
arr[mid]
is not greater thanarr[mid + 1]
, it means we're on an ascending part of the array, and the peak lies to the right ofmid
. Consequently, we move theleft
pointer tomid + 1
to adjust the search range. -
The loop continues until
left
andright
converge, which happens whenleft
equalsright
. At this point, both pointers are indicating the peak's index, so we return the value ofleft
.
By repeatedly halving the range of possible indices, we ensure the logarithmic time complexity O(log(arr.length))
, as required by the problem.
One thing to note about this implementation is that it uses a half-interval search. This means we always move one of our boundaries to the midpoint itself, not past it. This approach guarantees that the search space is reduced after each iteration and that we don't overshoot the peak and miss it in our search.
Lastly, the algorithm doesn't use any auxiliary data structures, it operates directly on the input array, which ensures space complexity of O(1)
- only constant extra space is used.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's consider a sample mountain array arr = [1, 3, 5, 4, 2]
to illustrate the solution approach:
-
Initialize two pointers,
left = 1
andright = len(arr) - 2 = 3
, to avoid checking the first and last elements as they cannot be the peak by definition. -
Start the while loop since
left < right
(1 < 3 is true). -
Calculate the midpoint
mid
using(left + right) >> 1
. For our example, it's(1 + 3) >> 1
, which equals 2. -
Compare
arr[mid]
toarr[mid + 1]
. Formid = 2
,arr[mid]
is 5 andarr[mid + 1]
is 4. Because 5 > 4, we updateright
tomid
. Nowleft = 1
andright = 2
. -
The loop continues because
left < right
is still true. -
Recalculate the midpoint with the updated pointers. Now,
mid
is(1 + 2) >> 1
, which equals 1. -
Compare
arr[mid]
toarr[mid + 1]
again. Formid = 1
,arr[mid]
is 3 andarr[mid + 1]
is 5. Because 3 < 5, we updateleft
tomid + 1
. Nowleft = 2
andright = 2
. -
The loop ends because
left
equalsright
, indicating convergence at the peak's index, which is2
in our example array. -
Return
left
, which is the peak index. In our array,arr[2]
is indeed the peak with a value of 5.
Through each iteration, the search space is narrowed down until the peak is found, fulfilling the O(log(arr.length))
time complexity for this logarithmic search approach.
Solution Implementation
1class Solution:
2 def peakIndexInMountainArray(self, arr: List[int]) -> int:
3 # Starting the search from second element to second last element
4 # because the peak can't be the first or last element.
5 left_index, right_index = 1, len(arr) - 2
6
7 # Use binary search to find the peak element
8 while left_index < right_index:
9 # Calculate the middle index of the current subarray
10 mid_index = (left_index + right_index) // 2 # Use '//' for floor division in Python 3
11
12 # If the middle element is greater than its next element,
13 # then the peak is in the left side of mid. Update the right_index.
14 if arr[mid_index] > arr[mid_index + 1]:
15 right_index = mid_index
16 else:
17 # If the middle element is less than its next element,
18 # then the peak is in the right side of mid. Update the left_index.
19 left_index = mid_index + 1
20
21 # When left_index==right_index, we have found the peak index
22 return left_index
23
1class Solution {
2 public int peakIndexInMountainArray(int[] arr) {
3 // Initialize left and right pointers, excluding the first and last elements
4 // because the peak cannot be at the ends of the array.
5 int left = 1;
6 int right = arr.length - 2;
7
8 // Perform a binary search to find the peak element
9 while (left < right) {
10 // Calculate the middle index
11 int mid = left + (right - left) / 2;
12
13 // If the middle element is greater than its successor,
14 // the peak is in the left half, including the mid element
15 if (arr[mid] > arr[mid + 1]) {
16 right = mid;
17 } else {
18 // If the middle element is less than or equal to its successor,
19 // the peak is in the right half
20 left = mid + 1;
21 }
22 }
23
24 // When left equals right, we have found the peak index
25 return left;
26 }
27}
28
1#include <vector>
2
3class Solution {
4public:
5 // Function to find the peak index in a mountain array
6 int peakIndexInMountainArray(vector<int>& arr) {
7 // Initialize the search range within the array
8 int left = 1; // Starting from 1 because the peak cannot be the first element
9 int right = arr.size() - 2; // Ending at size - 2 because the peak cannot be the last element
10
11 // Continue searching as long as the left index is less than the right index
12 while (left < right) {
13 // Calculate the middle index using bitwise right shift (equivalent to dividing by 2)
14 int mid = (left + right) >> 1;
15
16 // If the middle element is greater than its next element,
17 // we are in the descending part of the mountain
18 // therefore, we continue to search on the left side
19 if (arr[mid] > arr[mid + 1]) {
20 right = mid;
21 } else {
22 // Else, we are in the ascending part of the mountain,
23 // we continue to search on the right side
24 left = mid + 1;
25 }
26 }
27
28 // Since 'left' and 'right' converge to the peak index,
29 // we return 'left' as the peak index of the mountain array
30 return left;
31 }
32};
33
1function peakIndexInMountainArray(arr: number[]): number {
2 // Initialize the search range within the boundaries of the potential peak.
3 // We avoid the first and last elements as they cannot be the peak.
4 let left = 1;
5 let right = arr.length - 2;
6
7 // Use binary search to find the peak of the mountain array.
8 while (left < right) {
9 // Calculate the mid index by shifting right bitwise by 1 (equivalent to dividing by 2).
10 const mid = left + ((right - left) >> 1);
11
12 // If the mid element is greater than its next element, we continue in the left portion.
13 // This is because the peak must be to the left of mid or at mid.
14 if (arr[mid] > arr[mid + 1]) {
15 right = mid;
16 } else {
17 // Otherwise, the peak lies to the right of mid, so we continue searching in the right portion.
18 left = mid + 1;
19 }
20 }
21 // After the loop, left will be pointing at the peak element's index
22 return left;
23}
24
Time and Space Complexity
The given Python code performs a binary search to find the peak index in a mountain array. The complexity analysis is:
-
Time Complexity: The
while
loop keeps halving the search interval untilleft
andright
meet. This halving process continues for at mostO(log n)
iterations, wheren
is the length of the array. Hence, the time complexity of this code isO(log n)
. -
Space Complexity: The space complexity is
O(1)
because the code uses a fixed amount of additional memory (a few variables for left, right, and mid indices) regardless of the input array size.
Learn more about how to find time and space complexity quickly using problem constraints.
What's the output of running the following function using input 56
?
1KEYBOARD = {
2 '2': 'abc',
3 '3': 'def',
4 '4': 'ghi',
5 '5': 'jkl',
6 '6': 'mno',
7 '7': 'pqrs',
8 '8': 'tuv',
9 '9': 'wxyz',
10}
11
12def letter_combinations_of_phone_number(digits):
13 def dfs(path, res):
14 if len(path) == len(digits):
15 res.append(''.join(path))
16 return
17
18 next_number = digits[len(path)]
19 for letter in KEYBOARD[next_number]:
20 path.append(letter)
21 dfs(path, res)
22 path.pop()
23
24 res = []
25 dfs([], res)
26 return res
27
1private static final Map<Character, char[]> KEYBOARD = Map.of(
2 '2', "abc".toCharArray(),
3 '3', "def".toCharArray(),
4 '4', "ghi".toCharArray(),
5 '5', "jkl".toCharArray(),
6 '6', "mno".toCharArray(),
7 '7', "pqrs".toCharArray(),
8 '8', "tuv".toCharArray(),
9 '9', "wxyz".toCharArray()
10);
11
12public static List<String> letterCombinationsOfPhoneNumber(String digits) {
13 List<String> res = new ArrayList<>();
14 dfs(new StringBuilder(), res, digits.toCharArray());
15 return res;
16}
17
18private static void dfs(StringBuilder path, List<String> res, char[] digits) {
19 if (path.length() == digits.length) {
20 res.add(path.toString());
21 return;
22 }
23 char next_digit = digits[path.length()];
24 for (char letter : KEYBOARD.get(next_digit)) {
25 path.append(letter);
26 dfs(path, res, digits);
27 path.deleteCharAt(path.length() - 1);
28 }
29}
30
1const KEYBOARD = {
2 '2': 'abc',
3 '3': 'def',
4 '4': 'ghi',
5 '5': 'jkl',
6 '6': 'mno',
7 '7': 'pqrs',
8 '8': 'tuv',
9 '9': 'wxyz',
10}
11
12function letter_combinations_of_phone_number(digits) {
13 let res = [];
14 dfs(digits, [], res);
15 return res;
16}
17
18function dfs(digits, path, res) {
19 if (path.length === digits.length) {
20 res.push(path.join(''));
21 return;
22 }
23 let next_number = digits.charAt(path.length);
24 for (let letter of KEYBOARD[next_number]) {
25 path.push(letter);
26 dfs(digits, path, res);
27 path.pop();
28 }
29}
30
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
LeetCode 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 we
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 algomonster s3 us east 2 amazonaws com recursion jpg You first
Want a Structured Path to Master System Design Too? Don’t Miss This!