LeetCode 852:山脉数组的峰顶索引(二分查找) —— 题解

👋 欢迎阅读

🎯 欢迎来到「山脉数组的峰顶索引」题解之旅! 本文将带你从"翻越一座单峰山脉找最高点"这一直观场景出发,深入理解二分查找二段性 的精妙运用,并掌握如何比较相邻元素判断上升/下降定位峰顶下标

在开始之前,建议你先:

  • 了解题目背景 :这是 LeetCode 852 题,给定山脉数组 (先严格递增后严格递减,无重复),返回峰顶元素 的下标。本质上,数组具有二段性 ------左半满足"后一个更大",右半不满足,问题转化为二分找到二段性的分界点

  • 明确学习目标 :掌握相邻元素比较式二分 ,理解二段性判据上升/下降段的划分 ,并熟练处理峰顶在边界等边界情况。

  • 准备好环境 :建议在本地 IDE 或 LeetCode 在线编辑器中打开代码,边看边运行,亲手验证示例(如 arr = [0,2,1,0] 输出 1arr = [0,10,5,2] 输出 1)。

本文将从问题转化、相邻比较、区间收缩、返回结果 到代码实现,层层递进。即使你对二段性二分 还不熟悉,我们也会从"哪边在上升就往哪边走"这一直觉出发,让你轻松抓住核心思想 ------比较邻居,向高处收缩。现在,让我们一起二分登顶,找到山脉的最高点吧! ⛰️🎯

一.题目

852. 山脉数组的峰顶索引 - 力扣(LeetCode)

二.做题思路

一、问题分析(前置分析)

  • 题目要求:在山脉数组 (先严格递增后严格递减,长度 ≥ 3,无重复)中返回峰顶元素下标。
  • 关键约束:存在唯一的峰顶arr[0] < arr[1]arr[n-2] > arr[n-1](峰顶不在两端);元素无重复
  • 核心思路:利用二段性 ------峰顶左侧满足 arr[i] < arr[i+1](上升段),右侧满足 arr[i] > arr[i+1](下降段),二分找分界点。

二、算法策略(二段性二分 · 相邻比较)

核心步骤:

  1. 初始化区间left = 0right = n - 1
  2. 二分收敛while (left < right)mid 下取整left + (right - left) / 2)。
  3. 相邻比较
    • arr[mid] < arr[mid + 1] → mid 在上升段 ,峰顶在右半,left = mid + 1
    • arr[mid] > arr[mid + 1] → mid 在下降段 ,峰顶在左半(含 mid),right = mid
  4. 返回 :循环结束后 left == right 即峰顶下标。

示例执行过程arr = [0,2,1,0]):

阶段 left right mid arrmid vs arrmid+1 操作 结果
0 3 1 2 < 1 否 → 2 > 1 下降段,收缩右侧 right=1
0 1 0 0 < 2 上升段,收缩左侧 left=1
收敛 1 1 --- --- 返回 1 1

arr = [0,10,5,2]:mid=1,10 > 5 → right=1;mid=0,0 < 10 → left=1;收敛于 1,返回 1。均与题目一致。

三、正确性说明(简单版本)

  • 二段性判据可靠 :山脉数组任意相邻对 (arr[i], arr[i+1]) 只有两种状态------上升 (峰顶左侧)或下降 (峰顶右侧),且状态单调变化一次 ,判据 arr[mid] < arr[mid+1] 恰好识别二段性,不会误判
  • 收缩方向正确 :上升段丢弃左半(含 mid),下降段保留 mid 向左收敛,峰顶始终在区间内,单调收敛到唯一峰顶不漏解
  • 终止性left = mid + 1right = mid(下取整保证 mid < right)均严格缩小,不会死循环
  • 无需校验 :题目保证山脉结构,收敛点必为峰顶,无需事后比较。

四、实现细节(边界防护)

  • 初始化:left = 0right = (int)arr.size() - 1
  • 边界防护:题目保证 n >= 3 且峰顶不在两端,arr[mid+1] 访问安全(mid < right 恒成立);n == 1 的退化输入会直接返回 0(虽然题目不要求)。
  • 复杂度:时间 O(log n) (每次排除一半区间),空间 O(1)(仅常数个变量)。
  • 关键判断if (arr[mid] < arr[mid + 1]) left = mid + 1; else right = mid;(二段性收敛)、while (left < right)(循环边界)。

五、返回值(目标映射)

  • 返回 left峰顶下标,对应题目"返回峰顶元素的索引"。

三.代码

cpp 复制代码
class Solution
{
public:
    int peakIndexInMountainArray(vector<int>& arr)
    {
        int left = 0;                   // 区间左端点
        int right = (int)arr.size() - 1; // 区间右端点

        // 1. 二段性二分:比较相邻元素,判断 mid 在上升段还是下降段
        while (left < right)
        {
            int mid = left + (right - left) / 2;    // mid 下取整,配合 right = mid

            if (arr[mid] < arr[mid + 1])
            {
                left = mid + 1;         // 上升段:峰顶在右半,丢弃左半(含 mid)
            }
            else
            {
                right = mid;            // 下降段:峰顶在左半(含 mid),向左收敛
            }
        }

        // 2. 收敛点即峰顶(题目保证山脉结构,无需校验)
        return left;
    }
};

四、易错点分析

难点1:比较的是 arr[mid]arr[mid+1] 而非 target

cpp 复制代码
if (arr[mid] < arr[mid + 1])
    left = mid + 1;
else
    right = mid;

传统二分比较 arr[mid]target,本题没有 target,比较对象是相邻元素 。判据 arr[mid] < arr[mid+1] 回答的是"mid 在峰顶的哪一侧 "------上升段在左、下降段在右。把判据换成比较 mid 与固定值,或漏掉 mid+1 的越界检查,是本题最常见的错误方向。

难点2:为什么判据成立说明 mid 一定在"上升段"而非其他

cpp 复制代码
// 数组 0 2 1 0:mid=1 时 arr[1]=2 > arr[2]=1 → 下降段
// 数组 0 2 5 3:mid=1 时 arr[1]=2 < arr[2]=5 → 上升段

二段性保证:峰顶左侧所有相邻对都是上升 ,右侧都是下降 。因此只要 arr[mid] < arr[mid+1],mid 必在峰顶左侧(或恰好是峰顶前一位),峰顶在 [mid+1, right];反之 mid 在峰顶右侧或恰为峰顶,峰顶在 [left, mid]理解二段性的单调变化是正确推导收缩方向的关键。

难点3:收缩方向与 mid 取整的匹配

cpp 复制代码
int mid = left + (right - left) / 2;    // 下取整
...
right = mid;                            // 向左收缩保留 mid

下降段执行 right = mid ,要求 mid 下取整 :相邻区间(right = left + 1)时 mid 取 left,right = mid 使区间缩小,不会死循环 。若误用上取整(如照抄 35 题模板),相邻时 mid 取 right,right = mid 不缩小,死循环。模板必须成套使用。

难点4:arr[mid + 1] 的越界风险

cpp 复制代码
while (left < right)
{
    int mid = ...;
    if (arr[mid] < arr[mid + 1])   // 访问 mid+1

循环条件 left < right 保证 mid < right ≤ n-1,因此 mid + 1 ≤ n-1访问永远不越界 。若把循环改成 left <= right 或 mid 上取整,mid 可能等于 n-1 甚至 rightarr[mid+1]越界 。这是把"相邻比较"写进错误循环条件时的高频 bug。

五、流程图

🎯 闭幕

🎉 恭喜你完成了「山脉数组的峰顶索引」问题的学习!

为了巩固知识并进一步拓展,建议你:

🚀 动手实践

在 LeetCode 上提交代码,尝试不同的测试用例。

💡 深入思考

  • 本题利用山脉数组的 二段性 (先增后减),通过比较 arr[mid]arr[mid+1] 来判断 mid 在上升段还是下降段。为什么只比较相邻元素就能确定峰顶的方位? 这背后的单调性依据是什么?

  • 循环条件是 left < right,最后返回 left(或 right)。为什么不需要像传统二分那样在循环后校验结果? 山脉数组的结构保证了什么?

  • 本题要求时间复杂度 O(log n)。如果使用线性扫描找峰顶,时间复杂度是多少?n = 10^5 时两种方法的效率差异有多大?

如果你觉得本文对你有所帮助,欢迎:

👍 点赞 / 收藏

👤 关注作者 ,获取更多题解

💬 留言交流你的疑问或优化思路


📌 深入思考答案

  • 相邻元素比较 的核心依据是:若 arr[mid] < arr[mid+1],说明 mid 处于 上升段 ,峰顶一定在 mid 的右侧(因为右侧还有更高的元素);反之,若 arr[mid] > arr[mid+1],说明 mid 处于 下降段 ,峰顶一定在 mid 或其左侧。这是山脉数组"先增后减"的单调性决定的。

  • 无需校验:因为题目保证数组是山脉数组,峰顶一定存在且唯一,二分收敛点必然就是峰顶,不需要额外检查。

  • 线性扫描 O(n),而二分 O(log n),对于 n=10^5,线性扫描 10^5 次,二分只需约 17 次比较,效率提升显著。

祝你在 算法之路 上越走越稳,早日攻克每一道难题!下次见 🚀✨

相关推荐
程与留2 小时前
05_Qt 核心模块概览——Qt Core、Gui、Widgets、Quick 的职责划分
c++·qt
buhuizhiyuci2 小时前
【QT-百日筑基篇】修真世界摸爬滚打多年,终于黄天不负有心人,突破炼器中期——常用控件QWight的属性
开发语言·c++·qt·gui·图形化
小雪崩3 小时前
嵌入式学习 day25:哈希表及排序与查找
linux·c语言·数据结构·学习·排序算法
白露与泡影3 小时前
解密 Pi 的 Harness 工程:Agent 会话如何实现持久化与恢复
java·人工智能·算法
happyprince5 小时前
01-整体观-Codex全貌解读(源码)
算法·ai编程
俊基科技5 小时前
别自己写算法了,让这颗37.5毫米的模组替你“闭嘴干活”
神经网络·算法·硬件开发·ai降噪·语音模块·回音消除
_Narcissus_5 小时前
动态规划初探(含完整代码实现)
c语言·数据结构·c++·算法·动态规划·背包问题·最短路径
世人万千丶6 小时前
鸿蒙项目实战 - 社区活动编排板:标签云布局算法与自动换行
学习·算法·华为·harmonyos·鸿蒙