LeetCode #162 Medium

Find Peak Element

A peak is an element strictly greater than its neighbours. Given an unsorted array where nums[i] != nums[i+1], return the index of any peak in O(log n). Treat out-of-bounds neighbours as negative infinity.

Constraints
  • 1 <= nums.length <= 1000
  • -2³¹ <= nums[i] <= 2³¹ - 1
  • nums[i] != nums[i + 1] for all valid i.
binary-searcharray
Open on LeetCode ↗
02

Intuition

Find peak element returns the index of any element strictly greater than both neighbours, in O(log n). That an unsorted array admits a logarithmic search is the surprising part, and the reason is worth understanding. Binary search normally needs sorted data. Here the enabling property is different — the problem states that nums[i] != nums[i+1] for all i, and that out-of-bounds neighbours count as negative infinity: - Comparing nums[mid] with nums[mid + 1] reveals a slope, and a slope always leads to a peak in that direction. If nums[mid] < nums[mid + 1], the sequence is ascending at mid. Following it rightward, it either keeps rising until the array ends — where the boundary acts as negative infinity, making the last element a peak — or it turns downward at some point, which is a peak. Either way a peak exists to the right, so the left half can be discarded. If nums[mid] > nums[mid + 1], the same argument applies leftward, with mid itself still a candidate. That guarantee is what makes the halving valid without sorted input. The loop runs while left < right, and the descending case sets right = mid rather than mid - 1, since mid may be the peak. There is no equality case to handle, because adjacent elements are never equal. Any peak is acceptable, which is why finding a peak rather than the maximum is enough — and it is why the O(n) scan for the global maximum, while correct, does more work than asked.

How to spot this pattern

Binary search without a sorted array. Comparing nums[mid] to its right neighbour tells you which side must contain a peak: if the slope rises, a peak exists to the right; if it falls, one exists at or left of mid. The out-of-bounds -∞ convention guarantees a peak always exists.

03

Approach

Try it first

Before reading on: price up what the direct approach costs here, then ask what property lets you throw away half the range after one comparison. Aim for O(log n) time and O(1) space.

1

See why binary search applies

The array is unsorted, but adjacent elements are never equal and boundaries act as negative infinity. Those two facts, not sorting, are what enable the halving.

2

Compare mid with its right neighbour

Test nums[mid] against nums[mid + 1]. This single comparison reveals a slope, which is all the direction information the search needs.

3

Follow an ascending slope right

If nums[mid] < nums[mid + 1], the sequence rises. It either rises to the array's end — a peak by the boundary rule — or turns downward, also a peak. Either way a peak lies right, so set left = mid + 1.

4

Follow a descending slope left

If nums[mid] > nums[mid + 1], a peak lies at or left of mid. Set right = mid, not mid - 1, since mid itself may be the peak.

5

Skip the equality case

Adjacent elements are guaranteed distinct, so no third branch is needed. This is why the search never stalls.

6

Return either converged index

The loop ends when left and right meet, and that index is a peak. Any peak is acceptable — the problem does not ask for the global maximum.

7

Cost of the search

The range halves each step, giving O(log n) time and O(1) space — better than the O(n) scan that finding the global maximum would require.

04

Solution & live demo

▶1class Solution:
▶2 def findPeakElement(self, nums):
▶3 lo, hi = 0, len(nums) - 1
▶4 while lo < hi:
▶5 mid = (lo + hi) // 2
▶6 if nums[mid] < nums[mid + 1]:
▶7 lo = mid + 1
▶8 else:
▶9 hi = mid
▶10 return lo
05

Common pitfalls

Setting hi to mid - 1 on a descent

✗ Wrong
else:
    hi = mid - 1
✓ Right
else:
    hi = mid

When nums[mid] > nums[mid + 1], mid itself may be the peak. Excluding it can discard the only answer in that half and the search converges on a non-peak.

Comparing with both neighbours

✗ Wrong
if nums[mid-1] < nums[mid] > nums[mid+1]: return mid
✓ Right
if nums[mid] < nums[mid + 1]:

Needs bounds guards on both sides and doesn't help the search decide where to go when the test fails. The single right-neighbour comparison always determines a half that must contain a peak.

Scanning linearly

✗ Wrong
for i in range(n):
    if is_peak(i): return i
✓ Right
while lo < hi:

Correct but O(n), and the problem asks for O(log n). The slope argument makes half the array discardable at every step even though nothing is sorted.

06

Edge cases

Array of length 1

The loop never runs and index 0 is returned — correct, since both neighbours are negative infinity.

Strictly increasing array

Every comparison takes the ascending branch, driving lo to the last index, which is the peak.

Strictly decreasing array

Every comparison takes the descending branch, driving hi to 0, which is the peak.

Multiple peaks

Any one is acceptable. The search commits to whichever half it enters and returns the peak it finds there.

07

Complexity

Time
O(log n)
Space
O(1)
Each comparison discards half the remaining window. A linear scan is O(n) and would fail the stated requirement.