寻找峰值算法解析:二分查找如何破解无序数组的局部极大值问题

发布时间:2026/10/10 7:29:43
寻找峰值算法解析:二分查找如何破解无序数组的局部极大值问题 1. 峰值问题的本质与题目拆解刷算法题的人应该都有过这种经验一道题目看起来很简单第一眼扫过去甚至觉得有点“弱智”但真到了要写出最优解的时候才发现里面有坑。LeetCode上的“寻找峰值”Find Peak Element就属于这种类型的题目。这道题的所有描述其实都可以浓缩成一句话给定一个整数数组 nums找到一个峰值元素并返回其索引。所谓峰值就是同时大于左右两个相邻元素的元素。注意两个条件数组的边界元素只需要大于它的那一个邻居即可算峰值如果数组只有一个元素那它自身就是峰值。比如数组 [1, 2, 3, 1]3 比 2 大、比 1 大所以索引 2 就是一个峰值。再比如 [1, 2, 1, 3, 5, 6, 4]这里有两个峰值索引 1 上的 2 和索引 5 上的 6题目只要求返回任意一个即可。从热搜关键词“leecode必刷基础算法题”“python算法思维题”“java常见算法题”可以看出这道题之所以被高频提及核心原因是它考察的不只是“能不能找到峰值”而是“能不能在 O(log n) 的时间复杂度内找到峰值”——这直接指向了二分查找。这里要先浇一盆冷水很多人第一反应是用线性扫描从头到尾遍历一遍找到第一个满足 nums[i] nums[i1] 的位置。这个解法在 LeetCode 上是能通过的因为题目只要求找到任意峰值线性扫描最坏情况下也是 O(n)对大多数测试用例都能跑过。但这个解法只达到了“能解出来”的层面距离“理解这道题在考什么”还很远。面试或笔试中出现这道题考察的重点几乎必然是二分查找。原因也很简单能够用二分查找解决一个并不具备全局有序性质的数组问题这种思维转变才是很多算法爱好者的分水岭。所以我在写这篇分析时会按照“为什么二分查找在这里能成立”这条主线来拆解。这也是整道题最核心的价值所在。我会用 Python 和 Java 两种主流语言分别给出实现并罗列我在排查过程中踩过的典型麻烦。2. 解题思路的演进从暴力到二分2.1 线性扫描为何是大多数人的第一反应先承认一件事线性扫描是这道题的“正常”思路。因为峰值的定义本身就是局部的一个元素大于左右邻居那就是峰值。遍历一遍逐个比较 nums[i] 和 nums[i1] 的关系找到第一个出现下降的位置那个位置就是一个峰值。举一个例子nums [1, 2, 3, 4, 1]从索引 0 开始往后看nums[0] 1nums[1] 2上升中。nums[1] 2nums[2] 3上升中。nums[2] 3nums[3] 4上升中。nums[3] 4nums[4] 1出现第一次下降。此时索引 3 上的 4 同时大于 nums[2] 的 3 和 nums[4] 的 1所以直接返回 3 即可。如果数组全程递增比如 [1, 2, 3, 4, 5]那最后一个元素天然满足峰值条件——因为它只有左侧邻居而 5 4。线性扫描的时间复杂度是 O(n)空间复杂度 O(1)。代码也很简洁但问题是如果这道题的数组长度是百万级别或者面试官追问“能不能更快”线性扫描就有点拿不出手了。这里还要补充一个细节线性扫描还有一种写法是用哨兵或者说越界处理来统一边界判断。比如将 nums[-1] 和 nums[n] 都视为负无穷。这种思路本身没有错甚至在某些场景下能让代码更干净。但它改变不了时间的数量级所以它只是过渡方案。2.2 二分查找为什么能用在无序数组上这是整道题最反直觉的地方。提到二分大家的肌肉记忆往往是“数组必须是有序的”因为二分靠的是中间元素与目标值大小的比较来排除一半的搜索空间。可“寻找峰值”这道题数组可能完全无序。那它还怎么二分关键在于这道题里我们不需要找一个具体的值我们只需要找一个局部极大值。而判断“峰值在左半边还是右半边”的依据不是值的大小关系而是趋势方向。逻辑是这样的取中间位置 mid比较 nums[mid] 和 nums[mid1]。如果 nums[mid] nums[mid1]说明在 mid 到 mid1 这一段数组处于上升趋势。那峰值一定不会出现在 mid 的左侧包括 mid吗不一定说绝对没有但可以确定的是从 mid 往右走由于 nums[mid1] 比 nums[mid] 大而且数组右侧必然存在边界隐含的负无穷那么在 [mid1, 末尾] 这个区间内要么持续上升直到末尾那末尾元素本身就是峰值要么中间某个位置出现下降那那个转折点就是峰值。这有点像一个推理题你从山腰往上走只要一直是上坡就一定能走到山顶——因为山的尽头一定有边界边界外的海拔是负无穷。反之如果 nums[mid] nums[mid1]说明从 mid 到 mid1 是下降趋势那峰值一定存在于 mid 左侧或就在 mid 本身。因为从 mid 往左走有隐含的负无穷边界左侧要么持续上升到边界要么先出现一个下降的转折点无论哪种情况都能推出峰值存在。这就是这道题二分法的数学基础**只要确定了斜率的正负就能确定峰值存在于哪一边。**它不需要数组整体有序只需要你在每一步都能有依据地排除掉一半的搜索区间。2.3 边界和等值情况的处理逻辑这里有一个很多新手容易卡住的地方如果 nums[mid] 和 nums[mid1] 相等怎么办在 LeetCode 原题中有一个被忽视的前提条件——相邻元素不相等。也就是说 nums 中任意两个相邻元素都是严格不等的关系。题目说明了“你可以假设 nums[-1] nums[n] -∞”并且给出的约束是相邻元素互异。所以如果你在做 LeetCode 162 的话等值情况其实不需要处理。但如果是在一些自行设计的题或者面试扩展场景里出现了相邻元素相等那么策略应该是在nums[mid] nums[mid1]时把搜索区间挪到左半部分。这里用“而不是“”是为了保证区间收缩的一致性和防止死循环。这种取舍本质上是一种工程上的兜底写法。我在第三章的代码里也会体现这个细节。同时也要注意另一个边界right 收缩到和 left 相遇时循环结束返回 left 即可。因为此时搜索区间只有一个元素而在题目定义下这个元素一定是峰值——整个区间由二分过程保证收缩到了一个必然存在峰值的子区间中。3. 完整代码实现与核心参数解析3.1 二分查找的标准模板我用 Python 来写第一个版本。代码可以非常短但每个变量都有它的意义def find_peak_element(nums): left, right 0, len(nums) - 1 while left right: mid (left right) // 2 if nums[mid] nums[mid 1]: right mid else: left mid 1 return left核心逻辑只有这几行。拆开来看left和right表示当前搜索区间初始为整个数组。取中点mid为了防溢出可以用left (right - left) // 2。比较nums[mid]和nums[mid1]下降说明峰值在左半边把right收缩到mid。上升说明峰值在右半边把left收缩到mid 1。最终left right的位置一定会落在某个峰值上。这也符合上一节推演出的“总能在收缩后的区间内找到峰值”的结论。这里有个细节值得讲一下为什么下降时是right mid而不是right mid - 1因为在下降的场景中nums[mid]本身也可能是峰值它同时大于nums[mid-1]和nums[mid1]的情况是存在的所以不能贸然把mid排除。而上升时nums[mid]必然不可能成为峰值因为nums[mid] nums[mid1]所以可以大胆地left mid 1。3.2 Java 版本的实现与区别用 Java 写的话逻辑完全一样只是语法层面的差异。我这里会给一个带注释的版本public class FindPeakElement { public int findPeakElement(int[] nums) { int left 0, right nums.length - 1; while (left right) { int mid left (right - left) / 2; if (nums[mid] nums[mid 1]) { right mid; } else { left mid 1; } } return left; } }Java 版本需要注意的点用left (right - left) / 2来避免(left right)整型溢出的问题。Java 中/是向下取整对于非负整数来说和 Python 的//行为一致不会出现负数除法的问题。如果使用ArrayIndexOutOfBoundsException出现在mid1上基本可以断定是循环条件写成了left right。因为在left right时mid left此时mid1可能越界。3.3 参数与运行过程对照为了更直观地展示二分查找是怎么一步步锁定的我拿一个实际数组来模拟。nums [1, 2, 1, 3, 5, 6, 4]步骤leftrightmidnums[mid] vs nums[mid1]区间变化10633 5上升left 424656 4下降right 534545 6否上升left 5455循环结束返回 5索引 5 上对应的值是 6它同时大于 nums[4] 的 5 和 nums[6] 的 4是合法的峰值。这个例子故意选了一个有多个峰值的数组你可以看到算法最终返回了其中一个峰值索引 5而不是唯一的峰值。这也符合题目“返回任意一个峰值”的要求。4. 多语言实现对比与易错点排查实录4.1 Python 与 Java 的实现差异两种语言在写法上的差异主要集中在这几点循环条件都是while (left right)或者while left right。中值计算方式Python 直接用//Java 写left (right - left) / 2。返回值都是 left。我在多个平台上刷过这道题也帮人看过代码Python 版容易踩的坑是递归写法导致的栈溢出Java 版容易踩的坑是mid 1越界访问。核心逻辑没有任何语言层面的区别所以这道题非常适合作为跨语言的基础练习题。如果非要说语言层面的差异那只有一种情况需要关注当数组长度非常长时Python 的递归写法自己写的递归函数而不是循环可能会导致递归深度超过默认限制。但用循环就没这个问题。4.2 一个细节数组长度为 1 的处理这是我在各种讨论帖里看到新手翻车频率最高的边界条件。nums [2]按定义这个元素左右都没邻居只有它自己。题目设定里边界外的元素视为负无穷因此 2 -∞ 且 2 -∞它自然就是峰值。上面的二分代码在left 0, right 0时循环条件left right直接就 false 了不会进入循环返回 0正确。如果你用的是暴力解法且把边界判断写错比如要求i 0 i n-1或者忘了处理单元素数组就会直接返回 -1 或者空值。这也是为什么我建议直接掌握二分模板它天然对单元素数组免疫。4.3 峰值不一定唯一时该返回哪一个题目只要求任意一个峰值所以你在设计算法时不需要“找最大峰值”或者“找最左侧峰值”。但有些变种题会加条件比如“如果存在多个峰值返回索引最小的那个”或者“返回峰值中数值最大的那个索引”这些就需要额外的策略。这里一并说一下扩展思路求最左侧峰值线性扫描从左边开始找到第一个就停。这样找到的一定是最左侧峰值。求最大峰值那就得遍历全部O(n) 跑不掉的。如果要求 O(log n) 但又要求最左侧或最大峰值题目本身的条件就需要重新审视——通常需要数组具备某种更强的约束否则无解。我在踩过的坑里最典型的一次就是默认“峰值唯一”结果测试用例里出现了两个峰值返回了不满足题目要求的那个。所以解题前一定要看清题目描述中关于“任意峰值”和“峰值唯一”的限定词。4.4 边界条件与单调递增递减数组的归宿先看单调递增的数组nums [1, 2, 3, 4, 5, 6]二分过程mid 2nums[2] 3nums[3] 4上升left 3。mid 4nums[4] 5nums[5] 6上升left 5。返回 5。索引 5 对应的 6 就是峰值因为它只有左邻居 5。边界视为负无穷所以它同时大于 5 和负无穷。再看单调递减数组nums [6, 5, 4, 3, 2, 1]mid 2nums[2] 4nums[3] 3下降right 2。mid 1nums[1] 5nums[2] 4下降right 1。mid 0nums[0] 6nums[1] 5下降right 0。返回 0。索引 0 对应的 6 是峰值因为它大于右邻居 5左边界视为负无穷。这两个极端情况在二分模板下都能自动收敛到正确端点也反向验证了“相邻元素不相等边界负无穷”这两个条件下峰值必然存在的数学结论。4.5 高频坑位清单根据我自己的实战经验以及在网上评论区看到的高频问题列一个易错点清单易错点问题表现解决方案循环条件写错left right导致死循环或数组越界统一用left right最后返回 left收缩区间写反把上升误判为向左搜索直接结果错误牢记“上升则向左收缩下降则向右收缩”的方向判定依赖于nums[mid]与nums[mid1]的关系nums[mid] nums[mid1]时向右时向左mid1 越界数组访问越界异常循环条件用left right后mid 最大也只能是right-1mid1 不会越界忽视单元素数组返回空值或 -1模板天然支持单元素数组不要额外加自定义判断混淆“任意峰值”和“全局最大”期望返回唯一的全局最大值但题目允许多个峰值先读题确认 whether it asks for “任意峰值” 或者 “唯一峰值”这些坑我在一开始刷题的时候基本都踩过一遍尤其是循环条件的处理。如果你也遇到类似问题可以先对照这个表格自查。5. 从真题视角谈这道题的考核意义与后续扩展5.1 为什么面试官爱拿这道题考人“寻找峰值”在很多大厂面试题单里出现频率不低。相比那些动辄几百行代码的题目这道题代码量极小但它能在很短时间内暴露一个人的算法思维水平第一层能不能想到线性扫描能想到说明基本的数据结构基础没丢。第二层能不能从线性扫描优化到二分能想到说明对算法复杂度的敏感度达标。第三层能不能解释清楚“为什么无序数组也能二分”能说清说明对算法原理的理解是真正到位的。第四层能不能处理边界条件、写出无 Bug 又简洁的代码能说明工程能力合格而不仅仅是会背模板。这道题的巧妙之处在于它把“二分”从“有序数组的专属武器”变成了“只要你能找到一个收紧搜索范围的依据任何场景都可以二分”。这个思维方式通常也叫“二分的本质是排除法”在算法类博客中经常被讨论在后续很多题目里都会用到。5.2 后续扩展题单从这道题出发可以顺藤摸瓜刷几条相关的题形成一个小专题LeetCode 852山脉数组的峰顶索引。题目给出一个先递增后递减的数组要求找出峰顶索引。这道题就是“寻找峰值”的特例数组一定是单峰结构。LeetCode 1095山脉数组中查找目标值。多重二分思想的入门题先找峰顶再两侧分别二分查找。LeetCode 33搜索旋转排序数组。这种题考察的是“局部有序”时的二分变体和峰值问题在思路上同源。“在看似无序的序列中找到某种局部有序性再用二分排除”这个手感在这类题里反复用到。LeetCode 153寻找旋转排序数组中的最小值。同样涉及如何用二分定位数组的“断裂点”和峰值题是姊妹题。这些题按顺序刷下来你对“二分查找的两个关键要素——目标比较和区间收缩依据”会有更体系化的认识。5.3 我个人在实战中的体会我在刚开始刷这道题的时候其实也是先用线性扫描过的当时觉得题目很没意思一行代码就结束了。后来在准备面试复盘答案的时候才认真去研究二分写法背后的推导逻辑才意识到这道题完全不简单。它最值钱的部分不是那十几行代码本身而是“上升则向右下降则向左”这个反直觉的判断规则是如何被推理出来的。后来我把这道题作为一个“二分变体思维”的母题延伸去刷了旋转排序数组系列和山脉数组系列明显感觉到思维上有了一个质的提升。所谓算法思维很多时候就是靠这种“把一个具体问题的解法抽象成可迁移的策略”积累出来的。6. 排查思路与定位技巧补充写完代码只是第一步。在实际刷题环境下这里主要指 LeetCode 或其他在线判题平台提交代码后还可能出现各种报错。这一节专门整理排查技巧。6.1 环境运行差异Python 环境下如果用递归方式实现了二分查找而不是循环要注意sys.setrecursionlimit的问题。虽然这道题数组长度不会大到触发递归上限但如果是长数组或者递归深度写错了会直接抛RecursionError。面试当场遇到这个报错会非常尴尬。所以我建议统一用循环实现既省栈空间又直观。Java 环境下常见的编译错误发生在变量声明和数组访问上mid没有在循环外声明时作用域问题可能导致 return 时找不到变量。在if分支内声明了mid而循环外面又使用它编译直接报错。nums.length少写了()或者多了()都会编译错误。6.2 测试用例建议自己调试时建议准备好这几组测试数据单元素数组[5]期望返回 0。两个元素数组[1, 2]期望返回 1[2, 1]期望返回 0。完全递增数组[1, 2, 3, 4, 5]期望返回最后一个索引。完全递减数组[5, 4, 3, 2, 1]期望返回第一个索引。多峰值数组[1, 5, 3, 4, 2]期望返回任意一个合法峰值即可。只有中间一个峰值的数组[1, 2, 3, 4, 3, 2, 1]期望返回索引 3。用这几组用例做完自测边界条件基本就覆盖全了。6.3 如何从错误输出反向定位如果提交后返回的结果不是峰值通常问题出在比较符号上。比如把nums[mid] nums[mid1]写成了在允许相邻相等的题目里也许没问题但在 LeetCode 162 的约束下不会出错。可如果你手误写成了那区间收缩方向就全反了结果往往是返回一个非峰值位置。另一个常见错误是循环结束后返回了right而不是left。由于循环结束时两者相等返回谁理论上没区别。但如果你在循环条件或区间收缩上有一点点不对称那right和left终止时可能不相等。这时候返回left的容错性更高因为左边界始终是那个“已被证明可能存在峰值”的搜索下限。我试过在这样的不对称版本里返回 right结果在某些数组上就错了一个位置。6.4 调试工具的使用建议如果你在本地 IDE 里调试建议在循环里加一行临时的边界打印print(fleft{left}, right{right}, mid{mid}, val_left{nums[left]}, val_right{nums[right]})这样可以直观看到区间每一步如何收缩。定位到某一轮收缩方向不对就可以对照我前面给的“上升/下降决定搜索方向”的推理来检查代码。在我个人的刷题习惯里通常会在写出一个能通过的解法之后再手动推演两三个数组的完整二分过程确保理解透了再提交而不是靠试错去碰答案。这道题推演过程非常短对培养手感也很有帮助。这篇分析到这里就基本把“寻找峰值”这道题从题意、思路、代码、扩展到排查完整串联起来了。最后再分享一个小经验这道题我大概在不同阶段刷过不下五遍每一次都有新的领悟。第一遍是背模板第二遍是理解模板第三遍开始反思“为什么可以这样做”第四遍尝试把它迁移到其他题目上第五遍能在写代码前就画出完整的推导链条。如果你现在觉得二分查找处理不了无序数组不妨拿这道题作为突破口把那个“转折点”彻底想透比多刷十道简单题都管用。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询