LeetCode——162(寻找峰值)

题目链接

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]

题解思路:

通常我们认为二分查找只能用于"有序数组",但这道题数组是无序的。为什么还能用二分?关键在于题目给出的特殊约束和数学性质。

题目中有两个至关重要的条件:

  1. 边界负无穷nums[-1] = nums[n] = -∞。这意味着数组的两端都是"下坡"的起点(从负无穷爬上来,最后又跌回负无穷)。

  2. 相邻不相等nums[i] != nums[i+1]。这保证了波形是严格起伏的,没有平台。

数学推论(介值定理的离散形式): 想象你在爬山。

  • 如果你站在山脚(左边界),往右走是上坡。

  • 如果你一直往右走,只有两种可能:

    1. 一直走到右边界前都在上坡,那么最后一个元素就是峰值(因为右边是负无穷)。

    2. 中间某处开始下坡了。既然之前是上坡,现在变下坡了,那么转折的那个最高点就是峰值。

结论 :只要数组满足上述条件,峰值一定存在 。我们不需要遍历整个数组去找"最高"的那个,只需要找到任意一个局部最高点即可。

传统的二分查找是拿 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 = 3nums[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 = 5nums[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 = 4nums[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。这是一个峰值。


为什么这个算法一定能找到?

这个算法的本质是**"跟随高处走"**。

  • 每次比较 midmid+1,我们总是向数值更大的那一侧移动搜索范围。

  • 因为我们总是往高处走,且数组两端是负无穷,我们最终一定会被逼到一个"左边比它小(或边界),右边也比它小(或边界)"的位置。

  • left == right 时,这个位置就是那个"被包围的高点"。

细节总结

  1. 比较对象 :只比较 midmid+1。不需要看 mid-1。因为只要知道趋势(上坡还是下坡),就能确定峰值在哪一半。

  2. 边界处理:

    • 由于 mid 是通过 (left + right) // 2 计算的,且循环条件是 left < right,所以 mid 永远小于 right

    • 因此 mid + 1 最大等于 right,永远不会越界(不会超过数组长度)。

    • 不需要显式检查 nums[-1]nums[n],逻辑隐含在趋势判断中。

  3. 收敛方式:

    • 上坡: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
            
相关推荐
Sakinol#5 分钟前
Leetcode Hot 100 —— 普通数组
算法·leetcode
@Mike@13 分钟前
【算法】高精度
算法
leo__52014 分钟前
MHT多假设跟踪算法(Multiple Hypothesis Tracking)MATLAB实现
开发语言·算法·matlab
ShineWinsu14 分钟前
对于C++中unordered_set的详细介绍
数据结构·c++·算法·面试·stl·哈希表·unordered_set
吃着火锅x唱着歌14 分钟前
LeetCode 456.132模式
数据结构·算法·leetcode
二木九森17 分钟前
LeetCode-寻找环形链表的入口
算法·leetcode·链表
菜菜小狗的学习笔记23 分钟前
数据结构(三)哈希表
数据结构·散列表
飞Link23 分钟前
耳机连接电脑时调节耳机音量电脑音量也会随着改变
算法·电脑
此方ls30 分钟前
机器学习聚类算法一——K均值
算法·机器学习·聚类
再难也得平31 分钟前
力扣73. 矩阵置零(Java解法)
算法·leetcode·矩阵