题目链接
https://leetcode.cn/problems/find-peak-element
题目描述
峰值元素是指其值严格大于左右相邻值的元素。
给你一个整数数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。
你可以假设 nums[-1] = nums[n] = -∞ 。
你必须实现时间复杂度为 O(log n) 的算法来解决此问题。
示例 1:
输入:nums = [1,2,3,1]
输出:2
解释:3 是峰值元素,你的函数应该返回其索引 2。
示例 2:
输入:nums = [1,2,1,3,5,6,4]
输出:1 或 5
解释:你的函数可以返回索引 1,其峰值元素为 2;
或者返回索引 5, 其峰值元素为 6。
提示:
-
1 <= nums.length <= 1000 -
-231 <= nums[i] <= 231 - 1 -
对于所有有效的
i都有nums[i] != nums[i + 1]
题解思路:
通常我们认为二分查找只能用于"有序数组",但这道题数组是无序的。为什么还能用二分?关键在于题目给出的特殊约束和数学性质。
题目中有两个至关重要的条件:
-
边界负无穷 :
nums[-1] = nums[n] = -∞。这意味着数组的两端都是"下坡"的起点(从负无穷爬上来,最后又跌回负无穷)。 -
相邻不相等 :
nums[i] != nums[i+1]。这保证了波形是严格起伏的,没有平台。
数学推论(介值定理的离散形式): 想象你在爬山。
-
如果你站在山脚(左边界),往右走是上坡。
-
如果你一直往右走,只有两种可能:
-
一直走到右边界前都在上坡,那么最后一个元素就是峰值(因为右边是负无穷)。
-
中间某处开始下坡了。既然之前是上坡,现在变下坡了,那么转折的那个最高点就是峰值。
-
结论 :只要数组满足上述条件,峰值一定存在 。我们不需要遍历整个数组去找"最高"的那个,只需要找到任意一个局部最高点即可。
传统的二分查找是拿 nums[mid] 和 target 比。 这里的二分查找是拿 nums[mid] 和 邻居 nums[mid+1] 比,以此判断趋势。
我们将搜索区间定义为 [left, right]。取中点 mid。
情况 A:上坡趋势 (nums[mid] < nums[mid+1])
-
现象 :当前位置
mid比右边低。说明我们在一个"上坡"的路上。 -
推断:
-
既然右边更高,那么峰值一定在
mid的右侧。 -
哪怕右边紧接着就下坡(那
mid+1就是峰值),或者右边继续上坡再下坡,峰值肯定在(mid, right]这个范围内。 -
关键点 :
mid本身绝不可能是峰值(因为它右边比它大)。
-
-
操作:收缩左边界,排除 mid
left = mid + 1
情况 B:下坡趋势 (nums[mid] > nums[mid+1])
-
现象 :当前位置
mid比右边高。说明我们在一个"下坡"的路上,或者mid本身就是山顶。 -
推断:
-
峰值一定在
mid的左侧 (包含mid自己)。 -
为什么?因为左边要么是持续上坡直到
mid(那mid就是峰值),要么左边也有起伏,但无论如何,在(-∞, ..., mid]这段区间里,既然mid比右边高,且最左边是-∞,根据连续性,左边必然有一个最高点。 -
关键点 :
mid有可能就是峰值,所以我们不能排除它。
-
-
操作:收缩右边界,保留 mid
right = mid(注意这里不是mid-1)
算法执行流程演示
假设数组 nums = [1, 2, 1, 3, 5, 6, 4],我们要找峰值。
-
初始状态 :
left = 0,right = 6 -
第 1 轮:
-
mid = (0 + 6) // 2 = 3。nums[3] = 3。 -
比较
nums[3]和nums[4](即 3 和 5)。 -
3 < 5(上坡)。 -
决策:峰值在右边。
left = mid + 1 = 4。 -
新区间:
[4, 6](对应元素[5, 6, 4])。
-
-
第 2 轮:
-
left = 4,right = 6。 -
mid = (4 + 6) // 2 = 5。nums[5] = 6。 -
比较
nums[5]和nums[6](即 6 和 4)。 -
6 > 4(下坡)。 -
决策:峰值在左边(含 mid)。
right = mid = 5。 -
新区间:
[4, 5](对应元素[5, 6])。
-
-
第 3 轮:
-
left = 4,right = 5。 -
mid = (4 + 5) // 2 = 4。nums[4] = 5。 -
比较
nums[4]和nums[5](即 5 和 6)。 -
5 < 6(上坡)。 -
决策:峰值在右边。
left = mid + 1 = 5。 -
新区间:
[5, 5]。
-
-
终止:
-
此时
left == right(都等于 5)。 -
循环结束 (
while left < right不成立)。 -
返回
left(索引 5),对应的值是 6。这是一个峰值。
-
为什么这个算法一定能找到?
这个算法的本质是**"跟随高处走"**。
-
每次比较
mid和mid+1,我们总是向数值更大的那一侧移动搜索范围。 -
因为我们总是往高处走,且数组两端是负无穷,我们最终一定会被逼到一个"左边比它小(或边界),右边也比它小(或边界)"的位置。
-
当
left == right时,这个位置就是那个"被包围的高点"。
细节总结
-
比较对象 :只比较
mid和mid+1。不需要看mid-1。因为只要知道趋势(上坡还是下坡),就能确定峰值在哪一半。 -
边界处理:
-
由于
mid是通过(left + right) // 2计算的,且循环条件是left < right,所以mid永远小于right。 -
因此
mid + 1最大等于right,永远不会越界(不会超过数组长度)。 -
不需要显式检查
nums[-1]或nums[n],逻辑隐含在趋势判断中。
-
-
收敛方式:
-
上坡:
left = mid + 1(抛弃mid,因为它小)。 -
下坡:
right = mid(保留mid,因为它可能是最大的)。 -
这种不对称的收缩(一个+1,一个不减)配合
<循环条件,保证了不会死循环且最终收敛到唯一解。
-
class Solution:
def findPeakElement(self, nums: List[int]) -> int:
left = 0
right = len(nums) - 1
while left < right:
mid = (left + right)//2
if nums[mid] < nums[mid + 1]:
left = mid + 1
else:
right = mid
return left