Problem overview
The method findPeakElement(nums) receives an array of numbers in which no two neighbouring values are equal. A peak is an element larger than both of its neighbours. The method returns the index of any one peak. The two ends count too: the problem treats the positions before the first element and after the last as minus infinity, so the first element is a peak if it is larger than the second, and the last is a peak if it is larger than the one before it.
In [1, 2, 3, 1] the value 3 at index 2 is the only peak, so the answer is 2. In [1, 2, 1, 3, 5, 6, 4] there are two peaks, 2 at index 1 and 6 at index 5, and either index is a correct answer. The array may hold many peaks, but it always holds at least one.
The lesson also sets a budget: O(log n) time and O(1) extra space. A single scan from left to right would find a peak in O(n), so the budget is telling you to discard half of the array at every step.
Approach: follow the slope uphill
Pick any index middle and look at its neighbours. There are three cases:
- The left neighbour is larger. Walk left from
middle: the values go up while you move left, and they can't go up forever, because the array ends and the value beyond the end counts as minus infinity. So a peak exists somewhere on the left side. - The right neighbour is larger. By the same argument, a peak exists on the right side.
- Neither neighbour is larger. Then
middleis itself a peak.
Because neighbours are never equal, every index falls into one of these cases. When both neighbours are larger, both sides contain a peak and either choice is safe. Each step throws away half of the window, so there are about log2(n) steps. That is binary search, except the thing you search for is not a given value: you search for a place where the slope stops rising.
For [1, 2, 1, 3, 5, 6, 4] the window starts as indices 0 to 6, and the middle is index 3 (value 3). Its left neighbour 1 is smaller and its right neighbour 5 is larger, so the search moves right. The window is now indices 4 to 6 with middle 5 (value 6). Both neighbours are smaller, so index 5 is returned.
Step by step
- Track a window with two indices.
leftstarts at 0 andrightatnums.length - 1, because a peak can be at any index. A singleletdeclares both. - Loop while the window is not empty. The condition is
left <= right. With<=a window holding one index still gets examined, and that one index may be the peak. - Find the middle.
(left + right) >> 1halves the sum and drops the fraction. Plain/ 2would produce an index such as 2.5, because JavaScript numbers are not integers. - Ask whether to go left. A helper,
canGoLeft(nums, i), answers whether a left neighbour at least as large exists. Call it asthis.canGoLeft, since it is a method of the class. - Shrink from the right. If the left side holds a peak, set
right = middle - 1.middleitself can be dropped, because its left neighbour is larger and it can't be a peak. - Otherwise ask whether to go right with
canGoRight, and setleft = middle + 1if so. - Otherwise return
middle. Neither neighbour is larger, which is the definition of a peak. - Return 0 after the loop. The loop always returns before the window empties, so this line is only a safe default. Without it the method would return
undefinedif the loop ever ended. - Write the helpers.
canGoLeftreturnsfalseat index 0, where there is nothing on the left, and otherwise returns whethernums[i - 1] >= nums[i].canGoRightreturnsfalseat the last index and otherwise returns whethernums[i + 1] >= nums[i]. A comparison is already a boolean, so it is returned directly.
The solution in JavaScript
class Solution {
canGoRight(nums, i) {
if (i == nums.length - 1) {
return false
}
return nums[i + 1] >= nums[i]
}
canGoLeft(nums, i) {
if (i == 0) {
return false
}
return nums[i - 1] >= nums[i]
}
findPeakElement(nums) {
let left = 0, right = nums.length - 1
while (left <= right) {
let middle = (left + right) >> 1
if (this.canGoLeft(nums, middle)) {
right = middle - 1
} else if (this.canGoRight(nums, middle)) {
left = middle + 1
} else {
return middle
}
}
return 0
}
}
A few JavaScript details are worth knowing:
- Methods are called through
this. Inside a class, writingcanGoLeft(nums, middle)alone looks for a variable of that name and fails with aReferenceError. Methods are reached withthis.canGoLeft(...). >> 1works on 32-bit integers. The operator converts its operand to a 32-bit integer before shifting, so it also floors the result. That is fine for any array that fits in memory.Math.floor((left + right) / 2)gives the same index and avoids the 32-bit conversion.- The comparison
>=never sees equal neighbours in valid input, so it behaves like>. If equal neighbours were allowed, a plateau could hide the peak and this approach would fail. ==against===. The helpers compare two numbers with==, which is safe here. In general, prefer===.
Complexity
The window halves on every pass, so the loop runs at most about log2(n) times. Each pass does a constant amount of work: one midpoint, and at most two comparisons through the helpers. Time is O(log n). The method keeps only left, right and middle, so extra space is O(1).
Edge cases and common mistakes
- A single element.
[5]returns 0. Both helpers returnfalsethere, because each boundary check fires, so the element is a peak by default. - Two elements.
[1, 2]returns 1, and[2, 1]returns 0. The ends are peaks if they beat their only neighbour. - Strictly rising or falling arrays.
[1, 2, 3, 4, 5]returns 4 and[5, 4, 3, 2, 1]returns 0. The peak sits at an end, which is why the helpers must handle index 0 and the last index instead of reading outside the array. Readingnums[-1]givesundefined, and every comparison withundefinedisfalse, so it would not crash but the logic would rest on a value that does not exist. - Using
left < right. The loop would stop before looking at a window of one index and could miss the answer. - Setting
right = middleinstead ofmiddle - 1. The window may then stop shrinking, and the loop never ends. - Several valid answers. For
[1, 2, 1, 3, 5, 6, 4]this method returns 5. Checking for exactly 1 would reject a correct answer, so test by checking the neighbours of the returned index.
Testing
Try these in the editor:
[1, 2, 3, 1]returns2.[1, 2, 1, 3, 5, 6, 4]returns5.[5]returns0.[1, 2, 3, 4, 5]returns4.[5, 4, 3, 2, 1]returns0.
