Start learning free

LESSON 279 OF 414 · BINARY SEARCH

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

Practice O(log n) Read the full explanation

A peak element is an element that is greater than its neighbors.

Given an input array nums, where nums[i] ≠ nums[i+1], find a peak element and return its index.

The array may contain multiple peaks, in that case return the index to any one of the peaks is fine.

You may imagine that nums[-1] = nums[n] = -∞.

Example 1:

Input: nums = [1, 2, 3, 1]
Output: 2
Explanation: 3 is a peak element and your function should return the index number 2.

Example 2:

Input: nums = [1, 2, 1, 3, 5, 6, 4]
Output: 1 or 5 
Explanation: Your function can return either index number 1 where the peak element is 2, 
             or index number 5 where the peak element is 6.

Note:

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


STEP 1 OF 14
    Ctrl + Enter to check

    RUST · BINARY SEARCH

    Find Peak in Rust, explained

    Problem overview

    A peak is an element greater than its neighbours. The function find_peak_element receives a vector nums in which no two adjacent elements are equal, and returns the index of any peak. The ends count as having a neighbour of negative infinity, so the first element is a peak when it is larger than the second, and the last is a peak when it is larger than the one before it.

    For [1, 2, 3, 1] the answer is 2, since 3 beats both neighbours. For [1, 2, 1, 3, 5, 6, 4] both index 1 (the value 2) and index 5 (the value 6) are peaks, and either one is accepted.

    The statement asks for O(log n) time and O(1) extra space. The vector isn't sorted, but binary search still works, for the reason below.

    Approach: always walk uphill

    A scan that checks every element is O(n). To do better, notice what the neighbours of the middle element tell you. Take the element at middle:

    • If a neighbour is larger, there is a peak on that side. Keep walking in the direction of larger values: either the values keep rising until the end of the vector, where the imaginary negative infinity makes the last element a peak, or they stop rising at some element that is larger than both of its neighbours.
    • If neither neighbour is larger, middle itself is a peak.

    So one comparison tells you which half is guaranteed to contain a peak, and the other half can be discarded. Repeating that is a binary search over the indexes, and it needs no sorting.

    On [1, 2, 1, 3, 5, 6, 4]: the range is indexes 0 to 6 and the middle is index 3, holding 3. Its left neighbour, 1, is smaller, and its right neighbour, 5, is larger, so the search moves right: indexes 4 to 6. The middle is index 5, holding 6, and both neighbours (5 and 4) are smaller, so the function returns 5.

    Step by step

    1. Record the length in n. It is a usize, and it is reused for the bounds and the neighbour checks.
    2. Set left to 0i32. The suffix makes it a signed i32, the type of the result, and subtractions such as middle - 1 can't underflow the way a usize could.
    3. Set right to n as i32 - 1, the last index, because a peak may sit at the very end.
    4. Loop while left <= right, so the window still holds at least one index.
    5. Compute middle as left + (right - left) / 2.
    6. Convert it to m, a usize, for indexing.
    7. Decide whether to go left. can_go_left is true when m > 0 and nums[m - 1] >= nums[m]. The bounds check comes first, and && stops evaluating when it is false, so nums[m - 1] is never read for m = 0.
    8. Decide whether to go right the same way: m + 1 < n and nums[m + 1] >= nums[m].
    9. If going left, discard the middle and everything to its right: right = middle - 1.
    10. Else if going right, set left = middle + 1.
    11. Otherwise neither neighbour is larger, so return middle;.
    12. Fall back to 0 as the final expression if the loop ever ends without a return.

    The solution in Rust

    impl Solution {
        fn find_peak_element(nums: Vec<i32>) -> i32 {
            let n = nums.len();
            let mut left = 0i32;
            let mut right = n as i32 - 1;
            while left <= right {
                let middle = left + (right - left) / 2;
                let m = middle as usize;
                let can_go_left = m > 0 && nums[m - 1] >= nums[m];
                let can_go_right = m + 1 < n && nums[m + 1] >= nums[m];
                if can_go_left {
                    right = middle - 1;
                } else if can_go_right {
                    left = middle + 1;
                } else {
                    return middle;
                }
            }
            0
        }
    }
    

    Rust details worth a look:

    • Two integer types. n and m are usize, which Rust requires for indexing and which len() returns. left, right and middle are i32, the function's return type. Rust never mixes them in one expression, so the code converts with as in two places.
    • Short-circuit evaluation. In m > 0 && nums[m - 1] >= nums[m] the right side runs only if the left side is true. Without that guard, m - 1 would underflow for m = 0 and the index would be out of range.
    • Out-of-bounds indexing panics. Reading nums[m + 1] past the end stops the program, which is why m + 1 < n comes first.
    • Early return. return middle; exits from inside the loop; the final bare 0 is the value of the function when the loop ends normally.

    Complexity

    Time: O(log n). Every iteration either returns or discards the middle and one side, which is at least half of the window, so there are about log₂ n iterations of constant work.

    Space: O(1). A handful of integers are kept.

    Edge cases and common mistakes

    • One element. [5] returns 0: both guards are false, so the single element is a peak.
    • Increasing and decreasing vectors. [1, 2, 3, 4] returns 3 and [4, 3, 2, 1] returns 0, thanks to the negative-infinity ends.
    • Two elements. [1, 3] returns 1 and [3, 1] returns 0.
    • Several peaks. [1, 2, 1, 3, 5, 6, 4] returns 5 here, but 1 would be accepted too; don't write a test that expects a specific peak when there are several.
    • Reading neighbours without a bounds check. This panics at the ends of the vector.
    • Comparing with the ends' real neighbours only. Treating the vector's end as a wall, rather than as negative infinity, would hide the peaks at index 0 and the last index.
    • Empty vector. The loop doesn't run and the function returns 0, even though no element exists. The problem assumes at least one element.

    Testing

    • [1, 2, 3, 1] gives 2.
    • [1, 2, 1, 3, 5, 6, 4] gives 5 (or 1, both are peaks).
    • [5] gives 0.
    • [1, 2, 3, 4] gives 3.
    • [4, 3, 2, 1] gives 0.
    Keep going with RustAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free