题目描述
题目链接:
力扣852. 山脉数组的峰顶索引
题目描述:
示例 1:
输入: arr = [0,1,0]
输出: 1
示例 2:
输入: arr = [0,2,1,0]
输出: 1
示例 3:
输入: arr = [0,10,5,2]
输出: 1
注意:
3 <= arr.length <= 105
0 <= arr[i] <= 106
题目数据 保证 arr 是一个山脉数组
为什么这道题值得弄懂?
这道题是二分查找的经典**“单调性分段”问题**——山脉数组虽整体不单调,但可根据“峰顶左侧递增、右侧递减”的特性,划分出具有明确规律的两段区间。通过这道题,能进一步掌握“如何根据题目特性定义二分的判断条件”,并强化对“指针移动逻辑”“边界初始化”等细节的理解,为解决更复杂的分段查找问题(如旋转数组查找)打下基础。
为什么可以用二分?
二分查找的核心是利用数组的“二段性”——将数组分为“满足某条件”和“不满足某条件”的两部分,每次舍弃其中一部分,从而将时间复杂度压缩到 O(log n)。
本题中,山脉数组的“二段性”体现在:
- 对于任意下标
mid
,若arr[mid] < arr[mid + 1]
:说明mid
处于递增段,峰顶一定在mid
的右侧(包括mid + 1
); - 若
arr[mid] > arr[mid + 1]
:说明mid
处于递减段,峰顶一定在mid
的左侧(包括mid
)。
基于此特性,我们可通过二分不断缩小区间,最终定位到唯一的峰顶下标。
二分查找的核心思路:基于“递增/递减段”划分区间
本题的核心是通过 arr[mid]
与 arr[mid + 1]
的大小关系,判断 mid
所在的分段,进而确定峰顶的位置范围,逐步缩小区间。
1. 关键细节:边界初始化(为什么 left=1,right=arr.size()-2?)
题目明确给出两个关键条件:
- 山脉数组严格递增后严格递减,且长度 ≥ 3;
- 峰顶下标
i
满足0 < i < arr.length - 1
(即峰顶不可能是数组的第一个或最后一个元素)。
因此,我们无需从 0
和 arr.size()-1
开始查找,直接将初始边界设为:
left = 1
:排除第一个元素(不可能是峰顶);right = arr.size() - 2
:排除最后一个元素(不可能是峰顶)。
这一初始化不仅减少了不必要的查找次数,还避免了后续判断中“mid+1 越界”的风险(因 right
最大为 arr.size()-2
,mid
最大为 arr.size()-2
,mid+1
最大为 arr.size()-1
,属于合法下标)。
2. 二段性划分与判断条件
以 arr[mid] < arr[mid + 1]
作为核心判断条件,将数组分为两段:
- 满足条件(递增段):
arr[mid] < arr[mid + 1]
→ 峰顶在mid
右侧(mid + 1
到right
); - 不满足条件(递减段):
arr[mid] > arr[mid + 1]
→ 峰顶在mid
左侧(left
到mid
)。
示例:若 arr = [0,10,5,2]
,取 mid = 1
(left=1, right=2
):
arr[1] = 10
,arr[2] = 5
,满足arr[mid] > arr[mid + 1]
→ 峰顶在left=1
到mid=1
之间,直接定位到峰顶。
3. 指针移动逻辑
根据上述二段性划分,指针移动规则如下:
- 当
arr[mid] < arr[mid + 1]
时:mid
在递增段,峰顶一定在mid
右侧,因此舍弃左侧区间(包括mid
),将left
更新为mid + 1
; - 当
arr[mid] > arr[mid + 1]
时:mid
在递减段,峰顶一定在mid
左侧(或mid
本身),因此舍弃右侧区间(不包括mid
),将right
更新为mid
。
关键逻辑:为什么 arr[mid] > arr[mid + 1]
时 right = mid
?
因为 mid
有可能就是峰顶(例如 arr = [0,2,1]
,mid=1
时 arr[1] > arr[2]
,mid
本身就是峰顶),若将 right
更新为 mid - 1
,会漏掉峰顶,导致结果错误。
4. 循环结束条件
循环条件设为 left < right
,当 left == right
时,区间缩小到唯一元素,该元素就是峰顶的下标,循环结束。
至于为什么无需进一步判断,是因为二分的每一步都严格根据“递增/递减段”缩小范围,最终 left
和 right
会收敛到唯一的峰顶下标,无需额外验证。
5. 中间值(mid)的取法
采用向下取整:mid = left + (right - left) / 2
(等价于 (left + right) // 2
,避免整数溢出)。
为什么不用向上取整?
若用向上取整(mid = left + (right - left + 1) / 2
),当区间长度为 2 时(如 left=1, right=2
,arr = [0,3,2]
):
- 计算
mid = 1 + (2-1+1)/2 = 2
; - 因
arr[2] > arr[3]
(此处arr[3]
不存在,实际mid=2
是right
边界,arr[2] > arr[3]
不成立,实际判断为arr[2] > arr[3]
会越界?不,实际right
最大为arr.size()-2
,mid
最大为arr.size()-2
,mid+1
为arr.size()-1
,合法)。
更关键的问题是:当left=1, right=2
且arr[1] < arr[2]
时(如arr = [0,1,3,2]
,left=1, right=2
): - 向上取整
mid=2
,判断arr[2] > arr[3]
(成立),则right=2
; - 此时
left=1 < right=2
,循环继续,再次计算mid=1 + (2-1+1)/2=2
,陷入死循环。
而向下取整可避免此问题:
- 同样
left=1, right=2
,mid=1 + (2-1)/2=1
; - 若
arr[1] < arr[2]
,则left=mid+1=2
,此时left==right
,循环结束,无死循环。
代码实现
class Solution {
public:
int peakIndexInMountainArray(vector<int>& arr) {
// 边界初始化:排除首尾元素(不可能是峰顶)
int left = 1, right = arr.size() - 2;
// 循环缩小区间,直到left == right(定位到峰顶)
while (left < right) {
// 中间值向下取整,避免死循环
int mid = left + (right - left) / 2;
if (arr[mid] < arr[mid + 1]) {
// mid在递增段,峰顶在右侧,舍弃左侧
left = mid + 1;
} else {
// mid在递减段,峰顶在左侧(含mid),舍弃右侧
right = mid;
}
}
// 循环结束时,left == right,即为峰顶下标
return left;
}
};
总结:二分查找的关键细节复盘
- 边界初始化要结合题目条件:利用“峰顶不可能是首尾元素”的特性,将
left
设为 1、right
设为arr.size()-2
,减少查找次数并避免越界; - 判断条件要紧扣“二段性”:通过
arr[mid]
与arr[mid+1]
的大小关系,明确mid
所在的分段(递增/递减),进而确定峰顶的范围; - 指针移动要避免“漏解”:当
arr[mid] > arr[mid+1]
时,right = mid
(而非mid-1
),防止漏掉mid
本身就是峰顶的情况; - 中间值取整要匹配循环逻辑:因
left
可能更新为mid+1
、right
可能更新为mid
,需用向下取整避免区间长度为 2 时的死循环。
这道题的核心是“利用局部单调性划分二段区间”,掌握这一思路后,面对旋转数组、分段有序数组等更复杂的查找问题,也能快速找到二分的判断条件和边界逻辑。
下一篇题目预告
本次我们通过“山脉数组峰顶索引”掌握了“局部单调性+二段性”的二分思路,下一篇将进阶讲解同类型但更灵活的题目——力扣162. 寻找峰值。该题中数组可能存在多个峰值,且边界可视为“无穷小”,需要在本次思路基础上调整判断逻辑,感兴趣的朋友可以提前浏览题目,思考如何用二分解决,我们下一篇详细拆解!
“Doro又又又带着小花🌸来啦!这道山脉数组的峰顶索引,是不是把‘局部单调性+二分二段性’的细节讲透了呀?不管是‘left从1开始、right到size-2结束’的边界技巧,还是‘mid向下取整避免死循环’的关键逻辑,只要把这些点吃透,以后遇到‘分段有序数组找目标’的题,就能快速找到突破口啦~
如果觉得这篇拆解帮你理清了二分的思路,别忘了点赞收藏呀!下次写二分题想不起来边界怎么设、指针怎么移时,翻到这篇就能快速回忆细节~
关注博主,后面还会一起攻克更多二分变形题,比如下一篇要讲的「寻找峰值」,从‘会写二分’到‘写对二分’,咱们一步一步慢慢进阶!”