寻找峰值:往更大的一侧爬
相邻元素不同,边界外是 −∞。mid 比右边矮就往右走,比右边高就往左收,收敛处左右都无法再升。
题目保证相邻元素不同,且数组两端外侧视为负无穷。二分比较 nums[mid] 与 nums[mid+1]:若 nums[mid] < nums[mid+1],正在上坡,峰值在右侧,left = mid+1;否则峰值在左侧(含 mid),right = mid。left == right 时即一个峰值下标。时间 O(log n),空间 O(1)。
这是 LeetCode 162. Find Peak Element。峰值定义为严格大于左右邻居的元素;下标 0 的左邻居、最后一格的右邻居视为负无穷。数组相邻元素不相等,返回任意一个峰值的下标。要求 O(log n)。
主例 nums = [1, 2, 1, 3, 5, 6, 4]。下标 1 的 2、下标 5 的 6 都是峰值,返回哪个都行。演示沿上坡爬到下标 5。单元素数组它自己就是峰。
扫一遍找局部最大能做对,但题目要对数时间。普通二分需要数组整体有序,这里没有。缺的是:只要知道哪一侧还在升高,那一侧就一定还能碰到峰——边界是 −∞,升上去之后终究要落下来,或者一直升到端点,端点对外也是峰。
比较相邻,判断坡向
把数组看成地形。峰值是上坡转下坡的顶点,端点只要比唯一的邻居高也算。第一直觉找全局最大:全局最大一定是峰,但要扫完全部。题目允许任意一个峰,于是可以丢掉半边。
取 mid,只看 nums[mid] 和 nums[mid+1] 这一对(相邻不同,不会相等)。mid 更矮:从 mid 到 mid+1 在上坡,右边至少还能升一格;右边的尽头若一直升,最后一格对外是 −∞,它就是峰;中途若下降,下降前必有峰。因此峰值在 mid 右边,left = mid+1。mid 更高:要么 mid 自己是峰,要么峰在左边,right = mid,不能抛掉 mid。
循环用 left < right,停在一点。边界视为 −∞ 保证整个数组「从负无穷来、到负无穷去」,至少有一个峰,算法不会空转。
用手走主例,对应四帧。left=0、right=6、mid=3,nums[3]=3 < nums[4]=5,上坡,left=4。第二刀 left=4、right=6、mid=5,6>4,下坡,right=5。第三刀 left=4、right=5、mid=4,5<6,上坡,left=5。left==right==5,峰值 6。同一套比较也会在别的输入里停在下标 1 的 2,两种返回都合法。
上坡必有峰上升的那一侧,不是继续升到端点(端点对外是峰),就是中途下降(下降前是峰)。这是丢掉另一半仍然安全的理由。
六行 Go:二分找峰
func findPeakElement(nums []int) int {l, r := 0, len(nums)-1for l < r {m := l + (r-l)/2if nums[m] < nums[m+1] { l = m + 1 } else { r = m }}return l}
1l<r 时区间至少两格,m+1 不会越界。停时 l==r,那一格就是峰。
2nums[m]<nums[m+1]:上坡,峰在右边,l=m+1。主例第一刀、第三刀走这里。
3否则峰在左边含 m,r=m。主例第二刀 6>4 走这里。不要写成 r=m−1,会丢掉自己就是峰的 mid。
4返回 l。单元素数组循环不进,下标 0 对外两侧都是 −∞,是峰。
总结
矮就往右爬,高就往左收,停下来的格子左右都不能再升。主例爬到 6。
- 不要找全局最大。主例 2 也是合法峰值,题目只要一个。
- 边界是 −∞,上坡的那一侧必然还有峰,所以可以丢掉另一半。
- 和 LC153 一样用 l<r 且 r=m:不能把 mid 从「可能是答案」的那一侧扔掉。