Start learning free

LESSON 375 OF 671 · BINARY SEARCH

Find Peak Element in JavaScript: O(log n) Binary Search

Practice O(log n) Read the full explanation

You get an array of integers nums. No two neighbors are equal.

Return the index of a peak. A peak is a value bigger than the values right next to it.

If there are several peaks, the index of any one of them is a correct answer.

The spots just outside the array count as lower than every value. So the first value is a peak if it is bigger than the second. The last value is a peak if it is bigger than the one before it.

 

Example 1:

Input: nums = [2,5,9,4]
Output: 2
Explanation: 9 is at index 2.
It is bigger than 5 on its left and 4 on its right.

Example 2:

Input: nums = [3,8,6,1,7,2]
Output: 1
Explanation: 8 at index 1 is a peak.
7 at index 4 is a peak too.
So returning 4 is also correct.

Example 3:

Input: nums = [10,7,3]
Output: 0
Explanation: The values only go down.
The spot before index 0 counts as lower than 10.
So 10, at index 0, is the peak.

 

Constraints:

  • 2 to 1,000,000 numbers.
  • Each number is from 1 to 231 - 1.
  • Two neighbors are never equal.

Note:

Your algorithm should run in O(log n) time and use O(1) extra space.


STEP 1 OF 15
    Ctrl + Enter to check

    JAVASCRIPT · BINARY SEARCH

    Find Peak in JavaScript, explained

    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 middle is 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

    1. Track a window with two indices. left starts at 0 and right at nums.length - 1, because a peak can be at any index. A single let declares both.
    2. 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.
    3. Find the middle. (left + right) >> 1 halves the sum and drops the fraction. Plain / 2 would produce an index such as 2.5, because JavaScript numbers are not integers.
    4. Ask whether to go left. A helper, canGoLeft(nums, i), answers whether a left neighbour at least as large exists. Call it as this.canGoLeft, since it is a method of the class.
    5. Shrink from the right. If the left side holds a peak, set right = middle - 1. middle itself can be dropped, because its left neighbour is larger and it can't be a peak.
    6. Otherwise ask whether to go right with canGoRight, and set left = middle + 1 if so.
    7. Otherwise return middle. Neither neighbour is larger, which is the definition of a peak.
    8. 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 undefined if the loop ever ended.
    9. Write the helpers. canGoLeft returns false at index 0, where there is nothing on the left, and otherwise returns whether nums[i - 1] >= nums[i]. canGoRight returns false at the last index and otherwise returns whether nums[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, writing canGoLeft(nums, middle) alone looks for a variable of that name and fails with a ReferenceError. Methods are reached with this.canGoLeft(...).
    • >> 1 works 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 return false there, 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. Reading nums[-1] gives undefined, and every comparison with undefined is false, 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 = middle instead of middle - 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] returns 2.
    • [1, 2, 1, 3, 5, 6, 4] returns 5.
    • [5] returns 0.
    • [1, 2, 3, 4, 5] returns 4.
    • [5, 4, 3, 2, 1] returns 0.
    Keep going with JavaScript650+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free